Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EOL } from 'os';
import { SvelteDocument } from '../SvelteDocument';
import {
Position,
Expand All @@ -8,9 +9,21 @@ import {
} from 'vscode-languageserver';
import { SvelteTag, documentation, getLatestOpeningTag } from './SvelteTags';
import { isInTag } from '../../../lib/documents';
/**
* Get completions for special svelte tags within moustache tags.
*/

const HTML_COMMENT_START = '<!--';

const componentDocumentationCompletion: CompletionItem = {
label: '@component',
insertText: `component${EOL}$1${EOL}`,
documentation: 'Documentation for this component. ' +
'It will show up on hover. You can use markdown and code blocks here',
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Snippet,
sortText: '-1',
filterText: 'component',
preselect: true,
};

export function getCompletions(
svelteDoc: SvelteDocument,
position: Position,
Expand All @@ -28,13 +41,40 @@ export function getCompletions(
const notPreceededByOpeningBracket = !/[\s\S]*{\s*[#:/@]\w*$/.test(
lastCharactersBeforePosition,
);
if (isInStyleOrScript || notPreceededByOpeningBracket) {
if (isInStyleOrScript) {
return null;
}

const triggerCharacter = getTriggerCharacter(lastCharactersBeforePosition);
// return all, filtering with regards to user input will be done client side
return getCompletionsWithRegardToTriggerCharacter(triggerCharacter, svelteDoc, offset);
if (notPreceededByOpeningBracket) {
return getComponentDocumentationCompletions();
}

return getTagCompletionsWithinMoustache();

/**
* Get completions for special svelte tags within moustache tags.
*/
function getTagCompletionsWithinMoustache() {
const triggerCharacter = getTriggerCharacter(lastCharactersBeforePosition);
// return all, filtering with regards to user input will be done client side
return getCompletionsWithRegardToTriggerCharacter(triggerCharacter, svelteDoc, offset);
}

function getComponentDocumentationCompletions() {
if (!lastCharactersBeforePosition.includes(HTML_COMMENT_START)) {
return null;
}

const commentStartIndex = lastCharactersBeforePosition.lastIndexOf(HTML_COMMENT_START);
const text = lastCharactersBeforePosition.substring(
commentStartIndex + HTML_COMMENT_START.length
).trimLeft();

if (componentDocumentationCompletion.label.includes(text)) {
return CompletionList.create([componentDocumentationCompletion], false);
}
return null;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Range,
SymbolInformation,
WorkspaceEdit,
CompletionList,
} from 'vscode-languageserver';
import { Document, DocumentManager, mapSymbolInformationToOriginal } from '../../lib/documents';
import { LSConfigManager, LSTypescriptConfig } from '../../ls-config';
Expand Down Expand Up @@ -42,6 +43,7 @@ import { RenameProviderImpl } from './features/RenameProvider';
import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider';
import { LSAndTSDocResolver } from './LSAndTSDocResolver';
import { convertToLocationRange, getScriptKindFromFileName, symbolKindFromString } from './utils';
import { getDirectiveCommentCompletions } from './features/getDirectiveCommentCompletions';

export class TypeScriptPlugin
implements
Expand Down Expand Up @@ -168,7 +170,26 @@ export class TypeScriptPlugin
return null;
}

return this.completionProvider.getCompletions(document, position, completionContext);
const tsDirectiveCommentCompletions = getDirectiveCommentCompletions(
position,
document,
completionContext
);

const completions = await this.completionProvider.getCompletions(
document,
position,
completionContext
);

if (completions && tsDirectiveCommentCompletions) {
return CompletionList.create(
completions.items.concat(tsDirectiveCommentCompletions.items),
completions.isIncomplete
);
}

return completions ?? tsDirectiveCommentCompletions;
}

async resolveCompletion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDo
type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#';

export class CompletionsProviderImpl implements CompletionsProvider<CompletionEntryWithIdentifer> {
constructor(private readonly lsAndTsDocResovler: LSAndTSDocResolver) {}
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) { }

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

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

const filePath = tsDoc.filePath;
if (!filePath) {
Expand Down Expand Up @@ -136,7 +136,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
originalPosition: Position,
): AppCompletionItem<CompletionEntryWithIdentifer>[] {
const snapshot = getComponentAtPosition(
this.lsAndTsDocResovler,
this.lsAndTsDocResolver,
lang,
doc,
tsDoc,
Expand Down Expand Up @@ -236,7 +236,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
completionItem: AppCompletionItem<CompletionEntryWithIdentifer>,
): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
const { data: comp } = completionItem;
const { tsDoc, lang } = this.lsAndTsDocResovler.getLSAndTSDoc(document);
const { tsDoc, lang } = this.lsAndTsDocResolver.getLSAndTSDoc(document);

const filePath = tsDoc.filePath;

Expand Down Expand Up @@ -333,16 +333,16 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn

const { span } = change;

const virutalRange = convertRange(fragment, span);
const virtualRange = convertRange(fragment, span);
let range: Range;
const isNewImport = isImport && virutalRange.start.character === 0;
const isNewImport = isImport && virtualRange.start.character === 0;

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

// If range is somehow not mapped in parent,
Expand Down Expand Up @@ -372,8 +372,8 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
}

private mapRangeForNewImport(fragment: SvelteSnapshotFragment, virtualRange: Range) {
const sourceMapableRange = this.offsetLinesAndMovetoStartOfLine(virtualRange, -1);
const mappableRange = mapRangeToOriginal(fragment, sourceMapableRange);
const sourceMappableRange = this.offsetLinesAndMovetoStartOfLine(virtualRange, -1);
const mappableRange = mapRangeToOriginal(fragment, sourceMappableRange);
return this.offsetLinesAndMovetoStartOfLine(mappableRange, 1);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Document, isInTag } from '../../../lib/documents';
import {
Position,
CompletionItemKind,
CompletionItem,
TextEdit,
Range,
CompletionList,
CompletionContext,
} from 'vscode-languageserver';

/**
* from https://github.com/microsoft/vscode/blob/157255fa4b0775c5ab8729565faf95927b610cac/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts#L19
*/
export const tsDirectives = [
{
value: '@ts-check',
description:
'Enables semantic checking in a JavaScript file. Must be at the top of a file.',
},
{
value: '@ts-nocheck',
description:
'Disables semantic checking in a JavaScript file. Must be at the top of a file.',
},
{
value: '@ts-ignore',
description: 'Suppresses @ts-check errors on the next line of a file.',
},
{
value: '@ts-expect-error',
description:
'Suppresses @ts-check errors on the next line of a file, expecting at least one to exist.',
},
];

/**
* from https://github.com/microsoft/vscode/blob/157255fa4b0775c5ab8729565faf95927b610cac/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts#L64
*/
export function getDirectiveCommentCompletions(
position: Position,
document: Document,
completionContext: CompletionContext | undefined,
) {
// don't trigger until // @
if (completionContext?.triggerCharacter === '/') {
return null;
}

const inScript = isInTag(position, document.scriptInfo);
const inModule = isInTag(position, document.moduleScriptInfo);
if (!inModule && !inScript) {
return null;
}

const lineStart = document.offsetAt(Position.create(position.line, 0));
const offset = document.offsetAt(position);
const prefix = document.getText().slice(lineStart, offset);
const match = prefix.match(/^\s*\/\/+\s?(@[a-zA-Z-]*)?$/);

if (!match) {
return null;
}
const startCharacter = Math.max(0, position.character - (match[1]?.length ?? 0));
const start = Position.create(position.line, startCharacter);

const items = tsDirectives.map<CompletionItem>(({ value, description }) => ({
detail: description,
label: value,
kind: CompletionItemKind.Snippet,
textEdit: TextEdit.replace(
Range.create(start, Position.create(start.line, start.character + value.length)),
value,
),
}));

return CompletionList.create(items, false);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as assert from 'assert';
import { EOL } from 'os';
import { Position } from 'vscode-languageserver';
import { getCompletions } from '../../../../src/plugins/svelte/features/getCompletions';
import { SvelteDocument } from '../../../../src/plugins/svelte/SvelteDocument';
Expand Down Expand Up @@ -115,4 +116,17 @@ describe('SveltePlugin#getCompletions', () => {
expectCompletionsFor('{#if}{/if}{#if}{#await}{/').toEqual(['await']);
});
});

it('should return completion for component documentation comment', () => {
const content = '<!--@';
const svelteDoc = new SvelteDocument(new Document('url', content));
const completions = getCompletions(
svelteDoc,
Position.create(0, content.length)
);
assert.deepStrictEqual(
completions?.items?.[0].insertText,
`component${EOL}$1${EOL}`
);
});
});
Loading