Skip to content

Commit c9c55a9

Browse files
authored
(fix) code actions for ts-checked files (#1002)
- fix negative line of the "add ts-nocheck" code action which prevents other code actions in ts-checked js-files from showing up - fix alignment and positioning of other code actions such as ts-ignore
1 parent fcf10f9 commit c9c55a9

File tree

8 files changed

+235
-74
lines changed

8 files changed

+235
-74
lines changed

packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
TextEdit
1313
} from 'vscode-languageserver';
1414
import { mapObjWithRangeToOriginal, offsetAt, positionAt } from '../../../../lib/documents';
15-
import { pathToUrl } from '../../../../utils';
15+
import { getIndent, pathToUrl } from '../../../../utils';
1616
import { SvelteDocument } from '../../SvelteDocument';
1717
import ts from 'typescript';
1818
// estree does not have start/end in their public Node interface,
@@ -111,7 +111,7 @@ async function getSvelteIgnoreEdit(svelteDoc: SvelteDocument, ast: Ast, diagnost
111111
transpiled.getText()
112112
);
113113
const afterStartLineStart = content.slice(nodeLineStart);
114-
const indent = /^[ |\t]+/.exec(afterStartLineStart)?.[0] ?? '';
114+
const indent = getIndent(afterStartLineStart);
115115

116116
// TODO: Make all code action's new line consistent
117117
const ignore = `${indent}<!-- svelte-ignore ${code} -->${EOL}`;

packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts

Lines changed: 128 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
isInTag,
1616
getLineAtPosition
1717
} from '../../../lib/documents';
18-
import { pathToUrl, flatten, isNotNullOrUndefined, modifyLines } from '../../../utils';
18+
import { pathToUrl, flatten, isNotNullOrUndefined, modifyLines, getIndent } from '../../../utils';
1919
import { CodeActionsProvider } from '../../interfaces';
2020
import { SnapshotFragment, SvelteSnapshotFragment } from '../DocumentSnapshot';
2121
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
@@ -163,76 +163,93 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
163163
const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
164164
docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
165165

166-
return await Promise.all(
167-
codeFixes.map(async (fix) => {
168-
const documentChanges = await Promise.all(
169-
fix.changes.map(async (change) => {
170-
const { snapshot, fragment } = await docs.retrieve(change.fileName);
171-
return TextDocumentEdit.create(
172-
OptionalVersionedTextDocumentIdentifier.create(
173-
pathToUrl(change.fileName),
174-
null
175-
),
176-
change.textChanges
177-
.map((edit) => {
178-
if (
179-
fix.fixName === 'import' &&
180-
fragment instanceof SvelteSnapshotFragment
181-
) {
182-
return this.completionProvider.codeActionChangeToTextEdit(
183-
document,
184-
fragment,
185-
edit,
186-
true,
187-
isInTag(range.start, document.scriptInfo) ||
188-
isInTag(range.start, document.moduleScriptInfo)
189-
);
190-
}
191-
192-
if (
193-
!isNoTextSpanInGeneratedCode(
194-
snapshot.getFullText(),
195-
edit.span
196-
)
197-
) {
198-
return undefined;
199-
}
200-
201-
let originalRange = mapRangeToOriginal(
202-
fragment,
203-
convertRange(fragment, edit.span)
204-
);
205-
206-
if (fix.fixName === 'unusedIdentifier') {
207-
originalRange = this.checkRemoveImportCodeActionRange(
208-
edit,
209-
fragment,
210-
originalRange
211-
);
212-
}
213-
214-
if (fix.fixName === 'fixMissingFunctionDeclaration') {
215-
originalRange = this.checkEndOfFileCodeInsert(
216-
originalRange,
217-
range,
218-
document
219-
);
220-
}
221-
222-
return TextEdit.replace(originalRange, edit.newText);
223-
})
224-
.filter(isNotNullOrUndefined)
225-
);
226-
})
227-
);
228-
return CodeAction.create(
229-
fix.description,
230-
{
231-
documentChanges
232-
},
233-
CodeActionKind.QuickFix
166+
const codeActionsPromises = codeFixes.map(async (fix) => {
167+
const documentChangesPromises = fix.changes.map(async (change) => {
168+
const { snapshot, fragment } = await docs.retrieve(change.fileName);
169+
return TextDocumentEdit.create(
170+
OptionalVersionedTextDocumentIdentifier.create(
171+
pathToUrl(change.fileName),
172+
null
173+
),
174+
change.textChanges
175+
.map((edit) => {
176+
if (
177+
fix.fixName === 'import' &&
178+
fragment instanceof SvelteSnapshotFragment
179+
) {
180+
return this.completionProvider.codeActionChangeToTextEdit(
181+
document,
182+
fragment,
183+
edit,
184+
true,
185+
isInTag(range.start, document.scriptInfo) ||
186+
isInTag(range.start, document.moduleScriptInfo)
187+
);
188+
}
189+
190+
if (!isNoTextSpanInGeneratedCode(snapshot.getFullText(), edit.span)) {
191+
return undefined;
192+
}
193+
194+
let originalRange = mapRangeToOriginal(
195+
fragment,
196+
convertRange(fragment, edit.span)
197+
);
198+
199+
if (fix.fixName === 'unusedIdentifier') {
200+
originalRange = this.checkRemoveImportCodeActionRange(
201+
edit,
202+
fragment,
203+
originalRange
204+
);
205+
}
206+
207+
if (fix.fixName === 'fixMissingFunctionDeclaration') {
208+
originalRange = this.checkEndOfFileCodeInsert(
209+
originalRange,
210+
range,
211+
document
212+
);
213+
}
214+
215+
if (fix.fixName === 'disableJsDiagnostics') {
216+
if (edit.newText.includes('ts-nocheck')) {
217+
return this.checkTsNoCheckCodeInsert(document, edit);
218+
}
219+
220+
return this.checkDisableJsDiagnosticsCodeInsert(
221+
originalRange,
222+
document,
223+
edit
224+
);
225+
}
226+
227+
if (originalRange.start.line < 0 || originalRange.end.line < 0) {
228+
return undefined;
229+
}
230+
231+
return TextEdit.replace(originalRange, edit.newText);
232+
})
233+
.filter(isNotNullOrUndefined)
234234
);
235-
})
235+
});
236+
const documentChanges = await Promise.all(documentChangesPromises);
237+
return CodeAction.create(
238+
fix.description,
239+
{
240+
documentChanges
241+
},
242+
CodeActionKind.QuickFix
243+
);
244+
});
245+
246+
const codeActions = await Promise.all(codeActionsPromises);
247+
248+
// filter out empty code action
249+
return codeActions.filter((codeAction) =>
250+
codeAction.edit?.documentChanges?.every(
251+
(change) => (<TextDocumentEdit>change).edits.length > 0
252+
)
236253
);
237254
}
238255

@@ -398,6 +415,47 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
398415
return resultRange;
399416
}
400417

418+
private checkTsNoCheckCodeInsert(
419+
document: Document,
420+
edit: ts.TextChange
421+
): TextEdit | undefined {
422+
if (!document.scriptInfo) {
423+
return undefined;
424+
}
425+
426+
const newText = ts.sys.newLine + edit.newText;
427+
428+
return TextEdit.insert(document.scriptInfo.startPos, newText);
429+
}
430+
431+
private checkDisableJsDiagnosticsCodeInsert(
432+
originalRange: Range,
433+
document: Document,
434+
edit: ts.TextChange
435+
): TextEdit {
436+
const startOffset = document.offsetAt(originalRange.start);
437+
const text = document.getText();
438+
439+
// svetlte2tsx removes export in instance script
440+
const insertedAfterExport = text.slice(0, startOffset).trim().endsWith('export');
441+
442+
if (!insertedAfterExport) {
443+
return TextEdit.replace(originalRange, edit.newText);
444+
}
445+
446+
const position = document.positionAt(text.lastIndexOf('export', startOffset));
447+
448+
// fix the length of trailing indent
449+
const linesOfNewText = edit.newText.split('\n');
450+
if (/^[ \t]*$/.test(linesOfNewText[linesOfNewText.length - 1])) {
451+
const line = getLineAtPosition(originalRange.start, document.getText());
452+
const indent = getIndent(line);
453+
linesOfNewText[linesOfNewText.length - 1] = indent;
454+
}
455+
456+
return TextEdit.insert(position, linesOfNewText.join('\n'));
457+
}
458+
401459
private async getLSAndTSDoc(document: Document) {
402460
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
403461
}

packages/language-server/src/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,7 @@ export async function filterAsync<T>(
141141
)
142142
).filter((i) => i !== fail) as T[];
143143
}
144+
145+
export function getIndent(text: string) {
146+
return /^[ |\t]+/.exec(text)?.[0] ?? '';
147+
}

packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ import { pathToUrl } from '../../../../src/utils';
55
import ts from 'typescript';
66
import * as path from 'path';
77
import * as assert from 'assert';
8-
import { Range, Position, CodeActionKind, TextDocumentEdit } from 'vscode-languageserver';
8+
import {
9+
Range,
10+
Position,
11+
CodeActionKind,
12+
TextDocumentEdit,
13+
CodeAction
14+
} from 'vscode-languageserver';
915
import { CompletionsProviderImpl } from '../../../../src/plugins/typescript/features/CompletionProvider';
1016
import { LSConfigManager } from '../../../../src/ls-config';
1117

1218
const testDir = path.join(__dirname, '..');
1319

1420
describe('CodeActionsProvider', () => {
1521
function getFullPath(filename: string) {
16-
return path.join(testDir, 'testfiles', filename);
22+
return path.join(testDir, 'testfiles', 'code-actions', filename);
1723
}
1824

1925
function getUri(filename: string) {
@@ -152,6 +158,95 @@ describe('CodeActionsProvider', () => {
152158
]);
153159
});
154160

161+
it('provides quickfix for ts-checked-js', async () => {
162+
const { provider, document } = setup('codeaction-checkJs.svelte');
163+
const errorRange = Range.create(Position.create(2, 21), Position.create(2, 26));
164+
165+
const codeActions = await provider.getCodeActions(document, errorRange, {
166+
diagnostics: [
167+
{
168+
code: 2304,
169+
message: "Cannot find name 'blubb'.",
170+
range: errorRange
171+
}
172+
]
173+
});
174+
175+
for (const codeAction of codeActions) {
176+
(<TextDocumentEdit>codeAction.edit?.documentChanges?.[0])?.edits.forEach(
177+
(edit) => (edit.newText = harmonizeNewLines(edit.newText))
178+
);
179+
}
180+
181+
const textDocument = {
182+
uri: getUri('codeaction-checkJs.svelte'),
183+
version: null
184+
};
185+
assert.deepStrictEqual(codeActions, <CodeAction[]>[
186+
{
187+
edit: {
188+
documentChanges: [
189+
{
190+
edits: [
191+
{
192+
newText: '\nimport { blubb } from "../definitions";\n\n',
193+
range: Range.create(
194+
Position.create(0, 8),
195+
Position.create(0, 8)
196+
)
197+
}
198+
],
199+
textDocument
200+
}
201+
]
202+
},
203+
kind: 'quickfix',
204+
title: 'Import \'blubb\' from module "../definitions"'
205+
},
206+
{
207+
edit: {
208+
documentChanges: [
209+
{
210+
edits: [
211+
{
212+
newText: '// @ts-ignore\n ',
213+
range: Range.create(
214+
Position.create(2, 4),
215+
Position.create(2, 4)
216+
)
217+
}
218+
],
219+
textDocument
220+
}
221+
]
222+
},
223+
kind: 'quickfix',
224+
title: 'Ignore this error message'
225+
},
226+
{
227+
edit: {
228+
documentChanges: [
229+
{
230+
edits: [
231+
{
232+
newText: '\n// @ts-nocheck',
233+
range: Range.create(
234+
Position.create(0, 8),
235+
Position.create(0, 8)
236+
)
237+
}
238+
],
239+
textDocument
240+
}
241+
]
242+
},
243+
244+
kind: 'quickfix',
245+
title: 'Disable checking for this file'
246+
}
247+
]);
248+
});
249+
155250
it('organizes imports', async () => {
156251
const { provider, document } = setup('codeactions.svelte');
157252

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<script>
2+
// @ts-check
3+
export let abc = blubb;
4+
</script>

0 commit comments

Comments
 (0)