Joh/rainy-mollusk (#200976)

* snippet completions should also check with the completion model

this will allow to return "more" from the snippet completion item provider and rely on the completions model to do some filtering

* when debugging tests allow to use console.log

* fix https://github.com/microsoft/vscode/issues/191070
This commit is contained in:
Johannes Rieken 2023-12-15 16:07:56 +01:00 committed by GitHub
parent 74bf30a8ec
commit a5698e8857
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 410 additions and 167 deletions

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { MarkdownString } from 'vs/base/common/htmlContent';
import { compare, compareSubstring, firstNonWhitespaceIndex } from 'vs/base/common/strings';
import { compare, compareSubstring } from 'vs/base/common/strings';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
@ -17,9 +17,9 @@ import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/sn
import { isPatternInWord } from 'vs/base/common/filters';
import { StopWatch } from 'vs/base/common/stopwatch';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { getWordAtText } from 'vs/editor/common/core/wordHelper';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
const markSnippetAsUsed = '_snippet.markAsUsed';
@ -70,6 +70,12 @@ export class SnippetCompletion implements CompletionItem {
}
}
interface ISnippetPosition {
startColumn: number;
prefixLow: string;
isWord: boolean;
}
export class SnippetCompletionProvider implements CompletionItemProvider {
readonly _debugDisplayName = 'snippetCompletions';
@ -85,97 +91,100 @@ export class SnippetCompletionProvider implements CompletionItemProvider {
async provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext): Promise<CompletionList> {
const sw = new StopWatch();
const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase();
const wordUntil = model.getWordUntilPosition(position).word.toLowerCase();
// compute all snippet anchors: word starts and every non word character
const line = position.lineNumber;
const word = model.getWordAtPosition(position) ?? { startColumn: position.column, endColumn: position.column, word: '' };
const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase();
const lineContentWithWordLow = lineContentLow.substring(0, word.startColumn + word.word.length - 1);
const anchors = this._computeSnippetPositions(model, line, word, lineContentWithWordLow);
// loop over possible snippets and match them against the anchors
const columnOffset = position.column - 1;
const triggerCharacterLow = context.triggerCharacter?.toLowerCase() ?? '';
const languageId = this._getLanguageIdAtPosition(model, position);
const languageConfig = this._languageConfigurationService.getLanguageConfiguration(languageId);
const snippets = new Set(await this._snippets.getSnippets(languageId));
const suggestions: SnippetCompletion[] = [];
const columnOffset = position.column - 1;
const triggerCharacterLow = context.triggerCharacter?.toLowerCase() ?? '';
snippet: for (const snippet of snippets) {
for (const snippet of snippets) {
if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && !snippet.prefixLow.startsWith(triggerCharacterLow)) {
// strict -> when having trigger characters they must prefix-match
continue snippet;
continue;
}
const word = getWordAtText(1, languageConfig.getWordDefinition(), snippet.prefixLow, 0);
let candidate: ISnippetPosition | undefined;
for (const anchor of anchors) {
if (wordUntil && word && !isPatternInWord(wordUntil, 0, wordUntil.length, snippet.prefixLow, 0, snippet.prefixLow.length)) {
// when at a word the snippet prefix must match
continue snippet;
if (anchor.prefixLow.match(/^\s/) && !snippet.prefixLow.match(/^\s/)) {
// only allow whitespace anchor when snippet prefix starts with whitespace too
continue;
}
if (isPatternInWord(anchor.prefixLow, 0, anchor.prefixLow.length, snippet.prefixLow, 0, snippet.prefixLow.length)) {
candidate = anchor;
break;
}
}
// don't eat into leading whitespace unless the snippet prefix starts with whitespace
const minPos = firstNonWhitespaceIndex(snippet.prefixLow) === 0
? Math.max(0, model.getLineFirstNonWhitespaceColumn(position.lineNumber) - 1)
: 0;
column: for (let pos = Math.max(minPos, columnOffset - snippet.prefixLow.length); pos < lineContentLow.length; pos++) {
if (!isPatternInWord(lineContentLow, pos, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) {
continue column;
}
const prefixRestLen = snippet.prefixLow.length - (columnOffset - pos);
const endsWithPrefixRest = compareSubstring(lineContentLow, snippet.prefixLow, columnOffset, columnOffset + prefixRestLen, columnOffset - pos);
const startPosition = position.with(undefined, pos + 1);
if (wordUntil && position.equals(startPosition)) {
// at word-end but no overlap
continue snippet;
}
let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column;
// First check if there is anything to the right of the cursor
if (columnOffset < lineContentLow.length) {
const autoClosingPairs = languageConfig.getAutoClosingPairs();
const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContentLow[columnOffset]);
// If the character to the right of the cursor is a closing character of an autoclosing pair
if (standardAutoClosingPairConditionals?.some(p =>
// and the start position is the opening character of an autoclosing pair
p.open === lineContentLow[startPosition.column - 1] &&
// and the snippet prefix contains the opening and closing pair at its edges
snippet.prefix.startsWith(p.open) &&
snippet.prefix[snippet.prefix.length - 1] === p.close)
) {
// Eat the character that was likely inserted because of auto-closing pairs
endColumn++;
}
}
const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn });
const insert = replace.setEndPosition(position.lineNumber, position.column);
suggestions.push(new SnippetCompletion(snippet, { replace, insert }));
snippets.delete(snippet);
break;
if (!candidate) {
continue;
}
const pos = candidate.startColumn - 1;
const prefixRestLen = snippet.prefixLow.length - (columnOffset - pos);
const endsWithPrefixRest = compareSubstring(lineContentLow, snippet.prefixLow, columnOffset, columnOffset + prefixRestLen, columnOffset - pos);
const startPosition = position.with(undefined, pos + 1);
let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column;
// First check if there is anything to the right of the cursor
if (columnOffset < lineContentLow.length) {
const autoClosingPairs = languageConfig.getAutoClosingPairs();
const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContentLow[columnOffset]);
// If the character to the right of the cursor is a closing character of an autoclosing pair
if (standardAutoClosingPairConditionals?.some(p =>
// and the start position is the opening character of an autoclosing pair
p.open === lineContentLow[startPosition.column - 1] &&
// and the snippet prefix contains the opening and closing pair at its edges
snippet.prefix.startsWith(p.open) &&
snippet.prefix[snippet.prefix.length - 1] === p.close)
) {
// Eat the character that was likely inserted because of auto-closing pairs
endColumn++;
}
}
const replace = Range.fromPositions({ lineNumber: line, column: candidate.startColumn }, { lineNumber: line, column: endColumn });
const insert = replace.setEndPosition(line, position.column);
suggestions.push(new SnippetCompletion(snippet, { replace, insert }));
snippets.delete(snippet);
}
// add remaing snippets when the current prefix ends in whitespace or when line is empty
// and when not having a trigger character
if (!triggerCharacterLow) {
const endsInWhitespace = /\s/.test(lineContentLow[position.column - 2]);
if (endsInWhitespace || !lineContentLow /*empty line*/) {
for (const snippet of snippets) {
const insert = Range.fromPositions(position);
const replace = lineContentLow.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert;
suggestions.push(new SnippetCompletion(snippet, { replace, insert }));
}
if (!triggerCharacterLow && (/\s/.test(lineContentLow[position.column - 2]) /*end in whitespace */ || !lineContentLow /*empty line*/)) {
for (const snippet of snippets) {
const insert = Range.fromPositions(position);
const replace = lineContentLow.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert;
suggestions.push(new SnippetCompletion(snippet, { replace, insert }));
}
}
// dismbiguate suggestions with same labels
this._disambiguateSnippets(suggestions);
return {
suggestions,
duration: sw.elapsed()
};
}
private _disambiguateSnippets(suggestions: SnippetCompletion[]) {
suggestions.sort(SnippetCompletion.compareByLabel);
for (let i = 0; i < suggestions.length; i++) {
const item = suggestions[i];
@ -188,17 +197,45 @@ export class SnippetCompletionProvider implements CompletionItemProvider {
i = to;
}
}
return {
suggestions,
duration: sw.elapsed()
};
}
resolveCompletionItem(item: CompletionItem): CompletionItem {
return (item instanceof SnippetCompletion) ? item.resolve() : item;
}
private _computeSnippetPositions(model: ITextModel, line: number, word: IWordAtPosition, lineContentWithWordLow: string): ISnippetPosition[] {
const result: ISnippetPosition[] = [];
for (let column = 1; column < word.startColumn; column++) {
const wordInfo = model.getWordAtPosition(new Position(line, column));
result.push({
startColumn: column,
prefixLow: lineContentWithWordLow.substring(column - 1),
isWord: Boolean(wordInfo)
});
if (wordInfo) {
column = wordInfo.endColumn;
// the character right after a word is an anchor, always
result.push({
startColumn: wordInfo.endColumn,
prefixLow: lineContentWithWordLow.substring(wordInfo.endColumn - 1),
isWord: false
});
}
}
if (word.word.length > 0 || result.length === 0) {
result.push({
startColumn: word.startColumn,
prefixLow: lineContentWithWordLow.substring(word.startColumn - 1),
isWord: true
});
}
return result;
}
private _getLanguageIdAtPosition(model: ITextModel, position: Position): string {
// validate the `languageId` to ensure this is a user
// facing language with a name and the chance to have

View file

@ -5,7 +5,7 @@
import * as assert from 'assert';
import { SnippetCompletion, SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider';
import { Position } from 'vs/editor/common/core/position';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets';
import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
@ -17,6 +17,11 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/
import { ILanguageService } from 'vs/editor/common/languages/language';
import { generateUuid } from 'vs/base/common/uuid';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { ITextModel } from 'vs/editor/common/model';
import { CompletionModel } from 'vs/editor/contrib/suggest/browser/completionModel';
import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest';
import { WordDistance } from 'vs/editor/contrib/suggest/browser/wordDistance';
import { EditorOptions } from 'vs/editor/common/config/editorOptions';
class SimpleSnippetService implements ISnippetsService {
declare readonly _serviceBrand: undefined;
@ -42,7 +47,7 @@ class SimpleSnippetService implements ISnippetsService {
}
suite('SnippetsService', function () {
const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke };
const defaultCompletionContext: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke };
let disposables: DisposableStore;
let instantiationService: TestInstantiationService;
@ -86,15 +91,33 @@ suite('SnippetsService', function () {
ensureNoDisposablesAreLeakedInTestSuite();
test('snippet completions - simple', function () {
async function asCompletionModel(model: ITextModel, position: IPosition, provider: SnippetCompletionProvider, context: CompletionContext = defaultCompletionContext) {
const list = await provider.provideCompletionItems(model, Position.lift(position), context);
const result = new CompletionModel(list.suggestions.map(s => {
return new CompletionItem(position, s, list, provider);
}),
position.column,
{ characterCountDelta: 0, leadingLineContent: model.getLineContent(position.lineNumber).substring(0, position.column - 1) },
WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined
);
return result;
}
test('snippet completions - simple', async function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => {
await provider.provideCompletionItems(model, new Position(1, 1), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
});
const completions = await asCompletionModel(model, new Position(1, 1), provider);
assert.strictEqual(completions.items.length, 2);
});
test('snippet completions - simple 2', async function () {
@ -102,23 +125,29 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, 'hello ', 'fooLang'));
await provider.provideCompletionItems(model, new Position(1, 6) /* hello| */, context)!.then(result => {
await provider.provideCompletionItems(model, new Position(1, 6) /* hello| */, defaultCompletionContext)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 0);
});
await provider.provideCompletionItems(model, new Position(1, 7) /* hello |*/, context)!.then(result => {
await provider.provideCompletionItems(model, new Position(1, 7) /* hello |*/, defaultCompletionContext)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
});
const completions1 = await asCompletionModel(model, new Position(1, 6)/* hello| */, provider);
assert.strictEqual(completions1.items.length, 0);
const completions2 = await asCompletionModel(model, new Position(1, 7)/* hello |*/, provider);
assert.strictEqual(completions2.items.length, 2);
});
test('snippet completions - with prefix', function () {
test('snippet completions - with prefix', async function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, 'bar', 'fooLang'));
return provider.provideCompletionItems(model, new Position(1, 4), context)!.then(result => {
await provider.provideCompletionItems(model, new Position(1, 4), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 1);
assert.deepStrictEqual(result.suggestions[0].label, {
@ -128,6 +157,15 @@ suite('SnippetsService', function () {
assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 1);
assert.strictEqual(result.suggestions[0].insertText, 'barCodeSnippet');
});
const completions = await asCompletionModel(model, new Position(1, 4), provider);
assert.strictEqual(completions.items.length, 1);
assert.deepStrictEqual(completions.items[0].completion.label, {
label: 'bar',
description: 'barTest'
});
assert.strictEqual((completions.items[0].completion.range as any).insert.startColumn, 1);
assert.strictEqual(completions.items[0].completion.insertText, 'barCodeSnippet');
});
test('snippet completions - with different prefixes', async function () {
@ -156,63 +194,118 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, 'bar-bar', 'fooLang'));
await provider.provideCompletionItems(model, new Position(1, 3), context)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
assert.deepStrictEqual(result.suggestions[0].label, {
{
await provider.provideCompletionItems(model, new Position(1, 3), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
assert.deepStrictEqual(result.suggestions[0].label, {
label: 'bar',
description: 'barTest'
});
assert.strictEqual(result.suggestions[0].insertText, 's1');
assert.strictEqual((result.suggestions[0].range as CompletionItemRanges).insert.startColumn, 1);
assert.deepStrictEqual(result.suggestions[1].label, {
label: 'bar-bar',
description: 'name'
});
assert.strictEqual(result.suggestions[1].insertText, 's2');
assert.strictEqual((result.suggestions[1].range as CompletionItemRanges).insert.startColumn, 1);
});
const completions = await asCompletionModel(model, new Position(1, 3), provider);
assert.strictEqual(completions.items.length, 2);
assert.deepStrictEqual(completions.items[0].completion.label, {
label: 'bar',
description: 'barTest'
});
assert.strictEqual(result.suggestions[0].insertText, 's1');
assert.strictEqual((result.suggestions[0].range as CompletionItemRanges).insert.startColumn, 1);
assert.deepStrictEqual(result.suggestions[1].label, {
assert.strictEqual(completions.items[0].completion.insertText, 's1');
assert.strictEqual((completions.items[0].completion.range as CompletionItemRanges).insert.startColumn, 1);
assert.deepStrictEqual(completions.items[1].completion.label, {
label: 'bar-bar',
description: 'name'
});
assert.strictEqual(result.suggestions[1].insertText, 's2');
assert.strictEqual((result.suggestions[1].range as CompletionItemRanges).insert.startColumn, 1);
});
assert.strictEqual(completions.items[1].completion.insertText, 's2');
assert.strictEqual((completions.items[1].completion.range as CompletionItemRanges).insert.startColumn, 1);
}
await provider.provideCompletionItems(model, new Position(1, 5), context)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
{
await provider.provideCompletionItems(model, new Position(1, 5), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
const [first, second] = result.suggestions;
const [first, second] = result.suggestions;
assert.deepStrictEqual(first.label, {
label: 'bar',
description: 'barTest'
});
assert.strictEqual(first.insertText, 's1');
assert.strictEqual((first.range as CompletionItemRanges).insert.startColumn, 5);
assert.deepStrictEqual(second.label, {
label: 'bar-bar',
description: 'name'
});
assert.strictEqual(second.insertText, 's2');
assert.strictEqual((second.range as CompletionItemRanges).insert.startColumn, 1);
});
const completions = await asCompletionModel(model, new Position(1, 5), provider);
assert.strictEqual(completions.items.length, 2);
const [first, second] = completions.items.map(i => i.completion);
assert.deepStrictEqual(first.label, {
label: 'bar',
description: 'barTest'
label: 'bar-bar',
description: 'name'
});
assert.strictEqual(first.insertText, 's1');
assert.strictEqual((first.range as CompletionItemRanges).insert.startColumn, 5);
assert.strictEqual(first.insertText, 's2');
assert.strictEqual((first.range as CompletionItemRanges).insert.startColumn, 1);
assert.deepStrictEqual(second.label, {
label: 'bar-bar',
description: 'name'
});
assert.strictEqual(second.insertText, 's2');
assert.strictEqual((second.range as CompletionItemRanges).insert.startColumn, 1);
});
await provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
assert.deepStrictEqual(result.suggestions[0].label, {
label: 'bar',
description: 'barTest'
});
assert.strictEqual(result.suggestions[0].insertText, 's1');
assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 5);
assert.deepStrictEqual(result.suggestions[1].label, {
assert.strictEqual(second.insertText, 's1');
assert.strictEqual((second.range as CompletionItemRanges).insert.startColumn, 5);
}
{
await provider.provideCompletionItems(model, new Position(1, 6), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.incomplete, undefined);
assert.strictEqual(result.suggestions.length, 2);
assert.deepStrictEqual(result.suggestions[0].label, {
label: 'bar',
description: 'barTest'
});
assert.strictEqual(result.suggestions[0].insertText, 's1');
assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 5);
assert.deepStrictEqual(result.suggestions[1].label, {
label: 'bar-bar',
description: 'name'
});
assert.strictEqual(result.suggestions[1].insertText, 's2');
assert.strictEqual((result.suggestions[1].range as any).insert.startColumn, 1);
});
const completions = await asCompletionModel(model, new Position(1, 6), provider);
assert.strictEqual(completions.items.length, 2);
assert.deepStrictEqual(completions.items[0].completion.label, {
label: 'bar-bar',
description: 'name'
});
assert.strictEqual(result.suggestions[1].insertText, 's2');
assert.strictEqual((result.suggestions[1].range as any).insert.startColumn, 1);
});
assert.strictEqual(completions.items[0].completion.insertText, 's2');
assert.strictEqual((completions.items[0].completion.range as any).insert.startColumn, 1);
assert.deepStrictEqual(completions.items[1].completion.label, {
label: 'bar',
description: 'barTest'
});
assert.strictEqual(completions.items[1].completion.insertText, 's1');
assert.strictEqual((completions.items[1].completion.range as any).insert.startColumn, 5);
}
});
test('Cannot use "<?php" as user snippet prefix anymore, #26275', function () {
test('Cannot use "<?php" as user snippet prefix anymore, #26275', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
@ -228,27 +321,35 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
let model = instantiateTextModel(instantiationService, '\t<?php', 'fooLang');
return provider.provideCompletionItems(model, new Position(1, 7), context)!.then(result => {
await provider.provideCompletionItems(model, new Position(1, 7), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.suggestions.length, 1);
model.dispose();
model = instantiateTextModel(instantiationService, '\t<?', 'fooLang');
return provider.provideCompletionItems(model, new Position(1, 4), context)!;
}).then(result => {
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 2);
model.dispose();
model = instantiateTextModel(instantiationService, 'a<?', 'fooLang');
return provider.provideCompletionItems(model, new Position(1, 4), context)!;
}).then(result => {
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 2);
model.dispose();
});
const completions1 = await asCompletionModel(model, new Position(1, 7), provider);
assert.strictEqual(completions1.items.length, 1);
model.dispose();
model = instantiateTextModel(instantiationService, '\t<?', 'fooLang');
await provider.provideCompletionItems(model, new Position(1, 4), defaultCompletionContext).then(result => {
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 2);
});
const completions2 = await asCompletionModel(model, new Position(1, 4), provider);
assert.strictEqual(completions2.items.length, 1);
assert.strictEqual((completions2.items[0].completion.range as any).insert.startColumn, 2);
model.dispose();
model = instantiateTextModel(instantiationService, 'a<?', 'fooLang');
await provider.provideCompletionItems(model, new Position(1, 4), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 2);
});
const completions3 = await asCompletionModel(model, new Position(1, 4), provider);
assert.strictEqual(completions3.items.length, 1);
assert.strictEqual((completions3.items[0].completion.range as any).insert.startColumn, 2);
model.dispose();
});
test('No user snippets in suggestions, when inside the code, #30508', function () {
test('No user snippets in suggestions, when inside the code, #30508', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
@ -265,15 +366,22 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, '<head>\n\t\n>/head>', 'fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => {
assert.strictEqual(result.suggestions.length, 1);
return provider.provideCompletionItems(model, new Position(2, 2), context)!;
}).then(result => {
await provider.provideCompletionItems(model, new Position(1, 1), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.suggestions.length, 1);
});
const completions = await asCompletionModel(model, new Position(1, 1), provider);
assert.strictEqual(completions.items.length, 1);
await provider.provideCompletionItems(model, new Position(2, 2), defaultCompletionContext).then(result => {
assert.strictEqual(result.suggestions.length, 1);
});
const completions2 = await asCompletionModel(model, new Position(2, 2), provider);
assert.strictEqual(completions2.items.length, 1);
});
test('SnippetSuggest - ensure extension snippets come last ', function () {
test('SnippetSuggest - ensure extension snippets come last ', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
@ -299,7 +407,7 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => {
await provider.provideCompletionItems(model, new Position(1, 1), defaultCompletionContext)!.then(result => {
assert.strictEqual(result.suggestions.length, 2);
const [first, second] = result.suggestions;
assert.deepStrictEqual(first.label, {
@ -311,6 +419,18 @@ suite('SnippetsService', function () {
description: 'second'
});
});
const completions = await asCompletionModel(model, new Position(1, 1), provider);
assert.strictEqual(completions.items.length, 2);
const [first, second] = completions.items;
assert.deepStrictEqual(first.completion.label, {
label: 'first',
description: 'first'
});
assert.deepStrictEqual(second.completion.label, {
label: 'second',
description: 'second'
});
});
test('Dash in snippets prefix broken #53945', async function () {
@ -329,14 +449,20 @@ suite('SnippetsService', function () {
const model = disposables.add(instantiateTextModel(instantiationService, 'p-', 'fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!;
let result = await provider.provideCompletionItems(model, new Position(1, 2), defaultCompletionContext)!;
let completions = await asCompletionModel(model, new Position(1, 2), provider);
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual(completions.items.length, 1);
result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
result = await provider.provideCompletionItems(model, new Position(1, 3), defaultCompletionContext)!;
completions = await asCompletionModel(model, new Position(1, 3), provider);
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual(completions.items.length, 1);
result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
result = await provider.provideCompletionItems(model, new Position(1, 3), defaultCompletionContext)!;
completions = await asCompletionModel(model, new Position(1, 3), provider);
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual(completions.items.length, 1);
});
test('No snippets suggestion on long lines beyond character 100 #58807', async function () {
@ -355,9 +481,11 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang'));
const result = await provider.provideCompletionItems(model, new Position(1, 158), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 158), defaultCompletionContext)!;
const completions = await asCompletionModel(model, new Position(1, 158), provider);
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual(completions.items.length, 1);
});
test('Type colon will trigger snippet #60746', async function () {
@ -376,9 +504,11 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, ':', 'fooLang'));
const result = await provider.provideCompletionItems(model, new Position(1, 2), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 2), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 0);
const completions = await asCompletionModel(model, new Position(1, 2), provider);
assert.strictEqual(completions.items.length, 0);
});
test('substring of prefix can\'t trigger snippet #60737', async function () {
@ -397,13 +527,16 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, 'template', 'fooLang'));
const result = await provider.provideCompletionItems(model, new Position(1, 9), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 9), defaultCompletionContext);
assert.strictEqual(result.suggestions.length, 1);
assert.deepStrictEqual(result.suggestions[0].label, {
label: 'mytemplate',
description: 'mytemplate'
});
const completions = await asCompletionModel(model, new Position(1, 9), provider);
assert.strictEqual(completions.items.length, 0);
});
test('No snippets suggestion beyond character 100 if not at end of line #60247', async function () {
@ -422,9 +555,12 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang'));
const result = await provider.provideCompletionItems(model, new Position(1, 158), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 158), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 1);
const completions = await asCompletionModel(model, new Position(1, 158), provider);
assert.strictEqual(completions.items.length, 1);
});
test('issue #61296: VS code freezes when editing CSS file with emoji', async function () {
@ -448,9 +584,12 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService);
const model = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', 'fooLang'));
const result = await provider.provideCompletionItems(model, new Position(1, 8), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 8), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 1);
const completions = await asCompletionModel(model, new Position(1, 8), provider);
assert.strictEqual(completions.items.length, 1);
});
test('No snippets shown when triggering completions at whitespace on line that already has text #62335', async function () {
@ -469,9 +608,12 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = disposables.add(instantiateTextModel(instantiationService, 'a ', 'fooLang'));
const result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 3), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 1);
const completions = await asCompletionModel(model, new Position(1, 3), provider);
assert.strictEqual(completions.items.length, 1);
});
test('Snippet prefix with special chars and numbers does not work #62906', async function () {
@ -500,19 +642,27 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
let model = instantiateTextModel(instantiationService, ' <', 'fooLang');
let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
let result = await provider.provideCompletionItems(model, new Position(1, 3), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 1);
let [first] = result.suggestions;
assert.strictEqual((first.range as any).insert.startColumn, 2);
model.dispose();
let completions = await asCompletionModel(model, new Position(1, 3), provider);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editStart.column, 2);
model.dispose();
model = instantiateTextModel(instantiationService, '1', 'fooLang');
result = await provider.provideCompletionItems(model, new Position(1, 2), context)!;
result = await provider.provideCompletionItems(model, new Position(1, 2), defaultCompletionContext)!;
completions = await asCompletionModel(model, new Position(1, 2), provider);
assert.strictEqual(result.suggestions.length, 1);
[first] = result.suggestions;
assert.strictEqual((first.range as any).insert.startColumn, 1);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editStart.column, 1);
model.dispose();
});
@ -532,30 +682,46 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
let model = instantiateTextModel(instantiationService, 'not wordFoo bar', 'fooLang');
let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
let result = await provider.provideCompletionItems(model, new Position(1, 3), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 1);
let [first] = result.suggestions;
assert.strictEqual((first.range as any).insert.endColumn, 3);
assert.strictEqual((first.range as any).replace.endColumn, 9);
model.dispose();
let completions = await asCompletionModel(model, new Position(1, 3), provider);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editInsertEnd.column, 3);
assert.strictEqual(completions.items[0].editReplaceEnd.column, 9);
model.dispose();
model = instantiateTextModel(instantiationService, 'not woFoo bar', 'fooLang');
result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
result = await provider.provideCompletionItems(model, new Position(1, 3), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 1);
[first] = result.suggestions;
assert.strictEqual((first.range as any).insert.endColumn, 3);
assert.strictEqual((first.range as any).replace.endColumn, 3);
model.dispose();
completions = await asCompletionModel(model, new Position(1, 3), provider);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editInsertEnd.column, 3);
assert.strictEqual(completions.items[0].editReplaceEnd.column, 3);
model.dispose();
model = instantiateTextModel(instantiationService, 'not word', 'fooLang');
result = await provider.provideCompletionItems(model, new Position(1, 1), context)!;
result = await provider.provideCompletionItems(model, new Position(1, 1), defaultCompletionContext)!;
assert.strictEqual(result.suggestions.length, 1);
[first] = result.suggestions;
assert.strictEqual((first.range as any).insert.endColumn, 1);
assert.strictEqual((first.range as any).replace.endColumn, 9);
completions = await asCompletionModel(model, new Position(1, 1), provider);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editInsertEnd.column, 1);
assert.strictEqual(completions.items[0].editReplaceEnd.column, 9);
model.dispose();
});
@ -576,12 +742,18 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = instantiateTextModel(instantiationService, 'filler e KEEP ng filler', 'fooLang');
const result = await provider.provideCompletionItems(model, new Position(1, 9), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 9), defaultCompletionContext)!;
const completions = await asCompletionModel(model, new Position(1, 9), provider);
assert.strictEqual(result.suggestions.length, 1);
const [first] = result.suggestions;
assert.strictEqual((first.range as any).insert.endColumn, 9);
assert.strictEqual((first.range as any).replace.endColumn, 9);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editInsertEnd.column, 9);
assert.strictEqual(completions.items[0].editReplaceEnd.column, 9);
model.dispose();
});
@ -610,13 +782,19 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService);
const model = instantiateTextModel(instantiationService, '[psc]', 'fooLang');
const result = await provider.provideCompletionItems(model, new Position(1, 5), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 5), defaultCompletionContext)!;
const completions = await asCompletionModel(model, new Position(1, 5), provider);
assert.strictEqual(result.suggestions.length, 1);
const [first] = result.suggestions;
assert.strictEqual((first.range as any).insert.endColumn, 5);
// This is 6 because it should eat the `]` at the end of the text even if cursor is before it
assert.strictEqual((first.range as any).replace.endColumn, 6);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editInsertEnd.column, 5);
assert.strictEqual(completions.items[0].editReplaceEnd.column, 6);
model.dispose();
});
@ -637,13 +815,18 @@ suite('SnippetsService', function () {
const provider = new SnippetCompletionProvider(languageService, snippetService, disposables.add(new TestLanguageConfigurationService()));
const model = instantiateTextModel(instantiationService, ' ci', 'fooLang');
const result = await provider.provideCompletionItems(model, new Position(1, 4), context)!;
const result = await provider.provideCompletionItems(model, new Position(1, 4), defaultCompletionContext)!;
const completions = await asCompletionModel(model, new Position(1, 4), provider);
assert.strictEqual(result.suggestions.length, 1);
const [first] = result.suggestions;
assert.strictEqual((<CompletionItemLabel>first.label).label, ' cite');
assert.strictEqual((<CompletionItemRanges>first.range).insert.startColumn, 1);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].textLabel, ' cite');
assert.strictEqual(completions.items[0].editStart.column, 1);
model.dispose();
});
@ -688,6 +871,10 @@ suite('SnippetsService', function () {
)!;
assert.strictEqual(result.suggestions.length, 1);
const completions = await asCompletionModel(model, new Position(1, 2), provider, { triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: '\'' });
assert.strictEqual(completions.items.length, 1);
model.dispose();
});
@ -709,6 +896,11 @@ suite('SnippetsService', function () {
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual((<SnippetCompletion>result.suggestions[0]).label.label, 'hell_or_tell');
const completions = await asCompletionModel(model, new Position(1, 8), provider, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].textLabel, 'hell_or_tell');
model.dispose();
});
@ -730,6 +922,12 @@ suite('SnippetsService', function () {
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual((<SnippetCompletion>result.suggestions[0]).label.label, '^y');
const completions = await asCompletionModel(model, new Position(1, 5), provider, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].textLabel, '^y');
model.dispose();
});
@ -750,6 +948,10 @@ suite('SnippetsService', function () {
assert.strictEqual(result.suggestions.length, 1);
assert.strictEqual((<SnippetCompletion>result.suggestions[0]).label.label, 'foobarrrrrr');
const completions = await asCompletionModel(model, new Position(1, 7), provider, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].textLabel, 'foobarrrrrr');
model.dispose();
});
@ -818,9 +1020,13 @@ suite('SnippetsService', function () {
assert.strictEqual(result.suggestions.length, 1);
const first = result.suggestions[0];
assert.strictEqual((<CompletionItemRanges>first.range).insert.startColumn, 5);
const completions = await asCompletionModel(model, new Position(1, 8), provider);
assert.strictEqual(completions.items.length, 1);
assert.strictEqual(completions.items[0].editStart.column, 5);
});
test.skip('Autocomplete suggests based on the last letter of a word and it depends on the typing speed #191070', async function () {
test('Autocomplete suggests based on the last letter of a word and it depends on the typing speed #191070', async function () {
snippetService = new SimpleSnippetService([
new Snippet(false, ['fooLang'], '/whiletrue', '/whiletrue', '', 'one', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], '/sc not expanding', '/sc not expanding', '', 'two', '', SnippetSource.User, generateUuid()),
@ -836,8 +1042,8 @@ suite('SnippetsService', function () {
new Position(1, 2),
{ triggerKind: CompletionTriggerKind.Invoke }
);
assert.strictEqual(result1.suggestions.length, 1);
assert.strictEqual(result1.suggestions[0].insertText, 'one');
assert.strictEqual(result1.suggestions.length, 1);
}
{ // PREFIX: where
@ -847,8 +1053,8 @@ suite('SnippetsService', function () {
new Position(1, 6),
{ triggerKind: CompletionTriggerKind.Invoke }
);
assert.strictEqual(result2.suggestions[0].insertText, 'one'); // /whiletrue matches where (WHilEtRuE)
assert.strictEqual(result2.suggestions.length, 1);
assert.strictEqual(result2.suggestions[1].insertText, 'one'); // /whiletrue matches where (WHilEtRuE)
}
});
});

View file

@ -278,7 +278,7 @@ function loadTests(opts) {
teardown(() => {
// should not have unexpected output
if (_testsWithUnexpectedOutput) {
if (_testsWithUnexpectedOutput && !opts.dev) {
assert.ok(false, 'Error: Unexpected console output in test run. Please ensure no console.[log|error|info|warn] usage in tests or runtime errors.');
}