Skip to content

Commit 7e74770

Browse files
(feat) directive comments snippet (#557)
* component documentaion comment snippet * ts-directive completions * test for component documentation snippet * test for typescript directive comment completions * clean up, lint * wording Co-authored-by: Simon H <[email protected]> * code style * no isIncomplete Co-authored-by: Simon H <[email protected]>
1 parent dd540d6 commit 7e74770

File tree

7 files changed

+310
-19
lines changed

7 files changed

+310
-19
lines changed

packages/language-server/src/plugins/svelte/features/getCompletions.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EOL } from 'os';
12
import { SvelteDocument } from '../SvelteDocument';
23
import {
34
Position,
@@ -8,9 +9,21 @@ import {
89
} from 'vscode-languageserver';
910
import { SvelteTag, documentation, getLatestOpeningTag } from './SvelteTags';
1011
import { isInTag } from '../../../lib/documents';
11-
/**
12-
* Get completions for special svelte tags within moustache tags.
13-
*/
12+
13+
const HTML_COMMENT_START = '<!--';
14+
15+
const componentDocumentationCompletion: CompletionItem = {
16+
label: '@component',
17+
insertText: `component${EOL}$1${EOL}`,
18+
documentation: 'Documentation for this component. ' +
19+
'It will show up on hover. You can use markdown and code blocks here',
20+
insertTextFormat: InsertTextFormat.Snippet,
21+
kind: CompletionItemKind.Snippet,
22+
sortText: '-1',
23+
filterText: 'component',
24+
preselect: true,
25+
};
26+
1427
export function getCompletions(
1528
svelteDoc: SvelteDocument,
1629
position: Position,
@@ -28,13 +41,40 @@ export function getCompletions(
2841
const notPreceededByOpeningBracket = !/[\s\S]*{\s*[#:/@]\w*$/.test(
2942
lastCharactersBeforePosition,
3043
);
31-
if (isInStyleOrScript || notPreceededByOpeningBracket) {
44+
if (isInStyleOrScript) {
3245
return null;
3346
}
3447

35-
const triggerCharacter = getTriggerCharacter(lastCharactersBeforePosition);
36-
// return all, filtering with regards to user input will be done client side
37-
return getCompletionsWithRegardToTriggerCharacter(triggerCharacter, svelteDoc, offset);
48+
if (notPreceededByOpeningBracket) {
49+
return getComponentDocumentationCompletions();
50+
}
51+
52+
return getTagCompletionsWithinMoustache();
53+
54+
/**
55+
* Get completions for special svelte tags within moustache tags.
56+
*/
57+
function getTagCompletionsWithinMoustache() {
58+
const triggerCharacter = getTriggerCharacter(lastCharactersBeforePosition);
59+
// return all, filtering with regards to user input will be done client side
60+
return getCompletionsWithRegardToTriggerCharacter(triggerCharacter, svelteDoc, offset);
61+
}
62+
63+
function getComponentDocumentationCompletions() {
64+
if (!lastCharactersBeforePosition.includes(HTML_COMMENT_START)) {
65+
return null;
66+
}
67+
68+
const commentStartIndex = lastCharactersBeforePosition.lastIndexOf(HTML_COMMENT_START);
69+
const text = lastCharactersBeforePosition.substring(
70+
commentStartIndex + HTML_COMMENT_START.length
71+
).trimLeft();
72+
73+
if (componentDocumentationCompletion.label.includes(text)) {
74+
return CompletionList.create([componentDocumentationCompletion], false);
75+
}
76+
return null;
77+
}
3878
}
3979

4080
/**

packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Range,
1313
SymbolInformation,
1414
WorkspaceEdit,
15+
CompletionList,
1516
} from 'vscode-languageserver';
1617
import {
1718
Document,
@@ -47,6 +48,7 @@ import { RenameProviderImpl } from './features/RenameProvider';
4748
import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider';
4849
import { LSAndTSDocResolver } from './LSAndTSDocResolver';
4950
import { convertToLocationRange, getScriptKindFromFileName, symbolKindFromString } from './utils';
51+
import { getDirectiveCommentCompletions } from './features/getDirectiveCommentCompletions';
5052

5153
export class TypeScriptPlugin
5254
implements
@@ -189,7 +191,26 @@ export class TypeScriptPlugin
189191
return null;
190192
}
191193

192-
return this.completionProvider.getCompletions(document, position, completionContext);
194+
const tsDirectiveCommentCompletions = getDirectiveCommentCompletions(
195+
position,
196+
document,
197+
completionContext
198+
);
199+
200+
const completions = await this.completionProvider.getCompletions(
201+
document,
202+
position,
203+
completionContext
204+
);
205+
206+
if (completions && tsDirectiveCommentCompletions) {
207+
return CompletionList.create(
208+
completions.items.concat(tsDirectiveCommentCompletions.items),
209+
completions.isIncomplete
210+
);
211+
}
212+
213+
return completions ?? tsDirectiveCommentCompletions;
193214
}
194215

195216
async resolveCompletion(

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDo
3434
type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#';
3535

3636
export class CompletionsProviderImpl implements CompletionsProvider<CompletionEntryWithIdentifer> {
37-
constructor(private readonly lsAndTsDocResovler: LSAndTSDocResolver) {}
37+
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) { }
3838

3939
/**
4040
* The language service throws an error if the character is not a valid trigger character.
@@ -58,7 +58,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
5858
return null;
5959
}
6060

61-
const { lang, tsDoc } = this.lsAndTsDocResovler.getLSAndTSDoc(document);
61+
const { lang, tsDoc } = this.lsAndTsDocResolver.getLSAndTSDoc(document);
6262

6363
const filePath = tsDoc.filePath;
6464
if (!filePath) {
@@ -136,7 +136,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
136136
originalPosition: Position,
137137
): AppCompletionItem<CompletionEntryWithIdentifer>[] {
138138
const snapshot = getComponentAtPosition(
139-
this.lsAndTsDocResovler,
139+
this.lsAndTsDocResolver,
140140
lang,
141141
doc,
142142
tsDoc,
@@ -236,7 +236,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
236236
completionItem: AppCompletionItem<CompletionEntryWithIdentifer>,
237237
): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
238238
const { data: comp } = completionItem;
239-
const { tsDoc, lang } = this.lsAndTsDocResovler.getLSAndTSDoc(document);
239+
const { tsDoc, lang } = this.lsAndTsDocResolver.getLSAndTSDoc(document);
240240

241241
const filePath = tsDoc.filePath;
242242

@@ -333,16 +333,16 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
333333

334334
const { span } = change;
335335

336-
const virutalRange = convertRange(fragment, span);
336+
const virtualRange = convertRange(fragment, span);
337337
let range: Range;
338-
const isNewImport = isImport && virutalRange.start.character === 0;
338+
const isNewImport = isImport && virtualRange.start.character === 0;
339339

340340
// Since new import always can't be mapped, we'll have special treatment here
341341
// but only hack this when there is multiple line in script
342-
if (isNewImport && virutalRange.start.line > 1) {
343-
range = this.mapRangeForNewImport(fragment, virutalRange);
342+
if (isNewImport && virtualRange.start.line > 1) {
343+
range = this.mapRangeForNewImport(fragment, virtualRange);
344344
} else {
345-
range = mapRangeToOriginal(fragment, virutalRange);
345+
range = mapRangeToOriginal(fragment, virtualRange);
346346
}
347347

348348
// If range is somehow not mapped in parent,
@@ -372,8 +372,8 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
372372
}
373373

374374
private mapRangeForNewImport(fragment: SvelteSnapshotFragment, virtualRange: Range) {
375-
const sourceMapableRange = this.offsetLinesAndMovetoStartOfLine(virtualRange, -1);
376-
const mappableRange = mapRangeToOriginal(fragment, sourceMapableRange);
375+
const sourceMappableRange = this.offsetLinesAndMovetoStartOfLine(virtualRange, -1);
376+
const mappableRange = mapRangeToOriginal(fragment, sourceMappableRange);
377377
return this.offsetLinesAndMovetoStartOfLine(mappableRange, 1);
378378
}
379379

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Document, isInTag } from '../../../lib/documents';
2+
import {
3+
Position,
4+
CompletionItemKind,
5+
CompletionItem,
6+
TextEdit,
7+
Range,
8+
CompletionList,
9+
CompletionContext,
10+
} from 'vscode-languageserver';
11+
12+
/**
13+
* from https://github.com/microsoft/vscode/blob/157255fa4b0775c5ab8729565faf95927b610cac/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts#L19
14+
*/
15+
export const tsDirectives = [
16+
{
17+
value: '@ts-check',
18+
description:
19+
'Enables semantic checking in a JavaScript file. Must be at the top of a file.',
20+
},
21+
{
22+
value: '@ts-nocheck',
23+
description:
24+
'Disables semantic checking in a JavaScript file. Must be at the top of a file.',
25+
},
26+
{
27+
value: '@ts-ignore',
28+
description: 'Suppresses @ts-check errors on the next line of a file.',
29+
},
30+
{
31+
value: '@ts-expect-error',
32+
description:
33+
'Suppresses @ts-check errors on the next line of a file, expecting at least one to exist.',
34+
},
35+
];
36+
37+
/**
38+
* from https://github.com/microsoft/vscode/blob/157255fa4b0775c5ab8729565faf95927b610cac/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts#L64
39+
*/
40+
export function getDirectiveCommentCompletions(
41+
position: Position,
42+
document: Document,
43+
completionContext: CompletionContext | undefined,
44+
) {
45+
// don't trigger until // @
46+
if (completionContext?.triggerCharacter === '/') {
47+
return null;
48+
}
49+
50+
const inScript = isInTag(position, document.scriptInfo);
51+
const inModule = isInTag(position, document.moduleScriptInfo);
52+
if (!inModule && !inScript) {
53+
return null;
54+
}
55+
56+
const lineStart = document.offsetAt(Position.create(position.line, 0));
57+
const offset = document.offsetAt(position);
58+
const prefix = document.getText().slice(lineStart, offset);
59+
const match = prefix.match(/^\s*\/\/+\s?(@[a-zA-Z-]*)?$/);
60+
61+
if (!match) {
62+
return null;
63+
}
64+
const startCharacter = Math.max(0, position.character - (match[1]?.length ?? 0));
65+
const start = Position.create(position.line, startCharacter);
66+
67+
const items = tsDirectives.map<CompletionItem>(({ value, description }) => ({
68+
detail: description,
69+
label: value,
70+
kind: CompletionItemKind.Snippet,
71+
textEdit: TextEdit.replace(
72+
Range.create(start, Position.create(start.line, start.character + value.length)),
73+
value,
74+
),
75+
}));
76+
77+
return CompletionList.create(items, false);
78+
}

packages/language-server/test/plugins/svelte/features/getCompletions.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as assert from 'assert';
2+
import { EOL } from 'os';
23
import { Position } from 'vscode-languageserver';
34
import { getCompletions } from '../../../../src/plugins/svelte/features/getCompletions';
45
import { SvelteDocument } from '../../../../src/plugins/svelte/SvelteDocument';
@@ -115,4 +116,17 @@ describe('SveltePlugin#getCompletions', () => {
115116
expectCompletionsFor('{#if}{/if}{#if}{#await}{/').toEqual(['await']);
116117
});
117118
});
119+
120+
it('should return completion for component documentation comment', () => {
121+
const content = '<!--@';
122+
const svelteDoc = new SvelteDocument(new Document('url', content));
123+
const completions = getCompletions(
124+
svelteDoc,
125+
Position.create(0, content.length)
126+
);
127+
assert.deepStrictEqual(
128+
completions?.items?.[0].insertText,
129+
`component${EOL}$1${EOL}`
130+
);
131+
});
118132
});

0 commit comments

Comments
 (0)