Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IModeService } from 'vs/editor/common/services/modeService';
import { extname } from 'vs/base/common/path';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { URI } from 'vs/base/common/uri';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
import { IQuickPickItem, IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IFileService } from 'vs/platform/files/common/files';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { isValidBasename } from 'vs/base/common/extpath';
import { joinPath, basename } from 'vs/base/common/resources';
const id = 'workbench.action.openSnippets';
namespace ISnippetPick {
export function is(thing: object | undefined): thing is ISnippetPick {
return !!thing && URI.isUri((<ISnippetPick>thing).filepath);
}
}
interface ISnippetPick extends IQuickPickItem {
filepath: URI;
hint?: true;
}
async function computePicks(snippetService: ISnippetsService, envService: IEnvironmentService, modeService: IModeService) {
const existing: ISnippetPick[] = [];
const future: ISnippetPick[] = [];
const seen = new Set<string>();
for (const file of await snippetService.getSnippetFiles()) {
if (file.source === SnippetSource.Extension) {
// skip extension snippets
continue;
}
if (file.isGlobalSnippets) {
await file.load();
// list scopes for global snippets
const names = new Set<string>();
outer: for (const snippet of file.data) {
for (const scope of snippet.scopes) {
const name = modeService.getLanguageName(scope);
if (name) {
if (names.size >= 4) {
names.add(`${name}...`);
break outer;
} else {
names.add(name);
}
}
}
}
existing.push({
label: basename(file.location),
filepath: file.location,
description: names.size === 0
? nls.localize('global.scope', "(global)")
: nls.localize('global.1', "({0})", [...names].join(', '))
});
} else {
// language snippet
const mode = basename(file.location).replace(/\.json$/, '');
existing.push({
label: basename(file.location),
description: `(${modeService.getLanguageName(mode)})`,
filepath: file.location
});
seen.add(mode);
}
}
const dir = envService.snippetsHome;
for (const mode of modeService.getRegisteredModes()) {
const label = modeService.getLanguageName(mode);
if (label && !seen.has(mode)) {
future.push({
label: mode,
description: `(${label})`,
filepath: joinPath(dir, `${mode}.json`),
hint: true
});
}
}
existing.sort((a, b) => {
let a_ext = extname(a.filepath.path);
let b_ext = extname(b.filepath.path);
if (a_ext === b_ext) {
return a.label.localeCompare(b.label);
} else if (a_ext === '.code-snippets') {
return -1;
} else {
return 1;
}
});
future.sort((a, b) => {
return a.label.localeCompare(b.label);
});
return { existing, future };
}
async function createSnippetFile(scope: string, defaultPath: URI, quickInputService: IQuickInputService, fileService: IFileService, textFileService: ITextFileService, opener: IOpenerService) {
function createSnippetUri(input: string) {
const filename = extname(input) !== '.code-snippets'
? `${input}.code-snippets`
: input;
return joinPath(defaultPath, filename);
}
await fileService.createFolder(defaultPath);
const input = await quickInputService.input({
placeHolder: nls.localize('name', "Type snippet file name"),
async validateInput(input) {
if (!input) {
return nls.localize('bad_name1', "Invalid file name");
}
if (!isValidBasename(input)) {
return nls.localize('bad_name2', "'{0}' is not a valid file name", input);
}
if (await fileService.exists(createSnippetUri(input))) {
return nls.localize('bad_name3', "'{0}' already exists", input);
}
return undefined;
}
});
if (!input) {
return undefined;
}
const resource = createSnippetUri(input);
await textFileService.write(resource, [
'{',
'\t// Place your ' + scope + ' snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and ',
'\t// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope ',
'\t// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is ',
'\t// used to trigger the snippet and the body will be expanded and inserted. Possible variables are: ',
'\t// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. ',
'\t// Placeholders with the same ids are connected.',
'\t// Example:',
'\t// "Print to console": {',
'\t// \t"scope": "javascript,typescript",',
'\t// \t"prefix": "log",',
'\t// \t"body": [',
'\t// \t\t"console.log(\'$1\');",',
'\t// \t\t"$2"',
'\t// \t],',
'\t// \t"description": "Log output to console"',
'\t// }',
'}'
].join('\n'));
await opener.open(resource);
return undefined;
}
async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileService, textFileService: ITextFileService) {
if (await fileService.exists(pick.filepath)) {
return;
}
const contents = [
'{',
'\t// Place your snippets for ' + pick.label + ' here. Each snippet is defined under a snippet name and has a prefix, body and ',
'\t// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:',
'\t// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the ',
'\t// same ids are connected.',
'\t// Example:',
'\t// "Print to console": {',
'\t// \t"prefix": "log",',
'\t// \t"body": [',
'\t// \t\t"console.log(\'$1\');",',
'\t// \t\t"$2"',
'\t// \t],',
'\t// \t"description": "Log output to console"',
'\t// }',
'}'
].join('\n');
await textFileService.write(pick.filepath, contents);
}
CommandsRegistry.registerCommand(id, async (accessor): Promise<any> => {
const snippetService = accessor.get(ISnippetsService);
const quickInputService = accessor.get(IQuickInputService);
const opener = accessor.get(IOpenerService);
const modeService = accessor.get(IModeService);
const envService = accessor.get(IEnvironmentService);
const workspaceService = accessor.get(IWorkspaceContextService);
const fileService = accessor.get(IFileService);
const textFileService = accessor.get(ITextFileService);
const picks = await computePicks(snippetService, envService, modeService);
const existing: QuickPickInput[] = picks.existing;
type SnippetPick = IQuickPickItem & { uri: URI } & { scope: string };
const globalSnippetPicks: SnippetPick[] = [{
scope: nls.localize('new.global_scope', 'global'),
label: nls.localize('new.global', "New Global Snippets file..."),
uri: envService.snippetsHome
}];
const workspaceSnippetPicks: SnippetPick[] = [];
for (const folder of workspaceService.getWorkspace().folders) {
workspaceSnippetPicks.push({
scope: nls.localize('new.workspace_scope', "{0} workspace", folder.name),
label: nls.localize('new.folder', "New Snippets file for '{0}'...", folder.name),
uri: folder.toResource('.vscode')
});
}
if (existing.length > 0) {
existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") });
existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") });
} else {
existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") });
}
const pick = await quickInputService.pick(([] as QuickPickInput[]).concat(existing, globalSnippetPicks, workspaceSnippetPicks, picks.future), {
placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"),
matchOnDescription: true
});
if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) {
return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener);
} else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) {
return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener);
} else if (ISnippetPick.is(pick)) {
if (pick.hint) {
await createLanguageSnippetFile(pick, fileService, textFileService);
}
return opener.open(pick.filepath);
}
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id,
title: { value: nls.localize('openSnippet.label', "Configure User Snippets"), original: 'Configure User Snippets' },
category: { value: nls.localize('preferences', "Preferences"), original: 'Preferences' }
}
});
MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
group: '3_snippets',
command: {
id,
title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets")
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '3_snippets',
command: {
id,
title: nls.localize('userSnippets', "User Snippets")
},
order: 1
});

View File

@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { registerEditorAction, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions';
import { IModeService } from 'vs/editor/common/services/modeService';
import { LanguageId } from 'vs/editor/common/modes';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { IQuickPickItem, IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
interface ISnippetPick extends IQuickPickItem {
snippet: Snippet;
}
class Args {
static fromUser(arg: any): Args {
if (!arg || typeof arg !== 'object') {
return Args._empty;
}
let { snippet, name, langId } = arg;
if (typeof snippet !== 'string') {
snippet = undefined;
}
if (typeof name !== 'string') {
name = undefined;
}
if (typeof langId !== 'string') {
langId = undefined;
}
return new Args(snippet, name, langId);
}
private static readonly _empty = new Args(undefined, undefined, undefined);
private constructor(
public readonly snippet: string | undefined,
public readonly name: string | undefined,
public readonly langId: string | undefined
) { }
}
class InsertSnippetAction extends EditorAction {
constructor() {
super({
id: 'editor.action.insertSnippet',
label: nls.localize('snippet.suggestions.label', "Insert Snippet"),
alias: 'Insert Snippet',
precondition: EditorContextKeys.writable,
description: {
description: `Insert Snippet`,
args: [{
name: 'args',
schema: {
'type': 'object',
'properties': {
'snippet': {
'type': 'string'
},
'langId': {
'type': 'string',
},
'name': {
'type': 'string'
}
},
}
}]
}
});
}
async run(accessor: ServicesAccessor, editor: ICodeEditor, arg: any): Promise<void> {
const modeService = accessor.get(IModeService);
const snippetService = accessor.get(ISnippetsService);
if (!editor.hasModel()) {
return;
}
const clipboardService = accessor.get(IClipboardService);
const quickInputService = accessor.get(IQuickInputService);
const snippet = await new Promise<Snippet | undefined>(async (resolve, reject) => {
const { lineNumber, column } = editor.getPosition();
let { snippet, name, langId } = Args.fromUser(arg);
if (snippet) {
return resolve(new Snippet(
[],
'',
'',
'',
snippet,
'',
SnippetSource.User,
));
}
let languageId = LanguageId.Null;
if (langId) {
const otherLangId = modeService.getLanguageIdentifier(langId);
if (otherLangId) {
languageId = otherLangId.id;
}
} else {
editor.getModel().tokenizeIfCheap(lineNumber);
languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column);
// validate the `languageId` to ensure this is a user
// facing language with a name and the chance to have
// snippets, else fall back to the outer language
const otherLangId = modeService.getLanguageIdentifier(languageId);
if (otherLangId && !modeService.getLanguageName(otherLangId.language)) {
languageId = editor.getModel().getLanguageIdentifier().id;
}
}
if (name) {
// take selected snippet
(await snippetService.getSnippets(languageId)).every(snippet => {
if (snippet.name !== name) {
return true;
}
resolve(snippet);
return false;
});
} else {
// let user pick a snippet
const snippets = (await snippetService.getSnippets(languageId)).sort(Snippet.compare);
const picks: QuickPickInput<ISnippetPick>[] = [];
let prevSnippet: Snippet | undefined;
for (const snippet of snippets) {
const pick: ISnippetPick = {
label: snippet.prefix,
detail: snippet.description,
snippet
};
if (!prevSnippet || prevSnippet.snippetSource !== snippet.snippetSource) {
let label = '';
switch (snippet.snippetSource) {
case SnippetSource.User:
label = nls.localize('sep.userSnippet', "User Snippets");
break;
case SnippetSource.Extension:
label = nls.localize('sep.extSnippet', "Extension Snippets");
break;
case SnippetSource.Workspace:
label = nls.localize('sep.workspaceSnippet', "Workspace Snippets");
break;
}
picks.push({ type: 'separator', label });
}
picks.push(pick);
prevSnippet = snippet;
}
return quickInputService.pick(picks, { matchOnDetail: true }).then(pick => resolve(pick && pick.snippet), reject);
}
});
if (!snippet) {
return;
}
let clipboardText: string | undefined;
if (snippet.needsClipboard) {
clipboardText = await clipboardService.readText();
}
SnippetController2.get(editor).insert(snippet.codeSnippet, { clipboardText });
}
}
registerEditorAction(InsertSnippetAction);
// compatibility command to make sure old keybinding are still working
CommandsRegistry.registerCommand('editor.action.showSnippets', accessor => {
return accessor.get(ICommandService).executeCommand('editor.action.insertSnippet');
});

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MarkdownString } from 'vs/base/common/htmlContent';
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';
import { CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, LanguageId, CompletionItemInsertTextRule, CompletionContext, CompletionTriggerKind, CompletionItemLabel } from 'vs/editor/common/modes';
import { IModeService } from 'vs/editor/common/services/modeService';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { localize } from 'vs/nls';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { isPatternInWord } from 'vs/base/common/filters';
import { StopWatch } from 'vs/base/common/stopwatch';
export class SnippetCompletion implements CompletionItem {
label: CompletionItemLabel;
detail: string;
insertText: string;
documentation?: MarkdownString;
range: IRange | { insert: IRange, replace: IRange };
sortText: string;
kind: CompletionItemKind;
insertTextRules: CompletionItemInsertTextRule;
constructor(
readonly snippet: Snippet,
range: IRange | { insert: IRange, replace: IRange }
) {
this.label = { name: snippet.prefix, type: snippet.name };
this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source);
this.insertText = snippet.codeSnippet;
this.range = range;
this.sortText = `${snippet.snippetSource === SnippetSource.Extension ? 'z' : 'a'}-${snippet.prefix}`;
this.kind = CompletionItemKind.Snippet;
this.insertTextRules = CompletionItemInsertTextRule.InsertAsSnippet;
}
resolve(): this {
this.documentation = new MarkdownString().appendCodeblock('', new SnippetParser().text(this.snippet.codeSnippet));
return this;
}
static compareByLabel(a: SnippetCompletion, b: SnippetCompletion): number {
return compare(a.label.name, b.label.name);
}
}
export class SnippetCompletionProvider implements CompletionItemProvider {
readonly _debugDisplayName = 'snippetCompletions';
constructor(
@IModeService private readonly _modeService: IModeService,
@ISnippetsService private readonly _snippets: ISnippetsService
) {
//
}
async provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext): Promise<CompletionList> {
if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && context.triggerCharacter === ' ') {
// no snippets when suggestions have been triggered by space
return { suggestions: [] };
}
const sw = new StopWatch(true);
const languageId = this._getLanguageIdAtPosition(model, position);
const snippets = await this._snippets.getSnippets(languageId);
let pos = { lineNumber: position.lineNumber, column: 1 };
let lineOffsets: number[] = [];
const lineContent = model.getLineContent(position.lineNumber).toLowerCase();
const endsInWhitespace = /\s/.test(lineContent[position.column - 2]);
while (pos.column < position.column) {
let word = model.getWordAtPosition(pos);
if (word) {
// at a word
lineOffsets.push(word.startColumn - 1);
pos.column = word.endColumn + 1;
if (word.endColumn < position.column && !/\s/.test(lineContent[word.endColumn - 1])) {
lineOffsets.push(word.endColumn - 1);
}
}
else if (!/\s/.test(lineContent[pos.column - 1])) {
// at a none-whitespace character
lineOffsets.push(pos.column - 1);
pos.column += 1;
}
else {
// always advance!
pos.column += 1;
}
}
const availableSnippets = new Set<Snippet>(snippets);
const suggestions: SnippetCompletion[] = [];
const columnOffset = position.column - 1;
for (const start of lineOffsets) {
availableSnippets.forEach(snippet => {
if (isPatternInWord(lineContent, start, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) {
const prefixPos = position.column - (1 + start);
const prefixRestLen = snippet.prefixLow.length - prefixPos;
const endsWithPrefixRest = compareSubstring(lineContent, snippet.prefixLow, columnOffset, (columnOffset) + prefixRestLen, prefixPos, prefixPos + prefixRestLen);
const endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column;
const replace = Range.fromPositions(position.delta(0, -prefixPos), { lineNumber: position.lineNumber, column: endColumn });
const insert = replace.setEndPosition(position.lineNumber, position.column);
suggestions.push(new SnippetCompletion(snippet, { replace, insert }));
availableSnippets.delete(snippet);
}
});
}
if (endsInWhitespace || lineOffsets.length === 0) {
// add remaing snippets when the current prefix ends in whitespace or when no
// interesting positions have been found
availableSnippets.forEach(snippet => {
const insert = Range.fromPositions(position);
const replace = lineContent.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
suggestions.sort(SnippetCompletion.compareByLabel);
for (let i = 0; i < suggestions.length; i++) {
let item = suggestions[i];
let to = i + 1;
for (; to < suggestions.length && item.label === suggestions[to].label; to++) {
suggestions[to].label.name = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[to].label.name, suggestions[to].snippet.name);
}
if (to > i + 1) {
suggestions[i].label.name = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[i].label.name, suggestions[i].snippet.name);
i = to;
}
}
return {
suggestions,
duration: sw.elapsed()
};
}
resolveCompletionItem(item: CompletionItem): CompletionItem {
return (item instanceof SnippetCompletion) ? item.resolve() : item;
}
private _getLanguageIdAtPosition(model: ITextModel, position: Position): LanguageId {
// validate the `languageId` to ensure this is a user
// facing language with a name and the chance to have
// snippets, else fall back to the outer language
model.tokenizeIfCheap(position.lineNumber);
let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column);
const languageIdentifier = this._modeService.getLanguageIdentifier(languageId);
if (languageIdentifier && !this._modeService.getLanguageName(languageIdentifier.language)) {
languageId = model.getLanguageIdentifier().id;
}
return languageId;
}
}

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import * as nls from 'vs/nls';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { LanguageId } from 'vs/editor/common/modes';
import { SnippetFile, Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
export const ISnippetsService = createDecorator<ISnippetsService>('snippetService');
export interface ISnippetsService {
readonly _serviceBrand: undefined;
getSnippetFiles(): Promise<Iterable<SnippetFile>>;
getSnippets(languageId: LanguageId): Promise<Snippet[]>;
getSnippetsSync(languageId: LanguageId): Snippet[];
}
const languageScopeSchemaId = 'vscode://schemas/snippets';
const snippetSchemaProperties: IJSONSchemaMap = {
prefix: {
description: nls.localize('snippetSchema.json.prefix', 'The prefix to use when selecting the snippet in intellisense'),
type: ['string', 'array']
},
body: {
markdownDescription: nls.localize('snippetSchema.json.body', 'The snippet content. Use `$1`, `${1:defaultText}` to define cursor positions, use `$0` for the final cursor position. Insert variable values with `${varName}` and `${varName:defaultText}`, e.g. `This is file: $TM_FILENAME`.'),
type: ['string', 'array'],
items: {
type: 'string'
}
},
description: {
description: nls.localize('snippetSchema.json.description', 'The snippet description.'),
type: ['string', 'array']
}
};
const languageScopeSchema: IJSONSchema = {
id: languageScopeSchemaId,
allowComments: true,
allowTrailingCommas: true,
defaultSnippets: [{
label: nls.localize('snippetSchema.json.default', "Empty snippet"),
body: { '${1:snippetName}': { 'prefix': '${2:prefix}', 'body': '${3:snippet}', 'description': '${4:description}' } }
}],
type: 'object',
description: nls.localize('snippetSchema.json', 'User snippet configuration'),
additionalProperties: {
type: 'object',
required: ['prefix', 'body'],
properties: snippetSchemaProperties,
additionalProperties: false
}
};
const globalSchemaId = 'vscode://schemas/global-snippets';
const globalSchema: IJSONSchema = {
id: globalSchemaId,
allowComments: true,
allowTrailingCommas: true,
defaultSnippets: [{
label: nls.localize('snippetSchema.json.default', "Empty snippet"),
body: { '${1:snippetName}': { 'scope': '${2:scope}', 'prefix': '${3:prefix}', 'body': '${4:snippet}', 'description': '${5:description}' } }
}],
type: 'object',
description: nls.localize('snippetSchema.json', 'User snippet configuration'),
additionalProperties: {
type: 'object',
required: ['prefix', 'body'],
properties: {
...snippetSchemaProperties,
scope: {
description: nls.localize('snippetSchema.json.scope', "A list of language names to which this snippet applies, e.g. 'typescript,javascript'."),
type: 'string'
}
},
additionalProperties: false
}
};
const reg = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);
reg.registerSchema(languageScopeSchemaId, languageScopeSchema);
reg.registerSchema(globalSchemaId, globalSchema);

View File

@@ -0,0 +1,296 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { parse as jsonParse, getNodeType } from 'vs/base/common/json';
import { forEach } from 'vs/base/common/collections';
import { localize } from 'vs/nls';
import { extname, basename } from 'vs/base/common/path';
import { SnippetParser, Variable, Placeholder, Text } from 'vs/editor/contrib/snippet/snippetParser';
import { KnownSnippetVariableNames } from 'vs/editor/contrib/snippet/snippetVariables';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { IdleValue } from 'vs/base/common/async';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
class SnippetBodyInsights {
readonly codeSnippet: string;
readonly isBogous: boolean;
readonly needsClipboard: boolean;
constructor(body: string) {
// init with defaults
this.isBogous = false;
this.needsClipboard = false;
this.codeSnippet = body;
// check snippet...
const textmateSnippet = new SnippetParser().parse(body, false);
let placeholders = new Map<string, number>();
let placeholderMax = 0;
for (const placeholder of textmateSnippet.placeholders) {
placeholderMax = Math.max(placeholderMax, placeholder.index);
}
let stack = [...textmateSnippet.children];
while (stack.length > 0) {
const marker = stack.shift()!;
if (marker instanceof Variable) {
if (marker.children.length === 0 && !KnownSnippetVariableNames[marker.name]) {
// a 'variable' without a default value and not being one of our supported
// variables is automatically turned into a placeholder. This is to restore
// a bug we had before. So `${foo}` becomes `${N:foo}`
const index = placeholders.has(marker.name) ? placeholders.get(marker.name)! : ++placeholderMax;
placeholders.set(marker.name, index);
const synthetic = new Placeholder(index).appendChild(new Text(marker.name));
textmateSnippet.replace(marker, [synthetic]);
this.isBogous = true;
}
if (marker.name === 'CLIPBOARD') {
this.needsClipboard = true;
}
} else {
// recurse
stack.push(...marker.children);
}
}
if (this.isBogous) {
this.codeSnippet = textmateSnippet.toTextmateString();
}
}
}
export class Snippet {
private readonly _bodyInsights: IdleValue<SnippetBodyInsights>;
readonly prefixLow: string;
constructor(
readonly scopes: string[],
readonly name: string,
readonly prefix: string,
readonly description: string,
readonly body: string,
readonly source: string,
readonly snippetSource: SnippetSource,
) {
//
this.prefixLow = prefix ? prefix.toLowerCase() : prefix;
this._bodyInsights = new IdleValue(() => new SnippetBodyInsights(this.body));
}
get codeSnippet(): string {
return this._bodyInsights.value.codeSnippet;
}
get isBogous(): boolean {
return this._bodyInsights.value.isBogous;
}
get needsClipboard(): boolean {
return this._bodyInsights.value.needsClipboard;
}
static compare(a: Snippet, b: Snippet): number {
if (a.snippetSource < b.snippetSource) {
return -1;
} else if (a.snippetSource > b.snippetSource) {
return 1;
} else if (a.name > b.name) {
return 1;
} else if (a.name < b.name) {
return -1;
} else {
return 0;
}
}
}
interface JsonSerializedSnippet {
body: string;
scope: string;
prefix: string | string[];
description: string;
}
function isJsonSerializedSnippet(thing: any): thing is JsonSerializedSnippet {
return Boolean((<JsonSerializedSnippet>thing).body) && Boolean((<JsonSerializedSnippet>thing).prefix);
}
interface JsonSerializedSnippets {
[name: string]: JsonSerializedSnippet | { [name: string]: JsonSerializedSnippet };
}
export const enum SnippetSource {
User = 1,
Workspace = 2,
Extension = 3,
}
export class SnippetFile {
readonly data: Snippet[] = [];
readonly isGlobalSnippets: boolean;
readonly isUserSnippets: boolean;
private _loadPromise?: Promise<this>;
constructor(
readonly source: SnippetSource,
readonly location: URI,
public defaultScopes: string[] | undefined,
private readonly _extension: IExtensionDescription | undefined,
private readonly _fileService: IFileService,
private readonly _extensionResourceLoaderService: IExtensionResourceLoaderService
) {
this.isGlobalSnippets = extname(location.path) === '.code-snippets';
this.isUserSnippets = !this._extension;
}
select(selector: string, bucket: Snippet[]): void {
if (this.isGlobalSnippets || !this.isUserSnippets) {
this._scopeSelect(selector, bucket);
} else {
this._filepathSelect(selector, bucket);
}
}
private _filepathSelect(selector: string, bucket: Snippet[]): void {
// for `fooLang.json` files all snippets are accepted
if (selector + '.json' === basename(this.location.path)) {
bucket.push(...this.data);
}
}
private _scopeSelect(selector: string, bucket: Snippet[]): void {
// for `my.code-snippets` files we need to look at each snippet
for (const snippet of this.data) {
const len = snippet.scopes.length;
if (len === 0) {
// always accept
bucket.push(snippet);
} else {
for (let i = 0; i < len; i++) {
// match
if (snippet.scopes[i] === selector) {
bucket.push(snippet);
break; // match only once!
}
}
}
}
let idx = selector.lastIndexOf('.');
if (idx >= 0) {
this._scopeSelect(selector.substring(0, idx), bucket);
}
}
private async _load(): Promise<string> {
if (this._extension) {
return this._extensionResourceLoaderService.readExtensionResource(this.location);
} else {
const content = await this._fileService.readFile(this.location);
return content.value.toString();
}
}
load(): Promise<this> {
if (!this._loadPromise) {
this._loadPromise = Promise.resolve(this._load()).then(content => {
const data = <JsonSerializedSnippets>jsonParse(content);
if (getNodeType(data) === 'object') {
forEach(data, entry => {
const { key: name, value: scopeOrTemplate } = entry;
if (isJsonSerializedSnippet(scopeOrTemplate)) {
this._parseSnippet(name, scopeOrTemplate, this.data);
} else {
forEach(scopeOrTemplate, entry => {
const { key: name, value: template } = entry;
this._parseSnippet(name, template, this.data);
});
}
});
}
return this;
});
}
return this._loadPromise;
}
reset(): void {
this._loadPromise = undefined;
this.data.length = 0;
}
private _parseSnippet(name: string, snippet: JsonSerializedSnippet, bucket: Snippet[]): void {
let { prefix, body, description } = snippet;
if (Array.isArray(body)) {
body = body.join('\n');
}
if (Array.isArray(description)) {
description = description.join('\n');
}
if ((typeof prefix !== 'string' && !Array.isArray(prefix)) || typeof body !== 'string') {
return;
}
let scopes: string[];
if (this.defaultScopes) {
scopes = this.defaultScopes;
} else if (typeof snippet.scope === 'string') {
scopes = snippet.scope.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s));
} else {
scopes = [];
}
let source: string;
if (this._extension) {
// extension snippet -> show the name of the extension
source = this._extension.displayName || this._extension.name;
} else if (this.source === SnippetSource.Workspace) {
// workspace -> only *.code-snippets files
source = localize('source.workspaceSnippetGlobal', "Workspace Snippet");
} else {
// user -> global (*.code-snippets) and language snippets
if (this.isGlobalSnippets) {
source = localize('source.userSnippetGlobal', "Global User Snippet");
} else {
source = localize('source.userSnippet', "User Snippet");
}
}
let prefixes = Array.isArray(prefix) ? prefix : [prefix];
prefixes.forEach(p => {
bucket.push(new Snippet(
scopes,
name,
p,
description,
body,
source,
this.source
));
});
}
}

View File

@@ -0,0 +1,359 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { combinedDisposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import * as resources from 'vs/base/common/resources';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { LanguageId } from 'vs/editor/common/modes';
import { IModeService } from 'vs/editor/common/services/modeService';
import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest';
import { localize } from 'vs/nls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { FileChangeType, IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
import { Snippet, SnippetFile, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService';
import { SnippetCompletionProvider } from './snippetCompletionProvider';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
namespace snippetExt {
export interface ISnippetsExtensionPoint {
language: string;
path: string;
}
export interface IValidSnippetsExtensionPoint {
language: string;
location: URI;
}
export function toValidSnippet(extension: IExtensionPointUser<ISnippetsExtensionPoint[]>, snippet: ISnippetsExtensionPoint, modeService: IModeService): IValidSnippetsExtensionPoint | null {
if (isFalsyOrWhitespace(snippet.path)) {
extension.collector.error(localize(
'invalid.path.0',
"Expected string in `contributes.{0}.path`. Provided value: {1}",
extension.description.name, String(snippet.path)
));
return null;
}
if (isFalsyOrWhitespace(snippet.language) && !snippet.path.endsWith('.code-snippets')) {
extension.collector.error(localize(
'invalid.language.0',
"When omitting the language, the value of `contributes.{0}.path` must be a `.code-snippets`-file. Provided value: {1}",
extension.description.name, String(snippet.path)
));
return null;
}
if (!isFalsyOrWhitespace(snippet.language) && !modeService.isRegisteredMode(snippet.language)) {
extension.collector.error(localize(
'invalid.language',
"Unknown language in `contributes.{0}.language`. Provided value: {1}",
extension.description.name, String(snippet.language)
));
return null;
}
const extensionLocation = extension.description.extensionLocation;
const snippetLocation = resources.joinPath(extensionLocation, snippet.path);
if (!resources.isEqualOrParent(snippetLocation, extensionLocation)) {
extension.collector.error(localize(
'invalid.path.1',
"Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.",
extension.description.name, snippetLocation.path, extensionLocation.path
));
return null;
}
return {
language: snippet.language,
location: snippetLocation
};
}
export const snippetsContribution: IJSONSchema = {
description: localize('vscode.extension.contributes.snippets', 'Contributes snippets.'),
type: 'array',
defaultSnippets: [{ body: [{ language: '', path: '' }] }],
items: {
type: 'object',
defaultSnippets: [{ body: { language: '${1:id}', path: './snippets/${2:id}.json.' } }],
properties: {
language: {
description: localize('vscode.extension.contributes.snippets-language', 'Language identifier for which this snippet is contributed to.'),
type: 'string'
},
path: {
description: localize('vscode.extension.contributes.snippets-path', 'Path of the snippets file. The path is relative to the extension folder and typically starts with \'./snippets/\'.'),
type: 'string'
}
}
}
};
export const point = ExtensionsRegistry.registerExtensionPoint<snippetExt.ISnippetsExtensionPoint[]>({
extensionPoint: 'snippets',
deps: [languagesExtPoint],
jsonSchema: snippetExt.snippetsContribution
});
}
function watch(service: IFileService, resource: URI, callback: () => any): IDisposable {
return combinedDisposable(
service.watch(resource),
service.onDidFilesChange(e => {
if (e.affects(resource)) {
callback();
}
})
);
}
class SnippetsService implements ISnippetsService {
readonly _serviceBrand: undefined;
private readonly _disposables = new DisposableStore();
private readonly _pendingWork: Promise<any>[] = [];
private readonly _files = new Map<string, SnippetFile>();
constructor(
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@IModeService private readonly _modeService: IModeService,
@ILogService private readonly _logService: ILogService,
@IFileService private readonly _fileService: IFileService,
@IExtensionResourceLoaderService private readonly _extensionResourceLoaderService: IExtensionResourceLoaderService,
@ILifecycleService lifecycleService: ILifecycleService,
) {
this._pendingWork.push(Promise.resolve(lifecycleService.when(LifecyclePhase.Restored).then(() => {
this._initExtensionSnippets();
this._initUserSnippets();
this._initWorkspaceSnippets();
})));
setSnippetSuggestSupport(new SnippetCompletionProvider(this._modeService, this));
}
dispose(): void {
this._disposables.dispose();
}
private _joinSnippets(): Promise<any> {
const promises = this._pendingWork.slice(0);
this._pendingWork.length = 0;
return Promise.all(promises);
}
async getSnippetFiles(): Promise<Iterable<SnippetFile>> {
await this._joinSnippets();
return this._files.values();
}
getSnippets(languageId: LanguageId): Promise<Snippet[]> {
return this._joinSnippets().then(() => {
const result: Snippet[] = [];
const promises: Promise<any>[] = [];
const languageIdentifier = this._modeService.getLanguageIdentifier(languageId);
if (languageIdentifier) {
const langName = languageIdentifier.language;
for (const file of this._files.values()) {
promises.push(file.load()
.then(file => file.select(langName, result))
.catch(err => this._logService.error(err, file.location.toString()))
);
}
}
return Promise.all(promises).then(() => result);
});
}
getSnippetsSync(languageId: LanguageId): Snippet[] {
const result: Snippet[] = [];
const languageIdentifier = this._modeService.getLanguageIdentifier(languageId);
if (languageIdentifier) {
const langName = languageIdentifier.language;
for (const file of this._files.values()) {
// kick off loading (which is a noop in case it's already loaded)
// and optimistically collect snippets
file.load().catch(err => { /*ignore*/ });
file.select(langName, result);
}
}
return result;
}
// --- loading, watching
private _initExtensionSnippets(): void {
snippetExt.point.setHandler(extensions => {
for (let [key, value] of this._files) {
if (value.source === SnippetSource.Extension) {
this._files.delete(key);
}
}
for (const extension of extensions) {
for (const contribution of extension.value) {
const validContribution = snippetExt.toValidSnippet(extension, contribution, this._modeService);
if (!validContribution) {
continue;
}
const resource = validContribution.location.toString();
const file = this._files.get(resource);
if (file) {
if (file.defaultScopes) {
file.defaultScopes.push(validContribution.language);
} else {
file.defaultScopes = [];
}
} else {
const file = new SnippetFile(SnippetSource.Extension, validContribution.location, validContribution.language ? [validContribution.language] : undefined, extension.description, this._fileService, this._extensionResourceLoaderService);
this._files.set(file.location.toString(), file);
if (this._environmentService.isExtensionDevelopment) {
file.load().then(file => {
// warn about bad tabstop/variable usage
if (file.data.some(snippet => snippet.isBogous)) {
extension.collector.warn(localize(
'badVariableUse',
"One or more snippets from the extension '{0}' very likely confuse snippet-variables and snippet-placeholders (see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax for more details)",
extension.description.name
));
}
}, err => {
// generic error
extension.collector.warn(localize(
'badFile',
"The snippet file \"{0}\" could not be read.",
file.location.toString()
));
});
}
}
}
}
});
}
private _initWorkspaceSnippets(): void {
// workspace stuff
let disposables = new DisposableStore();
let updateWorkspaceSnippets = () => {
disposables.clear();
this._pendingWork.push(this._initWorkspaceFolderSnippets(this._contextService.getWorkspace(), disposables));
};
this._disposables.add(disposables);
this._disposables.add(this._contextService.onDidChangeWorkspaceFolders(updateWorkspaceSnippets));
this._disposables.add(this._contextService.onDidChangeWorkbenchState(updateWorkspaceSnippets));
updateWorkspaceSnippets();
}
private _initWorkspaceFolderSnippets(workspace: IWorkspace, bucket: DisposableStore): Promise<any> {
let promises = workspace.folders.map(folder => {
const snippetFolder = folder.toResource('.vscode');
return this._fileService.exists(snippetFolder).then(value => {
if (value) {
this._initFolderSnippets(SnippetSource.Workspace, snippetFolder, bucket);
} else {
// watch
bucket.add(this._fileService.onDidFilesChange(e => {
if (e.contains(snippetFolder, FileChangeType.ADDED)) {
this._initFolderSnippets(SnippetSource.Workspace, snippetFolder, bucket);
}
}));
}
});
});
return Promise.all(promises);
}
private _initUserSnippets(): Promise<any> {
const userSnippetsFolder = this._environmentService.snippetsHome;
return this._fileService.createFolder(userSnippetsFolder).then(() => this._initFolderSnippets(SnippetSource.User, userSnippetsFolder, this._disposables));
}
private _initFolderSnippets(source: SnippetSource, folder: URI, bucket: DisposableStore): Promise<any> {
const disposables = new DisposableStore();
const addFolderSnippets = async () => {
disposables.clear();
if (!await this._fileService.exists(folder)) {
return;
}
try {
const stat = await this._fileService.resolve(folder);
for (const entry of stat.children || []) {
disposables.add(this._addSnippetFile(entry.resource, source));
}
} catch (err) {
this._logService.error(`Failed snippets from folder '${folder.toString()}'`, err);
}
};
bucket.add(watch(this._fileService, folder, addFolderSnippets));
bucket.add(disposables);
return addFolderSnippets();
}
private _addSnippetFile(uri: URI, source: SnippetSource): IDisposable {
const ext = resources.extname(uri);
const key = uri.toString();
if (source === SnippetSource.User && ext === '.json') {
const langName = resources.basename(uri).replace(/\.json/, '');
this._files.set(key, new SnippetFile(source, uri, [langName], undefined, this._fileService, this._extensionResourceLoaderService));
} else if (ext === '.code-snippets') {
this._files.set(key, new SnippetFile(source, uri, undefined, undefined, this._fileService, this._extensionResourceLoaderService));
}
return {
dispose: () => this._files.delete(key)
};
}
}
registerSingleton(ISnippetsService, SnippetsService, true);
export interface ISimpleModel {
getLineContent(lineNumber: number): string;
}
export function getNonWhitespacePrefix(model: ISimpleModel, position: Position): string {
/**
* Do not analyze more characters
*/
const MAX_PREFIX_LENGTH = 100;
let line = model.getLineContent(position.lineNumber).substr(0, position.column - 1);
let minChIndex = Math.max(0, line.length - MAX_PREFIX_LENGTH);
for (let chIndex = line.length - 1; chIndex >= minChIndex; chIndex--) {
let ch = line.charAt(chIndex);
if (/\s/.test(ch)) {
return line.substr(chIndex + 1);
}
}
if (minChIndex === 0) {
return line;
}
return '';
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode } from 'vs/base/common/keyCodes';
import { RawContextKey, IContextKeyService, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ISnippetsService } from './snippets.contribution';
import { getNonWhitespacePrefix } from './snippetsService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { Range } from 'vs/editor/common/core/range';
import { registerEditorContribution, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/suggest';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Snippet } from './snippetsFile';
import { SnippetCompletion } from './snippetCompletionProvider';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState';
export class TabCompletionController implements IEditorContribution {
public static readonly ID = 'editor.tabCompletionController';
static readonly ContextKey = new RawContextKey<boolean>('hasSnippetCompletions', undefined);
public static get(editor: ICodeEditor): TabCompletionController {
return editor.getContribution<TabCompletionController>(TabCompletionController.ID);
}
private _hasSnippets: IContextKey<boolean>;
private _activeSnippets: Snippet[] = [];
private _enabled?: boolean;
private _selectionListener?: IDisposable;
private readonly _configListener: IDisposable;
constructor(
private readonly _editor: ICodeEditor,
@ISnippetsService private readonly _snippetService: ISnippetsService,
@IClipboardService private readonly _clipboardService: IClipboardService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
this._hasSnippets = TabCompletionController.ContextKey.bindTo(contextKeyService);
this._configListener = this._editor.onDidChangeConfiguration(e => {
if (e.hasChanged(EditorOption.tabCompletion)) {
this._update();
}
});
this._update();
}
dispose(): void {
this._configListener.dispose();
this._selectionListener?.dispose();
}
private _update(): void {
const enabled = this._editor.getOption(EditorOption.tabCompletion) === 'onlySnippets';
if (this._enabled !== enabled) {
this._enabled = enabled;
if (!this._enabled) {
this._selectionListener?.dispose();
} else {
this._selectionListener = this._editor.onDidChangeCursorSelection(e => this._updateSnippets());
if (this._editor.getModel()) {
this._updateSnippets();
}
}
}
}
private _updateSnippets(): void {
// reset first
this._activeSnippets = [];
if (!this._editor.hasModel()) {
return;
}
// lots of dance for getting the
const selection = this._editor.getSelection();
const model = this._editor.getModel();
model.tokenizeIfCheap(selection.positionLineNumber);
const id = model.getLanguageIdAtPosition(selection.positionLineNumber, selection.positionColumn);
const snippets = this._snippetService.getSnippetsSync(id);
if (!snippets) {
// nothing for this language
this._hasSnippets.set(false);
return;
}
if (Range.isEmpty(selection)) {
// empty selection -> real text (no whitespace) left of cursor
const prefix = getNonWhitespacePrefix(model, selection.getPosition());
if (prefix) {
for (const snippet of snippets) {
if (prefix.endsWith(snippet.prefix)) {
this._activeSnippets.push(snippet);
}
}
}
} else if (!Range.spansMultipleLines(selection) && model.getValueLengthInRange(selection) <= 100) {
// actual selection -> snippet must be a full match
const selected = model.getValueInRange(selection);
if (selected) {
for (const snippet of snippets) {
if (selected === snippet.prefix) {
this._activeSnippets.push(snippet);
}
}
}
}
this._hasSnippets.set(this._activeSnippets.length > 0);
}
async performSnippetCompletions() {
if (!this._editor.hasModel()) {
return;
}
if (this._activeSnippets.length === 1) {
// one -> just insert
const [snippet] = this._activeSnippets;
// async clipboard access might be required and in that case
// we need to check if the editor has changed in flight and then
// bail out (or be smarter than that)
let clipboardText: string | undefined;
if (snippet.needsClipboard) {
const state = new EditorState(this._editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);
clipboardText = await this._clipboardService.readText();
if (!state.validate(this._editor)) {
return;
}
}
SnippetController2.get(this._editor).insert(snippet.codeSnippet, {
overwriteBefore: snippet.prefix.length, overwriteAfter: 0,
clipboardText
});
} else if (this._activeSnippets.length > 1) {
// two or more -> show IntelliSense box
const position = this._editor.getPosition();
showSimpleSuggestions(this._editor, this._activeSnippets.map(snippet => {
const range = Range.fromPositions(position.delta(0, -snippet.prefix.length), position);
return new SnippetCompletion(snippet, range);
}));
}
}
}
registerEditorContribution(TabCompletionController.ID, TabCompletionController);
const TabCompletionCommand = EditorCommand.bindToContribution<TabCompletionController>(TabCompletionController.get);
registerEditorCommand(new TabCompletionCommand({
id: 'insertSnippet',
precondition: TabCompletionController.ContextKey,
handler: x => x.performSnippetCompletions(),
kbOpts: {
weight: KeybindingWeight.EditorContrib,
kbExpr: ContextKeyExpr.and(
EditorContextKeys.editorTextFocus,
EditorContextKeys.tabDoesNotMoveFocus,
SnippetController2.InSnippetMode.toNegated()
),
primary: KeyCode.Tab
}
}));

View File

@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { SnippetFile, Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { URI } from 'vs/base/common/uri';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
suite('Snippets', function () {
class TestSnippetFile extends SnippetFile {
constructor(filepath: URI, snippets: Snippet[]) {
super(SnippetSource.Extension, filepath, undefined, undefined, undefined!, undefined!);
this.data.push(...snippets);
}
}
test('SnippetFile#select', () => {
let file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), []);
let bucket: Snippet[] = [];
file.select('', bucket);
assert.equal(bucket.length, 0);
file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [
new Snippet(['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User),
new Snippet(['foo'], 'FooSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User),
new Snippet(['bar'], 'BarSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User),
new Snippet(['bar.comment'], 'BarSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User),
new Snippet(['bar.strings'], 'BarSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User),
new Snippet(['bazz', 'bazz'], 'BazzSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User),
]);
bucket = [];
file.select('foo', bucket);
assert.equal(bucket.length, 2);
bucket = [];
file.select('fo', bucket);
assert.equal(bucket.length, 0);
bucket = [];
file.select('bar', bucket);
assert.equal(bucket.length, 1);
bucket = [];
file.select('bar.comment', bucket);
assert.equal(bucket.length, 2);
bucket = [];
file.select('bazz', bucket);
assert.equal(bucket.length, 1);
});
test('SnippetFile#select - any scope', function () {
let file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [
new Snippet([], 'AnySnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User),
new Snippet(['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User),
]);
let bucket: Snippet[] = [];
file.select('foo', bucket);
assert.equal(bucket.length, 2);
});
test('Snippet#needsClipboard', function () {
function assertNeedsClipboard(body: string, expected: boolean): void {
let snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User);
assert.equal(snippet.needsClipboard, expected);
assert.equal(SnippetParser.guessNeedsClipboard(body), expected);
}
assertNeedsClipboard('foo$CLIPBOARD', true);
assertNeedsClipboard('${CLIPBOARD}', true);
assertNeedsClipboard('foo${CLIPBOARD}bar', true);
assertNeedsClipboard('foo$clipboard', false);
assertNeedsClipboard('foo${clipboard}', false);
assertNeedsClipboard('baba', false);
});
});

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { getNonWhitespacePrefix } from 'vs/workbench/contrib/snippets/browser/snippetsService';
import { Position } from 'vs/editor/common/core/position';
suite('getNonWhitespacePrefix', () => {
function assertGetNonWhitespacePrefix(line: string, column: number, expected: string): void {
let model = {
getLineContent: (lineNumber: number) => line
};
let actual = getNonWhitespacePrefix(model, new Position(1, column));
assert.equal(actual, expected);
}
test('empty line', () => {
assertGetNonWhitespacePrefix('', 1, '');
});
test('singleWordLine', () => {
assertGetNonWhitespacePrefix('something', 1, '');
assertGetNonWhitespacePrefix('something', 2, 's');
assertGetNonWhitespacePrefix('something', 3, 'so');
assertGetNonWhitespacePrefix('something', 4, 'som');
assertGetNonWhitespacePrefix('something', 5, 'some');
assertGetNonWhitespacePrefix('something', 6, 'somet');
assertGetNonWhitespacePrefix('something', 7, 'someth');
assertGetNonWhitespacePrefix('something', 8, 'somethi');
assertGetNonWhitespacePrefix('something', 9, 'somethin');
assertGetNonWhitespacePrefix('something', 10, 'something');
});
test('two word line', () => {
assertGetNonWhitespacePrefix('something interesting', 1, '');
assertGetNonWhitespacePrefix('something interesting', 2, 's');
assertGetNonWhitespacePrefix('something interesting', 3, 'so');
assertGetNonWhitespacePrefix('something interesting', 4, 'som');
assertGetNonWhitespacePrefix('something interesting', 5, 'some');
assertGetNonWhitespacePrefix('something interesting', 6, 'somet');
assertGetNonWhitespacePrefix('something interesting', 7, 'someth');
assertGetNonWhitespacePrefix('something interesting', 8, 'somethi');
assertGetNonWhitespacePrefix('something interesting', 9, 'somethin');
assertGetNonWhitespacePrefix('something interesting', 10, 'something');
assertGetNonWhitespacePrefix('something interesting', 11, '');
assertGetNonWhitespacePrefix('something interesting', 12, 'i');
assertGetNonWhitespacePrefix('something interesting', 13, 'in');
assertGetNonWhitespacePrefix('something interesting', 14, 'int');
assertGetNonWhitespacePrefix('something interesting', 15, 'inte');
assertGetNonWhitespacePrefix('something interesting', 16, 'inter');
assertGetNonWhitespacePrefix('something interesting', 17, 'intere');
assertGetNonWhitespacePrefix('something interesting', 18, 'interes');
assertGetNonWhitespacePrefix('something interesting', 19, 'interest');
assertGetNonWhitespacePrefix('something interesting', 20, 'interesti');
assertGetNonWhitespacePrefix('something interesting', 21, 'interestin');
assertGetNonWhitespacePrefix('something interesting', 22, 'interesting');
});
test('many separators', () => {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions?redirectlocale=en-US&redirectslug=JavaScript%2FGuide%2FRegular_Expressions#special-white-space
// \s matches a single white space character, including space, tab, form feed, line feed.
// Equivalent to [ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff].
assertGetNonWhitespacePrefix('something interesting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\tinteresting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\finteresting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\vinteresting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\u00a0interesting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\u2000interesting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\u2028interesting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\u3000interesting', 22, 'interesting');
assertGetNonWhitespacePrefix('something\ufeffinteresting', 22, 'interesting');
});
});

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
suite('SnippetRewrite', function () {
function assertRewrite(input: string, expected: string | boolean): void {
const actual = new Snippet(['foo'], 'foo', 'foo', 'foo', input, 'foo', SnippetSource.User);
if (typeof expected === 'boolean') {
assert.equal(actual.codeSnippet, input);
} else {
assert.equal(actual.codeSnippet, expected);
}
}
test('bogous variable rewrite', function () {
assertRewrite('foo', false);
assertRewrite('hello $1 world$0', false);
assertRewrite('$foo and $foo', '${1:foo} and ${1:foo}');
assertRewrite('$1 and $SELECTION and $foo', '$1 and ${SELECTION} and ${2:foo}');
assertRewrite(
[
'for (var ${index} = 0; ${index} < ${array}.length; ${index}++) {',
'\tvar ${element} = ${array}[${index}];',
'\t$0',
'}'
].join('\n'),
[
'for (var ${1:index} = 0; ${1:index} < ${2:array}.length; ${1:index}++) {',
'\tvar ${3:element} = ${2:array}[${1:index}];',
'\t$0',
'\\}'
].join('\n')
);
});
test('Snippet choices: unable to escape comma and pipe, #31521', function () {
assertRewrite('console.log(${1|not\\, not, five, 5, 1 23|});', false);
});
test('lazy bogous variable rewrite', function () {
const snippet = new Snippet(['fooLang'], 'foo', 'prefix', 'desc', 'This is ${bogous} because it is a ${var}', 'source', SnippetSource.Extension);
assert.equal(snippet.body, 'This is ${bogous} because it is a ${var}');
assert.equal(snippet.codeSnippet, 'This is ${1:bogous} because it is a ${2:var}');
assert.equal(snippet.isBogous, true);
});
});

View File

@@ -0,0 +1,500 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider';
import { Position } from 'vs/editor/common/core/position';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { CompletionContext, CompletionTriggerKind } from 'vs/editor/common/modes';
class SimpleSnippetService implements ISnippetsService {
declare readonly _serviceBrand: undefined;
constructor(readonly snippets: Snippet[]) {
}
getSnippets() {
return Promise.resolve(this.getSnippetsSync());
}
getSnippetsSync(): Snippet[] {
return this.snippets;
}
getSnippetFiles(): any {
throw new Error();
}
}
suite('SnippetsService', function () {
suiteSetup(function () {
ModesRegistry.registerLanguage({
id: 'fooLang',
extensions: ['.fooLang',]
});
});
let modeService: ModeServiceImpl;
let snippetService: ISnippetsService;
let context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke };
setup(function () {
modeService = new ModeServiceImpl();
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'barTest',
'bar',
'',
'barCodeSnippet',
'',
SnippetSource.User
), new Snippet(
['fooLang'],
'bazzTest',
'bazz',
'',
'bazzCodeSnippet',
'',
SnippetSource.User
)]);
});
test('snippet completions - simple', function () {
const provider = new SnippetCompletionProvider(modeService, snippetService);
const model = createTextModel('', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => {
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 2);
});
});
test('snippet completions - with prefix', function () {
const provider = new SnippetCompletionProvider(modeService, snippetService);
const model = createTextModel('bar', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 4), context)!.then(result => {
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 1);
assert.deepEqual(result.suggestions[0].label, {
name: 'bar',
type: 'barTest'
});
assert.equal((result.suggestions[0].range as any).insert.startColumn, 1);
assert.equal(result.suggestions[0].insertText, 'barCodeSnippet');
});
});
test('snippet completions - with different prefixes', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'barTest',
'bar',
'',
's1',
'',
SnippetSource.User
), new Snippet(
['fooLang'],
'name',
'bar-bar',
'',
's2',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
const model = createTextModel('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang'));
await provider.provideCompletionItems(model, new Position(1, 3), context)!.then(result => {
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 2);
assert.deepEqual(result.suggestions[0].label, {
name: 'bar',
type: 'barTest'
});
assert.equal(result.suggestions[0].insertText, 's1');
assert.equal((result.suggestions[0].range as any).insert.startColumn, 1);
assert.deepEqual(result.suggestions[1].label, {
name: 'bar-bar',
type: 'name'
});
assert.equal(result.suggestions[1].insertText, 's2');
assert.equal((result.suggestions[1].range as any).insert.startColumn, 1);
});
await provider.provideCompletionItems(model, new Position(1, 5), context)!.then(result => {
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 1);
assert.deepEqual(result.suggestions[0].label, {
name: 'bar-bar',
type: 'name'
});
assert.equal(result.suggestions[0].insertText, 's2');
assert.equal((result.suggestions[0].range as any).insert.startColumn, 1);
});
await provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => {
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 2);
assert.deepEqual(result.suggestions[0].label, {
name: 'bar',
type: 'barTest'
});
assert.equal(result.suggestions[0].insertText, 's1');
assert.equal((result.suggestions[0].range as any).insert.startColumn, 5);
assert.deepEqual(result.suggestions[1].label, {
name: 'bar-bar',
type: 'name'
});
assert.equal(result.suggestions[1].insertText, 's2');
assert.equal((result.suggestions[1].range as any).insert.startColumn, 1);
});
});
test('Cannot use "<?php" as user snippet prefix anymore, #26275', function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'',
'<?php',
'',
'insert me',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('\t<?php', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 7), context)!.then(result => {
assert.equal(result.suggestions.length, 1);
model.dispose();
model = createTextModel('\t<?', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 4), context)!;
}).then(result => {
assert.equal(result.suggestions.length, 1);
assert.equal((result.suggestions[0].range as any).insert.startColumn, 2);
model.dispose();
model = createTextModel('a<?', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 4), context)!;
}).then(result => {
assert.equal(result.suggestions.length, 1);
assert.equal((result.suggestions[0].range as any).insert.startColumn, 2);
model.dispose();
});
});
test('No user snippets in suggestions, when inside the code, #30508', function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'',
'foo',
'',
'<foo>$0</foo>',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('<head>\n\t\n>/head>', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => {
assert.equal(result.suggestions.length, 1);
return provider.provideCompletionItems(model, new Position(2, 2), context)!;
}).then(result => {
assert.equal(result.suggestions.length, 1);
});
});
test('SnippetSuggest - ensure extension snippets come last ', function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'second',
'second',
'',
'second',
'',
SnippetSource.Extension
), new Snippet(
['fooLang'],
'first',
'first',
'',
'first',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => {
assert.equal(result.suggestions.length, 2);
let [first, second] = result.suggestions;
assert.deepEqual(first.label, {
name: 'first',
type: 'first'
});
assert.deepEqual(second.label, {
name: 'second',
type: 'second'
});
});
});
test('Dash in snippets prefix broken #53945', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'p-a',
'p-a',
'',
'second',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('p-', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!;
assert.equal(result.suggestions.length, 1);
result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
assert.equal(result.suggestions.length, 1);
result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
assert.equal(result.suggestions.length, 1);
});
test('No snippets suggestion on long lines beyond character 100 #58807', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'bug',
'bug',
'',
'second',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!;
assert.equal(result.suggestions.length, 1);
});
test('Type colon will trigger snippet #60746', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'bug',
'bug',
'',
'second',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel(':', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!;
assert.equal(result.suggestions.length, 0);
});
test('substring of prefix can\'t trigger snippet #60737', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'mytemplate',
'mytemplate',
'',
'second',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('template', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!;
assert.equal(result.suggestions.length, 1);
assert.deepEqual(result.suggestions[0].label, {
name: 'mytemplate',
type: 'mytemplate'
});
});
test('No snippets suggestion beyond character 100 if not at end of line #60247', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'bug',
'bug',
'',
'second',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!;
assert.equal(result.suggestions.length, 1);
});
test('issue #61296: VS code freezes when editing CSS file with emoji', async function () {
let toDispose = LanguageConfigurationRegistry.register(modeService.getLanguageIdentifier('fooLang')!, {
wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g
});
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'bug',
'-a-bug',
'',
'second',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('.🐷-a-b', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!;
assert.equal(result.suggestions.length, 1);
toDispose.dispose();
});
test('No snippets shown when triggering completions at whitespace on line that already has text #62335', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'bug',
'bug',
'',
'second',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('a ', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
assert.equal(result.suggestions.length, 1);
});
test('Snippet prefix with special chars and numbers does not work #62906', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'noblockwdelay',
'<<',
'',
'<= #dly"',
'',
SnippetSource.User
), new Snippet(
['fooLang'],
'noblockwdelay',
'11',
'',
'eleven',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel(' <', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
assert.equal(result.suggestions.length, 1);
let [first] = result.suggestions;
assert.equal((first.range as any).insert.startColumn, 2);
model = createTextModel('1', undefined, modeService.getLanguageIdentifier('fooLang'));
result = await provider.provideCompletionItems(model, new Position(1, 2), context)!;
assert.equal(result.suggestions.length, 1);
[first] = result.suggestions;
assert.equal((first.range as any).insert.startColumn, 1);
});
test('Snippet replace range', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'notWordTest',
'not word',
'',
'not word snippet',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('not wordFoo bar', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
assert.equal(result.suggestions.length, 1);
let [first] = result.suggestions;
assert.equal((first.range as any).insert.endColumn, 3);
assert.equal((first.range as any).replace.endColumn, 9);
model = createTextModel('not woFoo bar', undefined, modeService.getLanguageIdentifier('fooLang'));
result = await provider.provideCompletionItems(model, new Position(1, 3), context)!;
assert.equal(result.suggestions.length, 1);
[first] = result.suggestions;
assert.equal((first.range as any).insert.endColumn, 3);
assert.equal((first.range as any).replace.endColumn, 3);
model = createTextModel('not word', undefined, modeService.getLanguageIdentifier('fooLang'));
result = await provider.provideCompletionItems(model, new Position(1, 1), context)!;
assert.equal(result.suggestions.length, 1);
[first] = result.suggestions;
assert.equal((first.range as any).insert.endColumn, 1);
assert.equal((first.range as any).replace.endColumn, 9);
});
test('Snippet replace-range incorrect #108894', async function () {
snippetService = new SimpleSnippetService([new Snippet(
['fooLang'],
'eng',
'eng',
'',
'<span></span>',
'',
SnippetSource.User
)]);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = createTextModel('filler e KEEP ng filler', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!;
assert.equal(result.suggestions.length, 1);
let [first] = result.suggestions;
assert.equal((first.range as any).insert.endColumn, 9);
assert.equal((first.range as any).replace.endColumn, 9);
});
});