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:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View 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

View 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');
});

View File

@@ -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;
}
}

View File

@@ -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);

View 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
));
});
}
}

View 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 '';
}

View 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
}
}));

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});