提交 790e61bf 编写于 作者: J Johannes Rieken

load user and extension snippets only when needed

上级 fe38e9bc
......@@ -219,11 +219,19 @@ suite('MenuService', function () {
MenuRegistry.addCommand({ id: 'b', title: 'Implicit' });
const [first, second] = MenuRegistry.getMenuItems(MenuId.CommandPalette);
assert.equal(first.command.id, 'a');
assert.equal(first.command.title, 'Explicit');
assert.equal(second.command.id, 'b');
assert.equal(second.command.title, 'Implicit');
let foundA = false;
let foundB = false;
for (const item of MenuRegistry.getMenuItems(MenuId.CommandPalette)) {
if (item.command.id === 'a') {
assert.equal(item.command.title, 'Explicit');
foundA = true;
}
if (item.command.id === 'b') {
assert.equal(item.command.title, 'Implicit');
foundB = true;
}
}
assert.equal(foundA, true);
assert.equal(foundB, true);
});
});
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { parse } from 'vs/base/common/json';
import { join } from 'path';
import { TPromise } from 'vs/base/common/winjs.base';
import { readFile } from 'vs/base/node/pfs';
import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
import { ISnippetsService, ISnippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService';
import { LanguageIdentifier } from 'vs/editor/common/modes';
import { SnippetParser, Placeholder, Variable, Text } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { EditorSnippetVariableResolver } from 'vs/editor/contrib/snippet/browser/snippetVariables';
interface ISnippetsExtensionPoint {
language: string;
path: string;
}
let snippetsExtensionPoint = ExtensionsRegistry.registerExtensionPoint<ISnippetsExtensionPoint[]>('snippets', [languagesExtPoint], {
description: nls.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: nls.localize('vscode.extension.contributes.snippets-language', 'Language identifier for which this snippet is contributed to.'),
type: 'string'
},
path: {
description: nls.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 class MainProcessTextMateSnippet implements IWorkbenchContribution {
constructor(
@IModeService private _modeService: IModeService,
@ISnippetsService private _snippetService: ISnippetsService
) {
snippetsExtensionPoint.setHandler((extensions) => {
for (let i = 0; i < extensions.length; i++) {
let tmSnippets = extensions[i].value;
for (let j = 0; j < tmSnippets.length; j++) {
this._withSnippetContribution(extensions[i].description.name, extensions[i].description.extensionFolderPath, tmSnippets[j], extensions[i].collector);
}
}
});
}
getId() {
return 'tmSnippetExtension';
}
private _withSnippetContribution(extensionName: string, extensionFolderPath: string, snippet: ISnippetsExtensionPoint, collector: ExtensionMessageCollector): void {
if (!snippet.language || (typeof snippet.language !== 'string') || !this._modeService.isRegisteredMode(snippet.language)) {
collector.error(nls.localize('invalid.language', "Unknown language in `contributes.{0}.language`. Provided value: {1}", snippetsExtensionPoint.name, String(snippet.language)));
return;
}
if (!snippet.path || (typeof snippet.path !== 'string')) {
collector.error(nls.localize('invalid.path.0', "Expected string in `contributes.{0}.path`. Provided value: {1}", snippetsExtensionPoint.name, String(snippet.path)));
return;
}
let normalizedAbsolutePath = join(extensionFolderPath, snippet.path);
if (normalizedAbsolutePath.indexOf(extensionFolderPath) !== 0) {
collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", snippetsExtensionPoint.name, normalizedAbsolutePath, extensionFolderPath));
}
let modeId = snippet.language;
let languageIdentifier = this._modeService.getLanguageIdentifier(modeId);
if (languageIdentifier) {
readAndRegisterSnippets(this._snippetService, languageIdentifier, normalizedAbsolutePath, extensionName, collector);
}
}
}
export function readAndRegisterSnippets(
snippetService: ISnippetsService, languageIdentifier: LanguageIdentifier, filePath: string,
extensionName?: string, collector?: ExtensionMessageCollector
): TPromise<void> {
return readFile(filePath).then(fileContents => {
let snippets = parseSnippetFile(fileContents.toString(), extensionName, collector);
snippetService.registerSnippets(languageIdentifier.id, snippets, filePath);
}, err => {
if (err && err.code === 'ENOENT') {
snippetService.registerSnippets(languageIdentifier.id, [], filePath);
} else {
throw err;
}
});
}
function parseSnippetFile(snippetFileContent: string, extensionName?: string, collector?: ExtensionMessageCollector): ISnippet[] {
let snippetsObj = parse(snippetFileContent);
if (!snippetsObj || typeof snippetsObj !== 'object') {
return [];
}
let topLevelProperties = Object.keys(snippetsObj);
let result: ISnippet[] = [];
let processSnippet = (snippet: any, name: string) => {
let prefix = snippet['prefix'];
let body = <string | string[]>snippet['body'];
if (Array.isArray(body)) {
body = body.join('\n');
}
if (typeof prefix !== 'string' || typeof body !== 'string') {
return;
}
snippet = {
name,
extensionName,
prefix,
description: snippet['description'] || name,
codeSnippet: body
};
const didRewrite = _rewriteBogousVariables(snippet);
if (didRewrite && collector) {
collector.warn(nls.localize(
'badVariableUse',
"The \"{0}\"-snippet very likely confuses snippet-variables and snippet-placeholders. See https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax for more details.",
name
));
}
result.push(snippet);
};
topLevelProperties.forEach(topLevelProperty => {
let scopeOrTemplate = snippetsObj[topLevelProperty];
if (scopeOrTemplate['body'] && scopeOrTemplate['prefix']) {
processSnippet(scopeOrTemplate, topLevelProperty);
} else {
let snippetNames = Object.keys(scopeOrTemplate);
snippetNames.forEach(name => {
processSnippet(scopeOrTemplate[name], name);
});
}
});
return result;
}
export function _rewriteBogousVariables(snippet: ISnippet): boolean {
const textmateSnippet = new SnippetParser().parse(snippet.codeSnippet, 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) {
let marker = stack.shift();
if (
marker instanceof Variable
&& marker.children.length === 0
&& !EditorSnippetVariableResolver.VariableNames[marker.name]
) {
// a 'variable' without a default value and not being one of our supported
// variables is automatically turing 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);
}
}
snippet.codeSnippet = textmateSnippet.toTextmateString();
return didChange;
}
......@@ -12,7 +12,7 @@ import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/
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, ISnippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { ISnippetsService, ISnippet } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
......@@ -75,14 +75,14 @@ class InsertSnippetAction extends EditorAction {
const { lineNumber, column } = editor.getPosition();
let { snippet, name, langId } = Args.fromUser(arg);
return new TPromise<ISnippet>((resolve, reject) => {
return new TPromise<ISnippet>(async (resolve, reject) => {
if (snippet) {
return resolve({
codeSnippet: snippet,
description: undefined,
name: undefined,
extensionName: undefined,
source: undefined,
prefix: undefined
});
}
......@@ -105,7 +105,7 @@ class InsertSnippetAction extends EditorAction {
if (name) {
// take selected snippet
snippetService.visitSnippets(languageId, snippet => {
(await snippetService.getSnippets(languageId)).every(snippet => {
if (snippet.name !== name) {
return true;
}
......@@ -114,14 +114,12 @@ class InsertSnippetAction extends EditorAction {
});
} else {
// let user pick a snippet
const picks: ISnippetPick[] = [];
snippetService.visitSnippets(languageId, snippet => {
picks.push({
const picks: ISnippetPick[] = (await snippetService.getSnippets(languageId)).map(snippet => {
return {
label: snippet.prefix,
detail: snippet.description,
snippet
});
return true;
};
});
return quickOpenService.pick(picks, { matchOnDetail: true }).then(pick => resolve(pick && pick.snippet), reject);
}
......
......@@ -3,11 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import 'vs/workbench/parts/snippets/electron-browser/insertSnippet';
import 'vs/workbench/parts/snippets/electron-browser/tabCompletion';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { fileExists, writeFile } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
......@@ -21,10 +16,31 @@ import { Registry } from 'vs/platform/registry/common/platform';
import * as errors from 'vs/base/common/errors';
import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import * as nls from 'vs/nls';
import * as snippetsTracker from './snippetsTracker';
import * as tmSnippets from './TMSnippets';
import * as winjs from 'vs/base/common/winjs.base';
import * as workbenchContributions from 'vs/workbench/common/contributions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { LanguageId } from 'vs/editor/common/modes';
import { TPromise } from 'vs/base/common/winjs.base';
export const ISnippetsService = createDecorator<ISnippetsService>('snippetService');
export interface ISnippetsService {
_serviceBrand: any;
getSnippets(languageId: LanguageId): TPromise<ISnippet[]>;
getSnippetsSync(languageId: LanguageId): ISnippet[];
}
export interface ISnippet {
readonly name: string;
readonly prefix: string;
readonly description: string;
readonly codeSnippet: string;
readonly source: string;
readonly isBogous?: boolean;
readonly isFromExtension?: boolean;
}
namespace OpenSnippetsAction {
......@@ -37,7 +53,7 @@ namespace OpenSnippetsAction {
const environmentService = accessor.get(IEnvironmentService);
const windowsService = accessor.get(IWindowsService);
function openFile(filePath: string): winjs.TPromise<void> {
function openFile(filePath: string): TPromise<void> {
return windowsService.openWindow([filePath], { forceReuseWindow: true });
}
......@@ -86,7 +102,7 @@ namespace OpenSnippetsAction {
});
});
}
return winjs.TPromise.as(null);
return TPromise.as(null);
});
});
......@@ -136,11 +152,3 @@ const schema: IJSONSchema = {
Registry
.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution)
.registerSchema(schemaId, schema);
Registry
.as<workbenchContributions.IWorkbenchContributionsRegistry>(workbenchContributions.Extensions.Workbench)
.registerWorkbenchContribution(snippetsTracker.SnippetsTracker);
Registry
.as<workbenchContributions.IWorkbenchContributionsRegistry>(workbenchContributions.Extensions.Workbench)
.registerWorkbenchContribution(tmSnippets.MainProcessTextMateSnippet);
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { readFile } from 'vs/base/node/pfs';
import { parse as jsonParse } from 'vs/base/common/json';
import { TPromise } from 'vs/base/common/winjs.base';
import { SnippetParser, Variable, Placeholder, Text } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { EditorSnippetVariableResolver } from 'vs/editor/contrib/snippet/browser/snippetVariables';
import { forEach } from 'vs/base/common/collections';
import { ISnippet } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
interface JsonSerializedSnippet {
body: string;
prefix: string | string[];
description: string;
}
function isJsonSerilziedSnippet(thing: any): thing is JsonSerializedSnippet {
return Boolean((<JsonSerializedSnippet>thing).body) && Boolean((<JsonSerializedSnippet>thing).prefix);
}
interface JsonSerializedSnippets {
[name: string]: JsonSerializedSnippet | { [name: string]: JsonSerializedSnippet };
}
export class SnippetFile {
private constructor(
readonly filepath: string,
readonly data: ISnippet[]
) {
//
}
static fromFile(filepath: string, source: string, isFromExtension?: boolean): TPromise<SnippetFile> {
return readFile(filepath).then(value => {
const data = <JsonSerializedSnippets>jsonParse(value.toString());
const snippets: ISnippet[] = [];
if (typeof data === 'object') {
forEach(data, entry => {
const { key: name, value: scopeOrTemplate } = entry;
if (isJsonSerilziedSnippet(scopeOrTemplate)) {
SnippetFile._parseSnippet(name, scopeOrTemplate, source, isFromExtension, snippets);
} else {
forEach(scopeOrTemplate, entry => {
const { key: name, value: template } = entry;
SnippetFile._parseSnippet(name, template, source, isFromExtension, snippets);
});
}
});
}
return new SnippetFile(filepath, snippets);
});
}
private static _parseSnippet(name: string, snippet: JsonSerializedSnippet, source: string, isFromExtension: boolean, bucket: ISnippet[]): void {
let { prefix, body, description } = snippet;
if (Array.isArray(body)) {
body = body.join('\n');
}
if (typeof prefix !== 'string' || typeof body !== 'string') {
return;
}
let rewrite = SnippetFile._rewriteBogousVariables(body);
let isBogous = false;
if (typeof rewrite === 'string') {
body = rewrite;
isBogous = true;
}
bucket.push({
codeSnippet: body,
name,
prefix,
description,
source,
isFromExtension,
isBogous
});
}
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) {
let marker = stack.shift();
if (
marker instanceof Variable
&& marker.children.length === 0
&& !EditorSnippetVariableResolver.VariableNames[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();
}
}
}
......@@ -7,75 +7,226 @@
import { localize } from 'vs/nls';
import { IModel } from 'vs/editor/common/editorCommon';
import { ISuggestSupport, ISuggestResult, ISuggestion, LanguageId, SuggestionType, SnippetType } from 'vs/editor/common/modes';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/browser/suggest';
import { IModeService } from 'vs/editor/common/services/modeService';
import { Position } from 'vs/editor/common/core/position';
import { overlap, compare, startsWith } from 'vs/base/common/strings';
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { join } from 'path';
import { mkdirp } from 'vs/base/node/pfs';
import { watch } from 'fs';
import { SnippetFile } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile';
import { TPromise } from 'vs/base/common/winjs.base';
import { ISnippet, ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/platform/extensions/common/extensionsRegistry';
import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService';
namespace schema {
export interface ISnippetsExtensionPoint {
language: string;
path: string;
}
export const ISnippetsService = createDecorator<ISnippetsService>('snippetService');
export function isValidSnippet(extension: IExtensionPointUser<ISnippetsExtensionPoint[]>, snippet: ISnippetsExtensionPoint, modeService: IModeService): boolean {
if (!snippet.language || (typeof snippet.language !== 'string') || !modeService.isRegisteredMode(snippet.language)) {
extension.collector.error(localize('invalid.language', "Unknown language in `contributes.{0}.language`. Provided value: {0}", String(snippet.language)));
return false;
} else if (!snippet.path || (typeof snippet.path !== 'string')) {
extension.collector.error(localize('invalid.path.0', "Expected string in `contributes.{0}.path`. Provided value: {0}", String(snippet.path)));
return false;
} else {
const normalizedAbsolutePath = join(extension.description.extensionFolderPath, snippet.path);
if (normalizedAbsolutePath.indexOf(extension.description.extensionFolderPath) !== 0) {
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, normalizedAbsolutePath, extension.description.extensionFolderPath
));
return false;
}
export interface ISnippetsService {
snippet.path = normalizedAbsolutePath;
return true;
}
}
_serviceBrand: any;
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'
}
}
}
};
}
registerSnippets(languageId: LanguageId, snippets: ISnippet[], owner: string): void;
class SnippetsService implements ISnippetsService {
visitSnippets(languageId: LanguageId, accept: (snippet: ISnippet) => boolean): void;
readonly _serviceBrand: any;
getSnippets(languageId: LanguageId): ISnippet[];
}
private readonly _pendingExtensionSnippets = new Map<LanguageId, [IExtensionPointUser<any>, string][]>();
private readonly _extensionSnippets = new Map<LanguageId, ISnippet[]>();
private readonly _userSnippets = new Map<LanguageId, ISnippet[]>();
private readonly _userSnippetsFolder: string;
private readonly _disposables: IDisposable[] = [];
export interface ISnippet {
name: string;
prefix: string;
description: string;
codeSnippet: string;
extensionName?: string;
}
constructor(
@IModeService readonly _modeService: IModeService,
@IExtensionService readonly _extensionService: IExtensionService,
@IEnvironmentService environmentService: IEnvironmentService,
) {
this._userSnippetsFolder = join(environmentService.appSettingsHome, 'snippets');
this._prepUserSnippetsWatching();
this._prepExtensionSnippets();
export class SnippetsService implements ISnippetsService {
setSnippetSuggestSupport(new SnippetSuggestProvider(this._modeService, this));
}
_serviceBrand: any;
dispose(): void {
dispose(this._disposables);
}
private readonly _snippets = new Map<LanguageId, Map<string, ISnippet[]>>();
async getSnippets(languageId: LanguageId): TPromise<ISnippet[]> {
let result: ISnippet[] = [];
await TPromise.join([
this._extensionService.onReady(),
this._getOrLoadUserSnippets(languageId, result),
this._getOrLoadExtensionSnippets(languageId, result)
]);
return result;
}
constructor(
@IModeService modeService: IModeService
) {
setSnippetSuggestSupport(new SnippetSuggestProvider(modeService, this));
getSnippetsSync(languageId: LanguageId): ISnippet[] {
// just kick off snippet loading for this language such
// that subseqent calls to this method return more
// correct results
this.getSnippets(languageId).done(undefined, undefined);
// collect and return what we already have
let userSnippets = this._userSnippets.get(languageId);
let extensionSnippets = this._extensionSnippets.get(languageId);
return (userSnippets || []).concat(extensionSnippets || []);
}
registerSnippets(languageId: LanguageId, snippets: ISnippet[], fileName: string): void {
if (!this._snippets.has(languageId)) {
this._snippets.set(languageId, new Map<string, ISnippet[]>());
}
this._snippets.get(languageId).set(fileName, snippets);
// --- extension snippet logic ---
private async _prepExtensionSnippets(): TPromise<void> {
ExtensionsRegistry.registerExtensionPoint<schema.ISnippetsExtensionPoint[]>('snippets', [languagesExtPoint], schema.snippetsContribution).setHandler(async extensions => {
for (const extension of extensions) {
for (const contribution of extension.value) {
if (schema.isValidSnippet(extension, contribution, this._modeService)) {
const { id } = this._modeService.getLanguageIdentifier(contribution.language);
const array = this._pendingExtensionSnippets.get(id);
if (!array) {
this._pendingExtensionSnippets.set(id, [[extension, contribution.path]]);
} else {
array.push([extension, contribution.path]);
}
}
}
}
});
}
visitSnippets(languageId: LanguageId, accept: (snippet: ISnippet) => boolean): void {
const modeSnippets = this._snippets.get(languageId);
if (modeSnippets) {
modeSnippets.forEach(snippets => {
let result = snippets.every(accept);
if (!result) {
return;
private async _getOrLoadExtensionSnippets(languageId: LanguageId, bucket: ISnippet[]): TPromise<void> {
if (this._extensionSnippets.has(languageId)) {
bucket.push(...this._extensionSnippets.get(languageId));
} else if (this._pendingExtensionSnippets.has(languageId)) {
const pending = this._pendingExtensionSnippets.get(languageId);
this._pendingExtensionSnippets.delete(languageId);
const snippets = [];
this._extensionSnippets.set(languageId, snippets);
for (const [extension, filepath] of pending) {
let file: SnippetFile;
try {
file = await SnippetFile.fromFile(filepath, extension.description.displayName || extension.description.name, true);
} catch (e) {
extension.collector.warn(localize(
'badFile',
"The snippet file \"{0}\" could not be read.",
filepath
));
}
});
if (file) {
for (const snippet of file.data) {
snippets.push(snippet);
bucket.push(snippet);
if (snippet.isBogous) {
extension.collector.warn(localize(
'badVariableUse',
"The \"{0}\"-snippet very likely confuses snippet-variables and snippet-placeholders. See https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax for more details.",
snippet.name
));
}
}
}
}
}
}
getSnippets(languageId: LanguageId): ISnippet[] {
const modeSnippets = this._snippets.get(languageId);
const ret: ISnippet[] = [];
if (modeSnippets) {
modeSnippets.forEach(snippets => {
ret.push(...snippets);
});
// --- user snippet logic ---
private async _getOrLoadUserSnippets(languageId: LanguageId, bucket: ISnippet[]): TPromise<void> {
let snippets = this._userSnippets.get(languageId);
if (snippets === undefined) {
try {
snippets = (await SnippetFile.fromFile(this._getUserSnippetFilepath(languageId), localize('source.snippet', "User Snippet"))).data;
} catch (e) {
snippets = null;
}
this._userSnippets.set(languageId, snippets);
}
return ret;
if (snippets) {
bucket.push(...snippets);
}
}
private _getUserSnippetFilepath(languageId: LanguageId): string {
const { language } = this._modeService.getLanguageIdentifier(languageId);
const filepath = join(this._userSnippetsFolder, `${language}.json`);
return filepath;
}
private _prepUserSnippetsWatching(): void {
// Install a FS watcher on the snippet directory and when an
// event occurs delete any cached snippet information
mkdirp(this._userSnippetsFolder).then(() => {
const watcher = watch(this._userSnippetsFolder);
this._disposables.push({ dispose: () => watcher.close() });
watcher.on('change', (type, filename) => {
if (typeof filename === 'string') {
const language = filename.replace(/\.json$/, '').toLowerCase();
const languageId = this._modeService.getLanguageIdentifier(language);
if (languageId) {
this._userSnippets.delete(languageId.id);
}
}
});
});
}
}
......@@ -87,8 +238,6 @@ export interface ISimpleModel {
export class SnippetSuggestion implements ISuggestion {
private static _userSnippet = localize('source.snippet', "User Snippet");
label: string;
detail: string;
insertText: string;
......@@ -99,16 +248,15 @@ export class SnippetSuggestion implements ISuggestion {
type: SuggestionType;
snippetType: SnippetType;
constructor(
readonly snippet: ISnippet,
overwriteBefore: number
) {
this.label = snippet.prefix;
this.detail = localize('detail.snippet', "{0} ({1})", snippet.description, snippet.extensionName || SnippetSuggestion._userSnippet);
this.detail = localize('detail.snippet', "{0} ({1})", snippet.description, snippet.source);
this.insertText = snippet.codeSnippet;
this.overwriteBefore = overwriteBefore;
this.sortText = `${snippet.prefix}-${snippet.extensionName || ''}`;
this.sortText = `${snippet.isFromExtension ? 'z' : 'a'}-${snippet.prefix}`;
this.noAutoAccept = true;
this.type = 'snippet';
this.snippetType = 'textmate';
......@@ -134,10 +282,10 @@ export class SnippetSuggestProvider implements ISuggestSupport {
//
}
provideCompletionItems(model: IModel, position: Position): ISuggestResult {
async provideCompletionItems(model: IModel, position: Position): TPromise<ISuggestResult> {
const languageId = this._getLanguageIdAtPosition(model, position);
const snippets = this._snippets.getSnippets(languageId);
const snippets = await this._snippets.getSnippets(languageId);
const suggestions: SnippetSuggestion[] = [];
const lowWordUntil = model.getWordUntilPosition(position).word.toLowerCase();
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { join } from 'path';
import { mkdirp, fileExists } from 'vs/base/node/pfs';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { readAndRegisterSnippets } from './TMSnippets';
import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { watch } from 'fs';
import { IModeService } from 'vs/editor/common/services/modeService';
export class SnippetsTracker implements IWorkbenchContribution {
private readonly _snippetFolder: string;
private readonly _toDispose: IDisposable[];
constructor(
@IModeService modeService: IModeService,
@ISnippetsService snippetService: ISnippetsService,
@IEnvironmentService environmentService: IEnvironmentService,
@IExtensionService extensionService: IExtensionService
) {
this._snippetFolder = join(environmentService.appSettingsHome, 'snippets');
this._toDispose = [];
// Whenever a mode is being created check if a snippet file exists
// and iff so read all snippets from it.
this._toDispose.push(modeService.onDidCreateMode(mode => {
const snippetPath = join(this._snippetFolder, `${mode.getId()}.json`);
fileExists(snippetPath)
.then(exists => exists && readAndRegisterSnippets(snippetService, mode.getLanguageIdentifier(), snippetPath))
.done(undefined, onUnexpectedError);
}));
// Install a FS watcher on the snippet directory and when an
// event occurs update the snippets for that one snippet.
mkdirp(this._snippetFolder).then(() => {
const watcher = watch(this._snippetFolder);
this._toDispose.push({ dispose: () => watcher.close() });
watcher.on('change', (type, filename) => {
if (typeof filename !== 'string') {
return;
}
extensionService.onReady().then(() => {
const langName = filename.replace(/\.json$/, '').toLowerCase();
const langId = modeService.getLanguageIdentifier(langName);
return langId && readAndRegisterSnippets(snippetService, langId, join(this._snippetFolder, filename));
}, onUnexpectedError);
});
});
}
getId(): string {
return 'vs.snippets.snippetsTracker';
}
dispose(): void {
dispose(this._toDispose);
}
}
......@@ -9,7 +9,8 @@ import { localize } from 'vs/nls';
import { KeyCode } from 'vs/base/common/keyCodes';
import { RawContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ISnippetsService, getNonWhitespacePrefix, ISnippet, SnippetSuggestion } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { ISnippetsService, ISnippet } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import { getNonWhitespacePrefix, SnippetSuggestion } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { Registry } from 'vs/platform/registry/common/platform';
import { endsWith } from 'vs/base/common/strings';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
......@@ -33,7 +34,7 @@ export class TabCompletionController implements editorCommon.IEditorContribution
private readonly _editor: editorCommon.ICommonCodeEditor;
private readonly _snippetController: SnippetController2;
private readonly _dispoables: IDisposable[] = [];
private readonly _snippets: ISnippet[] = [];
private _snippets: ISnippet[] = [];
constructor(
editor: editorCommon.ICommonCodeEditor,
......@@ -61,12 +62,10 @@ export class TabCompletionController implements editorCommon.IEditorContribution
}
if (selectFn) {
snippetService.visitSnippets(editor.getModel().getLanguageIdentifier().id, s => {
if (selectFn(s)) {
this._snippets.push(s);
}
return true;
});
const model = editor.getModel();
model.tokenizeIfCheap(e.selection.positionLineNumber);
const id = model.getLanguageIdAtPosition(e.selection.positionLineNumber, e.selection.positionColumn);
this._snippets = snippetService.getSnippetsSync(id).filter(selectFn);
}
hasSnippets.set(this._snippets.length > 0);
}));
......
......@@ -6,20 +6,19 @@
'use strict';
import * as assert from 'assert';
import { _rewriteBogousVariables } from 'vs/workbench/parts/snippets/electron-browser/TMSnippets';
import { SnippetFile } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile';
suite('TMSnippets', function () {
function assertRewrite(input: string, expected: string): void {
let snippet = { codeSnippet: input, description: undefined, extensionName: undefined, name: undefined, prefix: undefined };
_rewriteBogousVariables(snippet);
assert.equal(snippet.codeSnippet, expected);
function assertRewrite(input: string, expected: string | boolean): void {
const actual = SnippetFile._rewriteBogousVariables(input);
assert.equal(actual, expected);
}
test('bogous variable rewrite', function () {
assertRewrite('foo', 'foo');
assertRewrite('hello $1 world$0', 'hello $1 world$0');
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}');
......@@ -42,6 +41,6 @@ suite('TMSnippets', function () {
});
test('Snippet choices: unable to escape comma and pipe, #31521', function () {
assertRewrite('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(${1|not\\, not, five, 5, 1 23|});');
assertRewrite('console.log(${1|not\\, not, five, 5, 1 23|});', false);
});
});
......@@ -6,11 +6,25 @@
'use strict';
import * as assert from 'assert';
import { SnippetsService, ISnippet, SnippetSuggestProvider } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { SnippetSuggestProvider } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
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 { Model } from 'vs/editor/common/model/model';
import { ISnippetsService, ISnippet } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import { TPromise } from 'vs/base/common/winjs.base';
class SimpleSnippetService implements ISnippetsService {
_serviceBrand: any;
constructor(readonly snippets: ISnippet[]) {
}
getSnippets() {
return TPromise.as(this.getSnippetsSync());
}
getSnippetsSync(): ISnippet[] {
return this.snippets;
}
}
suite('SnippetsService', function () {
......@@ -22,42 +36,43 @@ suite('SnippetsService', function () {
});
let modeService: ModeServiceImpl;
let snippetService: SnippetsService;
let snippetService: ISnippetsService;
setup(function () {
modeService = new ModeServiceImpl();
snippetService = new SnippetsService(modeService);
snippetService.registerSnippets(modeService.getLanguageIdentifier('fooLang').id, <ISnippet[]>[{
snippetService = new SimpleSnippetService([{
prefix: 'bar',
codeSnippet: 'barCodeSnippet',
name: 'barTest',
description: ''
description: '',
source: ''
}, {
prefix: 'bazz',
codeSnippet: 'bazzCodeSnippet',
name: 'bazzTest',
description: ''
}], 'fooFile.json');
description: '',
source: ''
}]);
});
test('snippet completions - simple', function () {
test('snippet completions - simple', async function () {
const provider = new SnippetSuggestProvider(modeService, snippetService);
const model = Model.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang'));
const result = provider.provideCompletionItems(model, new Position(1, 1));
const result = await provider.provideCompletionItems(model, new Position(1, 1));
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 2);
});
test('snippet completions - with prefix', function () {
test('snippet completions - with prefix', async function () {
const provider = new SnippetSuggestProvider(modeService, snippetService);
const model = Model.createFromString('bar', undefined, modeService.getLanguageIdentifier('fooLang'));
const result = provider.provideCompletionItems(model, new Position(1, 4));
const result = await provider.provideCompletionItems(model, new Position(1, 4));
assert.equal(result.incomplete, undefined);
assert.equal(result.suggestions.length, 1);
......@@ -65,48 +80,50 @@ suite('SnippetsService', function () {
assert.equal(result.suggestions[0].insertText, 'barCodeSnippet');
});
test('Cannot use "<?php" as user snippet prefix anymore, #26275', function () {
snippetService.registerSnippets(modeService.getLanguageIdentifier('fooLang').id, <ISnippet[]>[{
test('Cannot use "<?php" as user snippet prefix anymore, #26275', async function () {
snippetService = new SimpleSnippetService([{
prefix: '<?php',
codeSnippet: 'insert me',
name: '',
description: ''
}], 'barFile.json');
description: '',
source: ''
}]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
let model = Model.createFromString('\t<?php', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = provider.provideCompletionItems(model, new Position(1, 7));
let result = await provider.provideCompletionItems(model, new Position(1, 7));
assert.equal(result.suggestions.length, 1);
model.dispose();
model = Model.createFromString('\t<?', undefined, modeService.getLanguageIdentifier('fooLang'));
result = provider.provideCompletionItems(model, new Position(1, 4));
result = await provider.provideCompletionItems(model, new Position(1, 4));
assert.equal(result.suggestions.length, 1);
model.dispose();
model = Model.createFromString('a<?', undefined, modeService.getLanguageIdentifier('fooLang'));
result = provider.provideCompletionItems(model, new Position(1, 4));
result = await provider.provideCompletionItems(model, new Position(1, 4));
assert.equal(result.suggestions.length, 0);
model.dispose();
});
test('No user snippets in suggestions, when inside the code, #30508', function () {
test('No user snippets in suggestions, when inside the code, #30508', async function () {
snippetService.registerSnippets(modeService.getLanguageIdentifier('fooLang').id, <ISnippet[]>[{
snippetService = new SimpleSnippetService([{
prefix: 'foo',
codeSnippet: '<foo>$0</foo>',
name: '',
description: ''
}], 'fooFile.json');
description: '',
source: ''
}]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
let model = Model.createFromString('<head>\n\t\n>/head>', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = provider.provideCompletionItems(model, new Position(1, 1));
let result = await provider.provideCompletionItems(model, new Position(1, 1));
assert.equal(result.suggestions.length, 1);
result = provider.provideCompletionItems(model, new Position(2, 2));
result = await provider.provideCompletionItems(model, new Position(2, 2));
assert.equal(result.suggestions.length, 1);
});
});
......@@ -89,6 +89,9 @@ import 'vs/workbench/parts/codeEditor/codeEditor.contribution';
import 'vs/workbench/parts/execution/electron-browser/execution.contribution';
import 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import 'vs/workbench/parts/snippets/electron-browser/insertSnippet';
import 'vs/workbench/parts/snippets/electron-browser/tabCompletion';
import 'vs/workbench/parts/themes/electron-browser/themes.contribution';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册