Skip to content

Commit cc6205d

Browse files
authored
(fix) filter out component import if already one present (#466)
#462 Drawback: Also will mark commented out imports as already imported, so there won't be svelte component completions in this case - but this seems like a bigger edge case than having double imports when doing barrel exports
1 parent 9f9fef9 commit cc6205d

File tree

5 files changed

+75
-3
lines changed

5 files changed

+75
-3
lines changed

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
mapRangeToOriginal,
1818
getNodeIfIsInComponentStartTag,
1919
} from '../../../lib/documents';
20-
import { isNotNullOrUndefined, pathToUrl } from '../../../utils';
20+
import { isNotNullOrUndefined, pathToUrl, getRegExpMatches, flatten } from '../../../utils';
2121
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
2222
import { SvelteSnapshotFragment, SvelteDocumentSnapshot } from '../DocumentSnapshot';
2323
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
@@ -102,9 +102,16 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
102102
return tsDoc.parserError ? CompletionList.create([], true) : null;
103103
}
104104

105+
const existingImports = this.getExistingImports(document);
105106
const completionItems = completions
106107
.map((comp) =>
107-
this.toCompletionItem(fragment, comp, pathToUrl(tsDoc.filePath), position),
108+
this.toCompletionItem(
109+
fragment,
110+
comp,
111+
pathToUrl(tsDoc.filePath),
112+
position,
113+
existingImports,
114+
),
108115
)
109116
.filter(isNotNullOrUndefined)
110117
.map((comp) => mapCompletionItemToOriginal(fragment, comp))
@@ -113,6 +120,14 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
113120
return CompletionList.create(completionItems, !!tsDoc.parserError);
114121
}
115122

123+
private getExistingImports(document: Document) {
124+
const rawImports = getRegExpMatches(scriptImportRegex, document.getText()).map((match) =>
125+
(match[1] ?? match[2]).split(','),
126+
);
127+
const tidiedImports = flatten(rawImports).map((match) => match.trim());
128+
return new Set(tidiedImports);
129+
}
130+
116131
private getEventCompletions(
117132
lang: ts.LanguageService,
118133
doc: Document,
@@ -182,13 +197,21 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
182197
comp: ts.CompletionEntry,
183198
uri: string,
184199
position: Position,
200+
existingImports: Set<string>,
185201
): AppCompletionItem<CompletionEntryWithIdentifer> | null {
186202
const completionLabelAndInsert = this.getCompletionLabelAndInsert(fragment, comp);
187203
if (!completionLabelAndInsert) {
188204
return null;
189205
}
190206

191207
const { label, insertText, isSvelteComp } = completionLabelAndInsert;
208+
// TS may suggest another Svelte component even if there already exists an import
209+
// with the same name, because under the hood every Svelte component is postfixed
210+
// with `__SvelteComponent`. In this case, filter out this completion by returning null.
211+
if (isSvelteComp && existingImports.has(label)) {
212+
return null;
213+
}
214+
192215
return {
193216
label,
194217
insertText,
@@ -418,3 +441,8 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
418441
}
419442

420443
const beginOfDocumentRange = Range.create(Position.create(0, 0), Position.create(0, 0));
444+
445+
// `import {...} from '..'` or `import ... from '..'`
446+
// Note: Does not take into account if import is within a comment.
447+
// eslint-disable-next-line max-len
448+
const scriptImportRegex = /\bimport\s+{([^}]*?)}\s+?from\s+['"`].+?['"`]|\bimport\s+(\w+?)\s+from\s+['"`].+?['"`]/g;

packages/language-server/src/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,15 @@ export function regexLastIndexOf(text: string, regex: RegExp, endPos?: number) {
9595
}
9696
return lastIndexOf;
9797
}
98+
99+
/**
100+
* Get all matches of a regexp.
101+
*/
102+
export function getRegExpMatches(regex: RegExp, str: string) {
103+
const matches: RegExpExecArray[] = [];
104+
let match: RegExpExecArray | null;
105+
while ((match = regex.exec(str))) {
106+
matches.push(match);
107+
}
108+
return matches;
109+
}

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,9 @@ describe('CompletionProviderImpl', () => {
358358
async function openFileToBeImported(
359359
docManager: DocumentManager,
360360
completionProvider: CompletionsProviderImpl,
361+
name = 'imported-file.svelte',
361362
) {
362-
const filePath = join(testFilesDir, 'imported-file.svelte');
363+
const filePath = join(testFilesDir, name);
363364
const hoverinfoDoc = docManager.openDocument(<any>{
364365
uri: pathToUrl(filePath),
365366
text: ts.sys.readFile(filePath) || '',
@@ -458,6 +459,32 @@ describe('CompletionProviderImpl', () => {
458459

459460
assert.strictEqual(additionalTextEdits, undefined);
460461
});
462+
463+
it('doesnt suggest svelte auto import when already other import with same name present', async () => {
464+
const { completionProvider, document, docManager } = setup(
465+
'importcompletions-2nd-import.svelte',
466+
);
467+
// make sure that the ts language service does know about the imported-file file
468+
await openFileToBeImported(docManager, completionProvider, 'ScndImport.svelte');
469+
470+
const completions = await completionProvider.getCompletions(
471+
document,
472+
Position.create(2, 13),
473+
);
474+
document.version++;
475+
476+
const items = completions?.items.filter((item) => item.label === 'ScndImport');
477+
assert.equal(items?.length, 1);
478+
479+
const item = items?.[0];
480+
assert.equal(item?.additionalTextEdits, undefined);
481+
assert.equal(item?.detail, undefined);
482+
assert.equal(item?.kind, CompletionItemKind.Variable);
483+
484+
const { additionalTextEdits } = await completionProvider.resolveCompletion(document, item!);
485+
486+
assert.strictEqual(additionalTextEdits, undefined);
487+
});
461488
});
462489

463490
function harmonizeNewLines(input?: string) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<script lang="ts">
2+
import { ScndImport } from "./to-import";
3+
ScndImpor
4+
</script>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export class ScndImport {}

0 commit comments

Comments
 (0)