diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 90e4c4ffdd6..dee1ce7c360 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -314,6 +314,13 @@ export class ShallowCancelThenPromise extends TPromise { } } +/** + * Replacement for `WinJS.Promise.timeout`. + */ +export function timeout(n: number): Promise { + return new Promise(resolve => setTimeout(resolve, n)); +} + /** * Returns a new promise that joins the provided promise. Upon completion of * the provided promise the provided function will always be called. This diff --git a/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts b/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts new file mode 100644 index 00000000000..cf1f0f40bb8 --- /dev/null +++ b/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { fileExists, writeFile, readdir } from 'vs/base/node/pfs'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IQuickOpenService, IPickOpenEntry, ISeparator } from 'vs/platform/quickOpen/common/quickOpen'; +import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; +import { join, basename, dirname } from 'path'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { endsWith } from 'vs/base/common/strings'; +import { timeout } from 'vs/base/common/async'; + +const id = 'workbench.action.openSnippets'; + +class LanguagePick implements IPickOpenEntry { + + static list(modeService: IModeService, envService: IEnvironmentService): LanguagePick[] { + const modes = modeService.getRegisteredModes(); + const result: LanguagePick[] = []; + for (const mode of modes) { + const langLabel = modeService.getLanguageName(mode); + if (langLabel) { + result.push(new LanguagePick(langLabel, mode, join(envService.appSettingsHome, 'snippets', `${mode}.json`))); + } + } + if (result.length > 0) { + result[0].separator = { label: nls.localize('group.lang', "Language Snippets") }; + } + return result.sort(LanguagePick.compare); + } + + label: string; + filepath: string; + langName: string; + + separator?: ISeparator; + + constructor(langLabel: string, langName: string, filepath: string) { + this.label = langLabel; + this.langName = langName; + this.filepath = filepath; + } + + async create(): Promise { + const contents = [ + '{', + '/*', + '\t// Place your snippets for ' + this.langName + ' 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 writeFile(this.filepath, contents); + return this; + } + + static compare(a: LanguagePick, b: LanguagePick): number { + return a.label.localeCompare(b.label); + } +} + +class SnippetFilePick implements IPickOpenEntry { + + static list(envService: IEnvironmentService): Thenable { + const dir = join(envService.appSettingsHome, 'snippets'); + return readdir(dir).then(entries => { + const result: SnippetFilePick[] = []; + for (const filename of entries) { + if (endsWith(filename, '.code-snippets')) { + result.push(new SnippetFilePick(join(dir, filename))); + } + } + if (result.length > 0) { + result[0].separator = { border: true, label: nls.localize('group.global', "Global Snippets") }; + } + return result; + }); + } + + label: string; + filepath: string; + separator?: ISeparator; + + constructor(filepath: string) { + this.label = basename(filepath); + this.filepath = filepath; + } +} + +class NewSnippetFilePick implements IPickOpenEntry { + + readonly label: string = nls.localize('create', "Create Global Snippets File..."); + + constructor( + private _envService: IEnvironmentService, + private _windowService: IWindowService + ) { + // + } + + async create(): Promise { + const defaultPath = join(this._envService.appSettingsHome, 'snippets'); + const path = await this._windowService.showSaveDialog({ + defaultPath, + filters: [{ name: 'Code Snippets', extensions: ['code-snippets'] }] + }); + if (!path || dirname(path) !== defaultPath) { + return undefined; + } + await writeFile(path, [ + '{', + '/*', + '\t// Each snippet is defined under a snippet name and has a scope, prefix, body and ', + '\t// description. The scope defines in watch languages the snippet is applicable. 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')); + + return path; + } +} + +type SnippetPick = LanguagePick | SnippetFilePick | NewSnippetFilePick; + +CommandsRegistry.registerCommand(id, async accessor => { + + const quickOpenService = accessor.get(IQuickOpenService); + const windowsService = accessor.get(IWindowsService); + const windowService = accessor.get(IWindowService); + const modeService = accessor.get(IModeService); + const envService = accessor.get(IEnvironmentService); + + function openFile(filePath: string): TPromise { + return windowsService.openWindow([filePath], { forceReuseWindow: true }); + } + + const picks = [ + ...LanguagePick.list(modeService, envService), + ...await SnippetFilePick.list(envService), + new NewSnippetFilePick(envService, windowService), + ]; + + const pick = await quickOpenService.pick(picks, { + placeHolder: nls.localize('openSnippet.pickLanguage', "Select Language or Global snippet") + }); + + if (pick instanceof LanguagePick) { + if (!await fileExists(pick.filepath)) { + await pick.create(); + } + return openFile(pick.filepath); + + } else if (pick instanceof SnippetFilePick) { + // simply open the file + return openFile(pick.filepath); + + } else if (pick instanceof NewSnippetFilePick) { + await timeout(500); // quick pick will stay open otherwise + const path = await pick.create(); + if (path) { + return openFile(path); + } + } +}); + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id, + title: { value: nls.localize('openSnippet.label', "Open User Snippets"), original: 'Preferences: Open User Snippets' }, + category: nls.localize('preferences', "Preferences") + } +}); diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippets.contribution.ts b/src/vs/workbench/parts/snippets/electron-browser/snippets.contribution.ts index 7a8a76cba0d..c6188f7543d 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippets.contribution.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippets.contribution.ts @@ -3,22 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { fileExists, writeFile } from 'vs/base/node/pfs'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; + import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; -import { join } from 'path'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { LanguageId } from 'vs/editor/common/modes'; -import { TPromise } from 'vs/base/common/winjs.base'; import { SnippetParser, Variable, Placeholder, Text } from 'vs/editor/contrib/snippet/snippetParser'; import { KnownSnippetVariableNames } from 'vs/editor/contrib/snippet/snippetVariables'; @@ -134,80 +125,6 @@ export class Snippet { } } - -{ - const id = 'workbench.action.openSnippets'; - - CommandsRegistry.registerCommand(id, accessor => { - - const modeService = accessor.get(IModeService); - const quickOpenService = accessor.get(IQuickOpenService); - const environmentService = accessor.get(IEnvironmentService); - const windowsService = accessor.get(IWindowsService); - - function openFile(filePath: string): TPromise { - return windowsService.openWindow([filePath], { forceReuseWindow: true }); - } - - const modeIds = modeService.getRegisteredModes(); - let picks: IPickOpenEntry[] = []; - modeIds.forEach((modeId) => { - const name = modeService.getLanguageName(modeId); - if (name) { - picks.push({ label: name, id: modeId }); - } - }); - picks = picks.sort((e1, e2) => - e1.label.localeCompare(e2.label) - ); - - return quickOpenService.pick(picks, { placeHolder: nls.localize('openSnippet.pickLanguage', "Select Language for Snippet") }).then((language) => { - if (language) { - const snippetPath = join(environmentService.appSettingsHome, 'snippets', language.id + '.json'); - return fileExists(snippetPath).then((success) => { - if (success) { - return openFile(snippetPath); - } - const defaultContent = [ - '{', - '/*', - '\t// Place your snippets for ' + language.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'); - return writeFile(snippetPath, defaultContent).then(() => { - return openFile(snippetPath); - }, (err) => { - errors.onUnexpectedError(nls.localize('openSnippet.errorOnCreate', 'Unable to create {0}', snippetPath)); - }); - }); - } - return TPromise.as(null); - }); - }); - - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id, - title: { value: nls.localize('openSnippet.label', "Open User Snippets"), original: 'Preferences: Open User Snippets' }, - category: nls.localize('preferences', "Preferences") - } - }); -} - - const languageScopeSchemaId = 'vscode://schemas/snippets'; const languageScopeSchema: IJSONSchema = { id: languageScopeSchemaId, diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index b624558e852..cc5fa97e31a 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -98,6 +98,7 @@ 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/configureSnippets'; import 'vs/workbench/parts/snippets/electron-browser/tabCompletion'; import 'vs/workbench/parts/themes/electron-browser/themes.contribution';