Skip to content

Commit 7409890

Browse files
authored
fix: component import quick-fix with "did you mean" diagnostics (#2373)
* fix: component import quick-fix when undeclared variable might be a typo * fix: combine fix sometime affect next document synchronization * fix-all as well * wording
1 parent 3147c81 commit 7409890

File tree

5 files changed

+185
-12
lines changed

5 files changed

+185
-12
lines changed

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

+25-9
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
276276
if (
277277
diagnostic.range.start.line < 0 ||
278278
diagnostic.range.end.line < 0 ||
279-
diagnostic.code !== DiagnosticCode.CANNOT_FIND_NAME
279+
(diagnostic.code !== DiagnosticCode.CANNOT_FIND_NAME &&
280+
diagnostic.code !== DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y)
280281
) {
281282
continue;
282283
}
@@ -313,10 +314,13 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
313314
const virtualDoc = new Document(virtualUri, newText);
314315
virtualDoc.openedByClient = true;
315316
// let typescript know about the virtual document
316-
await this.lsAndTsDocResolver.getSnapshot(virtualDoc);
317+
// getLSAndTSDoc instead of getSnapshot so that project dirty state is correctly tracked by us
318+
// otherwise, sometime the applied code fix might not be picked up by the language service
319+
// because we think the project is still dirty and doesn't update the project version
320+
await this.lsAndTsDocResolver.getLSAndTSDoc(virtualDoc);
317321

318322
return {
319-
virtualDoc: new Document(virtualUri, newText),
323+
virtualDoc,
320324
insertedNames: names
321325
};
322326
}
@@ -559,7 +563,9 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
559563
const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end));
560564
const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code));
561565
const cannotFindNameDiagnostic = context.diagnostics.filter(
562-
(diagnostic) => diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME
566+
(diagnostic) =>
567+
diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME ||
568+
diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y
563569
);
564570

565571
const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(
@@ -579,9 +585,14 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
579585
formatCodeSettings
580586
)
581587
: undefined;
582-
codeFixes =
583-
// either-or situation
584-
codeFixes || [
588+
589+
// either-or situation when it's not a "did you mean" fix
590+
if (
591+
codeFixes === undefined ||
592+
errorCodes.includes(DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y)
593+
) {
594+
codeFixes ??= [];
595+
codeFixes = codeFixes.concat(
585596
...lang.getCodeFixesAtPosition(
586597
tsDoc.filePath,
587598
start,
@@ -599,7 +610,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
599610
userPreferences,
600611
formatCodeSettings
601612
)
602-
];
613+
);
614+
}
603615

604616
const snapshots = new SnapshotMap(this.lsAndTsDocResolver);
605617
snapshots.set(tsDoc.filePath, tsDoc);
@@ -847,7 +859,11 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
847859
if (codeFix.fixName === FIX_IMPORT_FIX_NAME) {
848860
const allCannotFindNameDiagnostics = lang
849861
.getSemanticDiagnostics(fileName)
850-
.filter((diagnostic) => diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME);
862+
.filter(
863+
(diagnostic) =>
864+
diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME ||
865+
diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y
866+
);
851867

852868
if (allCannotFindNameDiagnostics.length < 2) {
853869
checkedFixIds.add(codeFix.fixId);

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

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export enum DiagnosticCode {
5252
MISSING_PROP = 2741, // "Property '..' is missing in type '..' but required in type '..'."
5353
NO_OVERLOAD_MATCHES_CALL = 2769, // "No overload matches this call"
5454
CANNOT_FIND_NAME = 2304, // "Cannot find name 'xxx'"
55+
CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y = 2552, // "Cannot find name '...' Did you mean '...'?"
5556
EXPECTED_N_ARGUMENTS = 2554, // Expected {0} arguments, but got {1}.
5657
DEPRECATED_SIGNATURE = 6387 // The signature '..' of '..' is deprecated
5758
}

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

+148-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDo
2020
import { __resetCache } from '../../../../src/plugins/typescript/service';
2121
import { pathToUrl } from '../../../../src/utils';
2222
import { recursiveServiceWarmup } from '../test-utils';
23+
import { DiagnosticCode } from '../../../../src/plugins/typescript/features/DiagnosticsProvider';
2324

2425
const testDir = path.join(__dirname, '..');
2526
const indent = ' '.repeat(4);
@@ -64,7 +65,7 @@ describe('CodeActionsProvider', function () {
6465
uri: pathToUrl(filePath),
6566
text: harmonizeNewLines(ts.sys.readFile(filePath) || '')
6667
});
67-
return { provider, document, docManager };
68+
return { provider, document, docManager, lsAndTsDocResolver };
6869
}
6970

7071
it('provides quickfix', async () => {
@@ -661,6 +662,81 @@ describe('CodeActionsProvider', function () {
661662
]);
662663
});
663664

665+
it('provides quickfix for component import with "did you mean" diagnostics', async () => {
666+
const { provider, document } = setup('codeaction-component-import.svelte');
667+
668+
const codeActions = await provider.getCodeActions(
669+
document,
670+
Range.create(Position.create(4, 1), Position.create(4, 6)),
671+
{
672+
diagnostics: [
673+
{
674+
code: 2552,
675+
message: "Cannot find name 'Empty'. Did you mean 'EMpty'?",
676+
range: Range.create(Position.create(4, 1), Position.create(4, 6)),
677+
source: 'ts'
678+
}
679+
],
680+
only: [CodeActionKind.QuickFix]
681+
}
682+
);
683+
684+
(<TextDocumentEdit>codeActions[0]?.edit?.documentChanges?.[0])?.edits.forEach(
685+
(edit) => (edit.newText = harmonizeNewLines(edit.newText))
686+
);
687+
688+
assert.deepStrictEqual(codeActions, <CodeAction[]>[
689+
{
690+
edit: {
691+
documentChanges: [
692+
{
693+
edits: [
694+
{
695+
newText: harmonizeNewLines(
696+
`\n${indent}import Empty from "../empty.svelte";\n`
697+
),
698+
range: {
699+
end: Position.create(0, 18),
700+
start: Position.create(0, 18)
701+
}
702+
}
703+
],
704+
textDocument: {
705+
uri: getUri('codeaction-component-import.svelte'),
706+
version: null
707+
}
708+
}
709+
]
710+
},
711+
kind: 'quickfix',
712+
title: 'Add import from "../empty.svelte"'
713+
},
714+
{
715+
edit: {
716+
documentChanges: [
717+
{
718+
edits: [
719+
{
720+
newText: 'EMpty',
721+
range: {
722+
end: Position.create(4, 6),
723+
start: Position.create(4, 1)
724+
}
725+
}
726+
],
727+
textDocument: {
728+
uri: getUri('codeaction-component-import.svelte'),
729+
version: null
730+
}
731+
}
732+
]
733+
},
734+
kind: 'quickfix',
735+
title: "Change spelling to 'EMpty'"
736+
}
737+
]);
738+
});
739+
664740
it('remove import inline with script tag', async () => {
665741
const { provider, document } = setup('remove-imports-inline.svelte');
666742

@@ -863,13 +939,15 @@ describe('CodeActionsProvider', function () {
863939
});
864940

865941
it('provide quick fix to fix all missing import component', async () => {
866-
const { provider, document } = setup('codeaction-custom-fix-all-component.svelte');
942+
const { provider, document, docManager, lsAndTsDocResolver } = setup(
943+
'codeaction-custom-fix-all-component.svelte'
944+
);
867945

868946
const range = Range.create(Position.create(4, 1), Position.create(4, 15));
869947
const codeActions = await provider.getCodeActions(document, range, {
870948
diagnostics: [
871949
{
872-
code: 2304,
950+
code: DiagnosticCode.CANNOT_FIND_NAME,
873951
message: "Cannot find name 'FixAllImported'.",
874952
range: range,
875953
source: 'ts'
@@ -912,6 +990,73 @@ describe('CodeActionsProvider', function () {
912990
}
913991
]
914992
});
993+
994+
// fix-all has some "creative" workaround. Testing if it won't affect the document synchronization after applying the fix
995+
docManager.updateDocument(
996+
document,
997+
resolvedFixAll.edit.documentChanges[0].edits.map((edit) => ({
998+
range: edit.range,
999+
text: edit.newText
1000+
}))
1001+
);
1002+
1003+
const { lang, tsDoc } = await lsAndTsDocResolver.getLSAndTSDoc(document);
1004+
const cannotFindNameDiagnostics = lang
1005+
.getSemanticDiagnostics(tsDoc.filePath)
1006+
.filter((diagnostic) => diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME);
1007+
assert.strictEqual(cannotFindNameDiagnostics.length, 0);
1008+
});
1009+
1010+
it('provide quick fix to fix all missing import component with "did you mean" diagnostics', async () => {
1011+
const { provider, document } = setup('codeaction-custom-fix-all-component4.svelte');
1012+
1013+
const range = Range.create(Position.create(4, 1), Position.create(4, 15));
1014+
const codeActions = await provider.getCodeActions(document, range, {
1015+
diagnostics: [
1016+
{
1017+
code: DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y,
1018+
message: "Cannot find name 'FixAllImported'. Did you mean 'FixAllImported3'?",
1019+
range: range,
1020+
source: 'ts'
1021+
}
1022+
],
1023+
only: [CodeActionKind.QuickFix]
1024+
});
1025+
1026+
const fixAll = codeActions.find((action) => action.data);
1027+
const resolvedFixAll = await provider.resolveCodeAction(document, fixAll!);
1028+
1029+
(<TextDocumentEdit>resolvedFixAll?.edit?.documentChanges?.[0])?.edits.forEach(
1030+
(edit) => (edit.newText = harmonizeNewLines(edit.newText))
1031+
);
1032+
1033+
assert.deepStrictEqual(resolvedFixAll.edit, {
1034+
documentChanges: [
1035+
{
1036+
edits: [
1037+
{
1038+
newText:
1039+
`\n${indent}import FixAllImported from \"./importing/FixAllImported.svelte\";\n` +
1040+
`${indent}import FixAllImported2 from \"./importing/FixAllImported2.svelte\";\n`,
1041+
range: {
1042+
start: {
1043+
character: 18,
1044+
line: 0
1045+
},
1046+
end: {
1047+
character: 18,
1048+
line: 0
1049+
}
1050+
}
1051+
}
1052+
],
1053+
textDocument: {
1054+
uri: getUri('codeaction-custom-fix-all-component4.svelte'),
1055+
version: null
1056+
}
1057+
}
1058+
]
1059+
});
9151060
});
9161061

9171062
it('provide quick fix to fix all missing import component without duplicate (script)', async () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script lang="ts">
2+
class EMpty {}
3+
</script>
4+
5+
<Empty />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script lang="ts">
2+
class FixAllImported3 {}
3+
</script>
4+
5+
<FixAllImported />
6+
<FixAllImported2 />

0 commit comments

Comments
 (0)