mirror of
https://github.com/coder/code-server.git
synced 2026-05-30 17:09:32 +00:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -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
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 '';
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}));
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user