mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-31 01:00:29 -04:00
Vscode merge (#4582)
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
This commit is contained in:
262
src/vs/workbench/contrib/snippets/browser/configureSnippets.ts
Normal file
262
src/vs/workbench/contrib/snippets/browser/configureSnippets.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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 { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { join, basename, dirname, extname } from 'vs/base/common/path';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
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 { values } from 'vs/base/common/map';
|
||||
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 { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
const id = 'workbench.action.openSnippets';
|
||||
|
||||
namespace ISnippetPick {
|
||||
export function is(thing: object): thing is ISnippetPick {
|
||||
return thing && typeof (<ISnippetPick>thing).filepath === 'string';
|
||||
}
|
||||
}
|
||||
|
||||
interface ISnippetPick extends IQuickPickItem {
|
||||
filepath: string;
|
||||
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.fsPath),
|
||||
filepath: file.location.fsPath,
|
||||
description: names.size === 0
|
||||
? nls.localize('global.scope', "(global)")
|
||||
: nls.localize('global.1', "({0})", values(names).join(', '))
|
||||
});
|
||||
|
||||
} else {
|
||||
// language snippet
|
||||
const mode = basename(file.location.fsPath).replace(/\.json$/, '');
|
||||
existing.push({
|
||||
label: basename(file.location.fsPath),
|
||||
description: `(${modeService.getLanguageName(mode)})`,
|
||||
filepath: file.location.fsPath
|
||||
});
|
||||
seen.add(mode);
|
||||
}
|
||||
}
|
||||
|
||||
const dir = join(envService.appSettingsHome, 'snippets');
|
||||
for (const mode of modeService.getRegisteredModes()) {
|
||||
const label = modeService.getLanguageName(mode);
|
||||
if (label && !seen.has(mode)) {
|
||||
future.push({
|
||||
label: mode,
|
||||
description: `(${label})`,
|
||||
filepath: join(dir, `${mode}.json`),
|
||||
hint: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
existing.sort((a, b) => {
|
||||
let a_ext = extname(a.filepath);
|
||||
let b_ext = extname(b.filepath);
|
||||
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, windowService: IWindowService, notificationService: INotificationService, fileService: IFileService, opener: IOpenerService) {
|
||||
|
||||
await fileService.createFolder(defaultPath);
|
||||
await timeout(100); // ensure quick pick closes...
|
||||
|
||||
const path = await windowService.showSaveDialog({
|
||||
defaultPath: defaultPath.fsPath,
|
||||
filters: [{ name: 'Code Snippets', extensions: ['code-snippets'] }]
|
||||
});
|
||||
if (!path) {
|
||||
return undefined;
|
||||
}
|
||||
const resource = URI.file(path);
|
||||
if (dirname(resource.fsPath) !== defaultPath.fsPath) {
|
||||
notificationService.error(nls.localize('badPath', "Snippets must be inside this folder: '{0}'. ", defaultPath.fsPath));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await fileService.updateContent(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) {
|
||||
if (await fileService.existsFile(URI.file(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 fileService.updateContent(URI.file(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 windowService = accessor.get(IWindowService);
|
||||
const modeService = accessor.get(IModeService);
|
||||
const envService = accessor.get(IEnvironmentService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
const workspaceService = accessor.get(IWorkspaceContextService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
|
||||
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: URI.file(join(envService.appSettingsHome, 'snippets'))
|
||||
}];
|
||||
|
||||
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, windowService, notificationService, fileService, opener);
|
||||
} else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) {
|
||||
return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, windowService, notificationService, fileService, opener);
|
||||
} else if (ISnippetPick.is(pick)) {
|
||||
if (pick.hint) {
|
||||
await createLanguageSnippetFile(pick, fileService);
|
||||
}
|
||||
return opener.open(URI.file(pick.filepath));
|
||||
}
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
|
||||
command: {
|
||||
id,
|
||||
title: { value: nls.localize('openSnippet.label', "Configure User Snippets"), original: 'Preferences: Configure User Snippets' },
|
||||
category: nls.localize('preferences', "Preferences")
|
||||
}
|
||||
});
|
||||
|
||||
// {{SQL CARBON EDIT}} - Disable unused menu item
|
||||
// MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
|
||||
// group: '3_snippets',
|
||||
// command: {
|
||||
// id,
|
||||
// title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets")
|
||||
// },
|
||||
// order: 1
|
||||
// });
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
181
src/vs/workbench/contrib/snippets/browser/insertSnippet.ts
Normal file
181
src/vs/workbench/contrib/snippets/browser/insertSnippet.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
|
||||
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'
|
||||
}
|
||||
},
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor, editor: ICodeEditor, arg: any): Promise<void> | undefined {
|
||||
const modeService = accessor.get(IModeService);
|
||||
const snippetService = accessor.get(ISnippetsService);
|
||||
|
||||
if (!editor.hasModel()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const { lineNumber, column } = editor.getPosition();
|
||||
let { snippet, name, langId } = Args.fromUser(arg);
|
||||
|
||||
return new Promise<Snippet>(async (resolve, reject) => {
|
||||
|
||||
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);
|
||||
}
|
||||
}).then(snippet => {
|
||||
if (snippet) {
|
||||
SnippetController2.get(editor).insert(snippet.codeSnippet, 0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,170 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { compare } 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 } 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';
|
||||
|
||||
export class SnippetCompletion implements CompletionItem {
|
||||
|
||||
label: string;
|
||||
detail: string;
|
||||
insertText: string;
|
||||
documentation: MarkdownString;
|
||||
range: IRange;
|
||||
sortText: string;
|
||||
kind: CompletionItemKind;
|
||||
insertTextRules: CompletionItemInsertTextRule;
|
||||
|
||||
constructor(
|
||||
readonly snippet: Snippet,
|
||||
range: IRange
|
||||
) {
|
||||
this.label = snippet.prefix;
|
||||
this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source);
|
||||
this.insertText = snippet.body;
|
||||
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));
|
||||
this.insertText = this.snippet.codeSnippet;
|
||||
return this;
|
||||
}
|
||||
|
||||
static compareByLabel(a: SnippetCompletion, b: SnippetCompletion): number {
|
||||
return compare(a.label, b.label);
|
||||
}
|
||||
}
|
||||
|
||||
export function matches(pattern: string, patternStart: number, word: string, wordStart: number): boolean {
|
||||
while (patternStart < pattern.length && wordStart < word.length) {
|
||||
if (pattern[patternStart] === word[wordStart]) {
|
||||
patternStart += 1;
|
||||
}
|
||||
wordStart += 1;
|
||||
}
|
||||
return patternStart === pattern.length;
|
||||
}
|
||||
|
||||
export class SnippetCompletionProvider implements CompletionItemProvider {
|
||||
|
||||
private static readonly _maxPrefix = 10000;
|
||||
|
||||
constructor(
|
||||
@IModeService
|
||||
private readonly _modeService: IModeService,
|
||||
@ISnippetsService
|
||||
private readonly _snippets: ISnippetsService
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext): Promise<CompletionList> | undefined {
|
||||
|
||||
if (position.column >= SnippetCompletionProvider._maxPrefix) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && context.triggerCharacter === ' ') {
|
||||
// no snippets when suggestions have been triggered by space
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const languageId = this._getLanguageIdAtPosition(model, position);
|
||||
return this._snippets.getSnippets(languageId).then(snippets => {
|
||||
|
||||
let suggestions: SnippetCompletion[];
|
||||
let pos = { lineNumber: position.lineNumber, column: 1 };
|
||||
let lineOffsets: number[] = [];
|
||||
let linePrefixLow = model.getLineContent(position.lineNumber).substr(0, position.column - 1).toLowerCase();
|
||||
let endsInWhitespace = linePrefixLow.match(/\s$/);
|
||||
|
||||
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 - 1 < linePrefixLow.length && !/\s/.test(linePrefixLow[word.endColumn - 1])) {
|
||||
lineOffsets.push(word.endColumn - 1);
|
||||
}
|
||||
}
|
||||
else if (!/\s/.test(linePrefixLow[pos.column - 1])) {
|
||||
// at a none-whitespace character
|
||||
lineOffsets.push(pos.column - 1);
|
||||
pos.column += 1;
|
||||
}
|
||||
else {
|
||||
// always advance!
|
||||
pos.column += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let availableSnippets = new Set<Snippet>();
|
||||
snippets.forEach(availableSnippets.add, availableSnippets);
|
||||
suggestions = [];
|
||||
for (let start of lineOffsets) {
|
||||
availableSnippets.forEach(snippet => {
|
||||
if (matches(linePrefixLow, start, snippet.prefixLow, 0)) {
|
||||
suggestions.push(new SnippetCompletion(snippet, Range.fromPositions(position.delta(0, -(linePrefixLow.length - start)), position)));
|
||||
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 => {
|
||||
suggestions.push(new SnippetCompletion(snippet, Range.fromPositions(position)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 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 = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[to].label, suggestions[to].snippet.name);
|
||||
}
|
||||
if (to > i + 1) {
|
||||
suggestions[i].label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[i].label, suggestions[i].snippet.name);
|
||||
i = to;
|
||||
}
|
||||
}
|
||||
return { suggestions };
|
||||
});
|
||||
}
|
||||
|
||||
resolveCompletionItem?(model: ITextModel, position: Position, 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,102 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } 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 {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
getSnippetFiles(): Promise<SnippetFile[]>;
|
||||
|
||||
getSnippets(languageId: LanguageId): Promise<Snippet[]>;
|
||||
|
||||
getSnippetsSync(languageId: LanguageId): Snippet[];
|
||||
}
|
||||
|
||||
const languageScopeSchemaId = 'vscode://schemas/snippets';
|
||||
const languageScopeSchema: IJSONSchema = {
|
||||
id: languageScopeSchemaId,
|
||||
allowComments: 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: {
|
||||
prefix: {
|
||||
description: nls.localize('snippetSchema.json.prefix', 'The prefix to used when selecting the snippet in intellisense'),
|
||||
type: ['string', 'array']
|
||||
},
|
||||
body: {
|
||||
description: 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']
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const globalSchemaId = 'vscode://schemas/global-snippets';
|
||||
const globalSchema: IJSONSchema = {
|
||||
id: globalSchemaId,
|
||||
allowComments: 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: {
|
||||
prefix: {
|
||||
description: nls.localize('snippetSchema.json.prefix', 'The prefix to used when selecting the snippet in intellisense'),
|
||||
type: ['string', 'array']
|
||||
},
|
||||
scope: {
|
||||
description: nls.localize('snippetSchema.json.scope', "A list of language names to which this snippet applies, e.g 'typescript,javascript'."),
|
||||
type: 'string'
|
||||
},
|
||||
body: {
|
||||
description: 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'
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
};
|
||||
|
||||
const reg = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);
|
||||
reg.registerSchema(languageScopeSchemaId, languageScopeSchema);
|
||||
reg.registerSchema(globalSchemaId, globalSchema);
|
||||
282
src/vs/workbench/contrib/snippets/browser/snippetsFile.ts
Normal file
282
src/vs/workbench/contrib/snippets/browser/snippetsFile.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { parse as jsonParse } 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';
|
||||
|
||||
export class Snippet {
|
||||
|
||||
private _codeSnippet: string;
|
||||
private _isBogous: boolean;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
get codeSnippet(): string {
|
||||
this._ensureCodeSnippet();
|
||||
return this._codeSnippet;
|
||||
}
|
||||
|
||||
get isBogous(): boolean {
|
||||
this._ensureCodeSnippet();
|
||||
return this._isBogous;
|
||||
}
|
||||
|
||||
private _ensureCodeSnippet() {
|
||||
if (!this._codeSnippet) {
|
||||
const rewrite = Snippet._rewriteBogousVariables(this.body);
|
||||
if (typeof rewrite === 'string') {
|
||||
this._codeSnippet = rewrite;
|
||||
this._isBogous = true;
|
||||
} else {
|
||||
this._codeSnippet = this.body;
|
||||
this._isBogous = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
static _rewriteBogousVariables(template: string): false | string {
|
||||
const textmateSnippet = new SnippetParser().parse(template, false);
|
||||
|
||||
let placeholders = new Map<string, number>();
|
||||
let placeholderMax = 0;
|
||||
for (const placeholder of textmateSnippet.placeholders) {
|
||||
placeholderMax = Math.max(placeholderMax, placeholder.index);
|
||||
}
|
||||
|
||||
let didChange = false;
|
||||
let stack = [...textmateSnippet.children];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const marker = stack.shift()!;
|
||||
|
||||
if (
|
||||
marker instanceof Variable
|
||||
&& 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]);
|
||||
didChange = true;
|
||||
|
||||
} else {
|
||||
// recurse
|
||||
stack.push(...marker.children);
|
||||
}
|
||||
}
|
||||
|
||||
if (!didChange) {
|
||||
return false;
|
||||
} else {
|
||||
return textmateSnippet.toTextmateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
load(): Promise<this> {
|
||||
if (!this._loadPromise) {
|
||||
this._loadPromise = Promise.resolve(this._fileService.resolveContent(this.location, { encoding: 'utf8' })).then(content => {
|
||||
const data = <JsonSerializedSnippets>jsonParse(content.value.toString());
|
||||
if (typeof 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
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
365
src/vs/workbench/contrib/snippets/browser/snippetsService.ts
Normal file
365
src/vs/workbench/contrib/snippets/browser/snippetsService.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { combinedDisposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { endsWith, 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/platform/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';
|
||||
|
||||
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) && !endsWith(snippet.path, '.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: (type: FileChangeType, resource: URI) => any): IDisposable {
|
||||
let listener = service.onFileChanges(e => {
|
||||
for (const change of e.changes) {
|
||||
if (resources.isEqualOrParent(change.resource, resource)) {
|
||||
callback(change.type, change.resource);
|
||||
}
|
||||
}
|
||||
});
|
||||
service.watchFileChanges(resource);
|
||||
return {
|
||||
dispose() {
|
||||
listener.dispose();
|
||||
service.unwatchFileChanges(resource);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class SnippetsService implements ISnippetsService {
|
||||
|
||||
readonly _serviceBrand: any;
|
||||
|
||||
private readonly _disposables: IDisposable[] = [];
|
||||
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,
|
||||
@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 {
|
||||
dispose(this._disposables);
|
||||
}
|
||||
|
||||
private _joinSnippets(): Promise<any> {
|
||||
const promises = this._pendingWork.slice(0);
|
||||
this._pendingWork.length = 0;
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
getSnippetFiles(): Promise<SnippetFile[]> {
|
||||
return this._joinSnippets().then(() => values(this._files));
|
||||
}
|
||||
|
||||
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;
|
||||
this._files.forEach(file => {
|
||||
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;
|
||||
this._files.forEach(file => {
|
||||
// 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 => {
|
||||
|
||||
this._files.forEach((value, key) => {
|
||||
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._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: IDisposable[] = [];
|
||||
let updateWorkspaceSnippets = () => {
|
||||
disposables = dispose(disposables);
|
||||
this._pendingWork.push(this._initWorkspaceFolderSnippets(this._contextService.getWorkspace(), disposables));
|
||||
};
|
||||
this._disposables.push({
|
||||
dispose() { dispose(disposables); }
|
||||
});
|
||||
this._disposables.push(this._contextService.onDidChangeWorkspaceFolders(updateWorkspaceSnippets));
|
||||
this._disposables.push(this._contextService.onDidChangeWorkbenchState(updateWorkspaceSnippets));
|
||||
updateWorkspaceSnippets();
|
||||
}
|
||||
|
||||
private _initWorkspaceFolderSnippets(workspace: IWorkspace, bucket: IDisposable[]): Promise<any> {
|
||||
let promises = workspace.folders.map(folder => {
|
||||
const snippetFolder = folder.toResource('.vscode');
|
||||
return this._fileService.existsFile(snippetFolder).then(value => {
|
||||
if (value) {
|
||||
this._initFolderSnippets(SnippetSource.Workspace, snippetFolder, bucket);
|
||||
} else {
|
||||
// watch
|
||||
bucket.push(watch(this._fileService, snippetFolder, (type) => {
|
||||
if (type === FileChangeType.ADDED) {
|
||||
this._initFolderSnippets(SnippetSource.Workspace, snippetFolder, bucket);
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
private _initUserSnippets(): Promise<any> {
|
||||
const userSnippetsFolder = URI.file(join(this._environmentService.appSettingsHome, 'snippets'));
|
||||
return this._fileService.createFolder(userSnippetsFolder).then(() => this._initFolderSnippets(SnippetSource.User, userSnippetsFolder, this._disposables));
|
||||
}
|
||||
|
||||
private _initFolderSnippets(source: SnippetSource, folder: URI, bucket: IDisposable[]): Promise<any> {
|
||||
let disposables: IDisposable[] = [];
|
||||
let addFolderSnippets = (type?: FileChangeType) => {
|
||||
disposables = dispose(disposables);
|
||||
if (type === FileChangeType.DELETED) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._fileService.resolveFile(folder).then(stat => {
|
||||
for (const entry of stat.children || []) {
|
||||
disposables.push(this._addSnippetFile(entry.resource, source));
|
||||
}
|
||||
}, err => {
|
||||
this._logService.error(`Failed snippets from folder '${folder.toString()}'`, err);
|
||||
});
|
||||
};
|
||||
|
||||
bucket.push(watch(this._fileService, folder, addFolderSnippets));
|
||||
bucket.push(combinedDisposable(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));
|
||||
} else if (ext === '.code-snippets') {
|
||||
this._files.set(key, new SnippetFile(source, uri, undefined, undefined, this._fileService));
|
||||
}
|
||||
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 '';
|
||||
}
|
||||
162
src/vs/workbench/contrib/snippets/browser/tabCompletion.ts
Normal file
162
src/vs/workbench/contrib/snippets/browser/tabCompletion.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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 { endsWith } from 'vs/base/common/strings';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import * as editorCommon 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';
|
||||
|
||||
export class TabCompletionController implements editorCommon.IEditorContribution {
|
||||
|
||||
private static readonly ID = 'editor.tabCompletionController';
|
||||
static 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 _configListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@ISnippetsService private readonly _snippetService: ISnippetsService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
) {
|
||||
this._hasSnippets = TabCompletionController.ContextKey.bindTo(contextKeyService);
|
||||
this._configListener = this._editor.onDidChangeConfiguration(e => {
|
||||
if (e.contribInfo) {
|
||||
this._update();
|
||||
}
|
||||
});
|
||||
this._update();
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return TabCompletionController.ID;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._configListener);
|
||||
dispose(this._selectionListener);
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
const enabled = this._editor.getConfiguration().contribInfo.tabCompletion === 'onlySnippets';
|
||||
if (this._enabled !== enabled) {
|
||||
this._enabled = enabled;
|
||||
if (!this._enabled) {
|
||||
dispose(this._selectionListener);
|
||||
} 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 (endsWith(prefix, 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);
|
||||
}
|
||||
|
||||
performSnippetCompletions(): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._activeSnippets.length === 1) {
|
||||
// one -> just insert
|
||||
const [snippet] = this._activeSnippets;
|
||||
SnippetController2.get(this._editor).insert(snippet.codeSnippet, snippet.prefix.length, 0);
|
||||
|
||||
} 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);
|
||||
|
||||
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,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
|
||||
suite('Snippets', function () {
|
||||
|
||||
class TestSnippetFile extends SnippetFile {
|
||||
constructor(filepath: URI, snippets: Snippet[]) {
|
||||
super(SnippetSource.Extension, filepath, 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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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 = Snippet._rewriteBogousVariables(input);
|
||||
assert.equal(actual, 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,412 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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 { TextModel } from 'vs/editor/common/model/textModel';
|
||||
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 {
|
||||
_serviceBrand: any;
|
||||
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 = TextModel.createFromString('', 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 = TextModel.createFromString('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.equal(result.suggestions[0].label, 'bar');
|
||||
assert.equal(result.suggestions[0].range.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 = TextModel.createFromString('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.equal(result.suggestions[0].label, 'bar');
|
||||
assert.equal(result.suggestions[0].insertText, 's1');
|
||||
assert.equal(result.suggestions[0].range.startColumn, 1);
|
||||
assert.equal(result.suggestions[1].label, 'bar-bar');
|
||||
assert.equal(result.suggestions[1].insertText, 's2');
|
||||
assert.equal(result.suggestions[1].range.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.equal(result.suggestions[0].label, 'bar-bar');
|
||||
assert.equal(result.suggestions[0].insertText, 's2');
|
||||
assert.equal(result.suggestions[0].range.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.equal(result.suggestions[0].label, 'bar');
|
||||
assert.equal(result.suggestions[0].insertText, 's1');
|
||||
assert.equal(result.suggestions[0].range.startColumn, 5);
|
||||
assert.equal(result.suggestions[1].label, 'bar-bar');
|
||||
assert.equal(result.suggestions[1].insertText, 's2');
|
||||
assert.equal(result.suggestions[1].range.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 = TextModel.createFromString('\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 = TextModel.createFromString('\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.startColumn, 2);
|
||||
model.dispose();
|
||||
|
||||
model = TextModel.createFromString('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.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 = TextModel.createFromString('<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 = TextModel.createFromString('', 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.equal(first.label, 'first');
|
||||
assert.equal(second.label, '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 = TextModel.createFromString('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 = TextModel.createFromString('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 = TextModel.createFromString(':', 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 = TextModel.createFromString('template', undefined, modeService.getLanguageIdentifier('fooLang'));
|
||||
let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!;
|
||||
|
||||
assert.equal(result.suggestions.length, 1);
|
||||
assert.equal(result.suggestions[0].label, '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 = TextModel.createFromString('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 = TextModel.createFromString('.🐷-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 = TextModel.createFromString('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 = TextModel.createFromString(' <', 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.startColumn, 2);
|
||||
|
||||
model = TextModel.createFromString('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.startColumn, 1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user