From 20b534d41eec11ab39015b0b2d6d2f0ea46cfbb5 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 3 Nov 2020 15:45:02 +0100 Subject: [PATCH 1/3] add persisted snippet enablement --- .../snippets/browser/snippets.contribution.ts | 8 ++- .../contrib/snippets/browser/snippetsFile.ts | 5 +- .../snippets/browser/snippetsService.ts | 67 +++++++++++++++++-- .../test/browser/snippetsService.test.ts | 9 ++- 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index c7adf5c4fcc26..a98a176e6862d 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -19,9 +19,13 @@ export interface ISnippetsService { getSnippetFiles(): Promise>; - getSnippets(languageId: LanguageId): Promise; + isEnabled(snippet: Snippet): boolean; - getSnippetsSync(languageId: LanguageId): Snippet[]; + updateEnablement(snippet: Snippet, enabled: string): void; + + getSnippets(languageId: LanguageId, includeDisabledSnippets?: boolean): Promise; + + getSnippetsSync(languageId: LanguageId, includeDisabledSnippets?: boolean): Snippet[]; } const languageScopeSchemaId = 'vscode://schemas/snippets'; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index a9e0f87a05bec..f4b312fc7c0b1 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -15,6 +15,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IdleValue } from 'vs/base/common/async'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; +import { relativePath } from 'vs/base/common/resources'; class SnippetBodyInsights { @@ -86,6 +87,7 @@ export class Snippet { readonly body: string, readonly source: string, readonly snippetSource: SnippetSource, + readonly snippetIdentifier?: string ) { // this.prefixLow = prefix ? prefix.toLowerCase() : prefix; @@ -289,7 +291,8 @@ export class SnippetFile { description, body, source, - this.source + this.source, + this._extension && `${relativePath(this._extension.extensionLocation, this.location)}/${name}` )); }); } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 5d1e9d1eb38f2..50eed66c21d45 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -26,6 +26,10 @@ import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchMo import { SnippetCompletionProvider } from './snippetCompletionProvider'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { ResourceMap } from 'vs/base/common/map'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { isStringArray } from 'vs/base/common/types'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; namespace snippetExt { @@ -124,6 +128,47 @@ function watch(service: IFileService, resource: URI, callback: () => any): IDisp ); } +class SnippetEnablement { + + private static _key = 'snippets.ignoredSnippets'; + + private readonly _ignored: Set; + + constructor( + @IStorageService private readonly _storageService: IStorageService, + @IStorageKeysSyncRegistryService storageKeysSyncService: IStorageKeysSyncRegistryService, + ) { + + storageKeysSyncService.registerStorageKey({ key: SnippetEnablement._key, version: 1 }); + + const raw = _storageService.get(SnippetEnablement._key, StorageScope.GLOBAL, ''); + let data: string[] | undefined; + try { + data = JSON.parse(raw); + } catch { } + + this._ignored = isStringArray(data) ? new Set(data) : new Set(); + } + + isIgnored(id: string): boolean { + return this._ignored.has(id); + } + + updateIgnored(id: string, value: boolean): void { + let changed = false; + if (this._ignored.has(id) && !value) { + this._ignored.delete(id); + changed = true; + } else if (!this._ignored.has(id) && value) { + this._ignored.add(id); + changed = true; + } + if (changed) { + this._storageService.store(SnippetEnablement._key, JSON.stringify(Array.from(this._ignored)), StorageScope.GLOBAL); + } + } +} + class SnippetsService implements ISnippetsService { declare readonly _serviceBrand: undefined; @@ -131,6 +176,7 @@ class SnippetsService implements ISnippetsService { private readonly _disposables = new DisposableStore(); private readonly _pendingWork: Promise[] = []; private readonly _files = new ResourceMap(); + private readonly _enablement: SnippetEnablement; constructor( @IEnvironmentService private readonly _environmentService: IEnvironmentService, @@ -140,6 +186,7 @@ class SnippetsService implements ISnippetsService { @IFileService private readonly _fileService: IFileService, @IExtensionResourceLoaderService private readonly _extensionResourceLoaderService: IExtensionResourceLoaderService, @ILifecycleService lifecycleService: ILifecycleService, + @IInstantiationService instantiationService: IInstantiationService, ) { this._pendingWork.push(Promise.resolve(lifecycleService.when(LifecyclePhase.Restored).then(() => { this._initExtensionSnippets(); @@ -148,12 +195,24 @@ class SnippetsService implements ISnippetsService { }))); setSnippetSuggestSupport(new SnippetCompletionProvider(this._modeService, this)); + + this._enablement = instantiationService.createInstance(SnippetEnablement); } dispose(): void { this._disposables.dispose(); } + isEnabled(snippet: Snippet): boolean { + return !snippet.snippetIdentifier || !this._enablement.isIgnored(snippet.snippetIdentifier); + } + + updateEnablement(snippet: Snippet, enabled: string): void { + if (snippet.snippetIdentifier) { + this._enablement.updateIgnored(snippet.snippetIdentifier, !enabled); + } + } + private _joinSnippets(): Promise { const promises = this._pendingWork.slice(0); this._pendingWork.length = 0; @@ -165,7 +224,7 @@ class SnippetsService implements ISnippetsService { return this._files.values(); } - async getSnippets(languageId: LanguageId): Promise { + async getSnippets(languageId: LanguageId, includeIgnored?: boolean): Promise { await this._joinSnippets(); const result: Snippet[] = []; @@ -182,10 +241,10 @@ class SnippetsService implements ISnippetsService { } } await Promise.all(promises); - return result; + return includeIgnored ? result : result.filter(this.isEnabled, this); } - getSnippetsSync(languageId: LanguageId): Snippet[] { + getSnippetsSync(languageId: LanguageId, includeIgnored?: boolean): Snippet[] { const result: Snippet[] = []; const languageIdentifier = this._modeService.getLanguageIdentifier(languageId); if (languageIdentifier) { @@ -197,7 +256,7 @@ class SnippetsService implements ISnippetsService { file.select(langName, result); } } - return result; + return includeIgnored ? result : result.filter(this.isEnabled, this); } // --- loading, watching diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index cb3b8cbc6c5e9..10419ba6cdd4f 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -16,8 +16,7 @@ import { CompletionContext, CompletionTriggerKind } from 'vs/editor/common/modes class SimpleSnippetService implements ISnippetsService { declare readonly _serviceBrand: undefined; - constructor(readonly snippets: Snippet[]) { - } + constructor(readonly snippets: Snippet[]) { } getSnippets() { return Promise.resolve(this.getSnippetsSync()); } @@ -27,6 +26,12 @@ class SimpleSnippetService implements ISnippetsService { getSnippetFiles(): any { throw new Error(); } + isEnabled(): boolean { + throw new Error(); + } + updateEnablement(): void { + throw new Error(); + } } suite('SnippetsService', function () { From 503135e9b80ced5c5c71a65cafa20a1cf746dac3 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 3 Nov 2020 16:32:54 +0100 Subject: [PATCH 2/3] expose snippet enablement inside "Insert Snippet" picker --- .../contrib/snippets/browser/insertSnippet.ts | 122 ++++++++++++------ .../snippets/browser/snippets.contribution.ts | 2 +- .../snippets/browser/snippetsService.ts | 2 +- 3 files changed, 84 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index fd8440e4056ed..1c8beb17116f3 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -15,10 +15,9 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { IQuickPickItem, IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; -interface ISnippetPick extends IQuickPickItem { - snippet: Snippet; -} class Args { @@ -91,7 +90,7 @@ class InsertSnippetAction extends EditorAction { const clipboardService = accessor.get(IClipboardService); const quickInputService = accessor.get(IQuickInputService); - const snippet = await new Promise(async (resolve, reject) => { + const snippet = await new Promise(async (resolve) => { const { lineNumber, column } = editor.getPosition(); let { snippet, name, langId } = Args.fromUser(arg); @@ -129,44 +128,13 @@ class InsertSnippetAction extends EditorAction { if (name) { // take selected snippet - (await snippetService.getSnippets(languageId)).every(snippet => { - if (snippet.name !== name) { - return true; - } - resolve(snippet); - return false; - }); + const snippet = (await snippetService.getSnippets(languageId)).find(snippet => snippet.name === name); + resolve(snippet); + } else { // let user pick a snippet - const snippets = (await snippetService.getSnippets(languageId)).sort(Snippet.compare); - const picks: QuickPickInput[] = []; - 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); + const snippet = await this._pickSnippet(snippetService, quickInputService, languageId); + resolve(snippet); } }); @@ -179,6 +147,80 @@ class InsertSnippetAction extends EditorAction { } SnippetController2.get(editor).insert(snippet.codeSnippet, { clipboardText }); } + + private async _pickSnippet(snippetService: ISnippetsService, quickInputService: IQuickInputService, languageId: LanguageId): Promise { + + interface ISnippetPick extends IQuickPickItem { + snippet: Snippet; + } + + const snippets = (await snippetService.getSnippets(languageId, true)).sort(Snippet.compare); + + const makeSnippetPicks = () => { + const result: QuickPickInput[] = []; + 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; + } + result.push({ type: 'separator', label }); + } + + if (snippet.snippetSource === SnippetSource.Extension) { + const isEnabled = snippetService.isEnabled(snippet); + if (isEnabled) { + pick.buttons = [{ + iconClass: Codicon.eye.classNames, + tooltip: nls.localize('disableSnippet', 'Disable Snippet') + }]; + } else { + pick.description = nls.localize('isDisabled', "(disabled)"); + pick.buttons = [{ + iconClass: Codicon.eyeClosed.classNames, + tooltip: nls.localize('enable.snippet', 'Enable Snippet') + }]; + } + } + + result.push(pick); + prevSnippet = snippet; + } + return result; + }; + + const picker = quickInputService.createQuickPick(); + picker.placeholder = nls.localize('pick.placeholder', "Select a snippet"); + picker.matchOnDescription = true; + picker.ignoreFocusOut = false; + picker.onDidTriggerItemButton(ctx => { + const isEnabled = snippetService.isEnabled(ctx.item.snippet); + snippetService.updateEnablement(ctx.item.snippet, !isEnabled); + picker.items = makeSnippetPicks(); + }); + picker.items = makeSnippetPicks(); + picker.show(); + + // wait for an item to be picked or the picker to become hidden + await Promise.race([Event.toPromise(picker.onDidAccept), Event.toPromise(picker.onDidHide)]); + const result = picker.selectedItems[0]?.snippet; + picker.dispose(); + return result; + } } registerEditorAction(InsertSnippetAction); diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index a98a176e6862d..cdda4fdfff42e 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -21,7 +21,7 @@ export interface ISnippetsService { isEnabled(snippet: Snippet): boolean; - updateEnablement(snippet: Snippet, enabled: string): void; + updateEnablement(snippet: Snippet, enabled: boolean): void; getSnippets(languageId: LanguageId, includeDisabledSnippets?: boolean): Promise; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 50eed66c21d45..07ac2686f30a2 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -207,7 +207,7 @@ class SnippetsService implements ISnippetsService { return !snippet.snippetIdentifier || !this._enablement.isIgnored(snippet.snippetIdentifier); } - updateEnablement(snippet: Snippet, enabled: string): void { + updateEnablement(snippet: Snippet, enabled: boolean): void { if (snippet.snippetIdentifier) { this._enablement.updateIgnored(snippet.snippetIdentifier, !enabled); } From bd7a83c110af91fffb30a046e322ece19c72ebab Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 4 Nov 2020 09:23:07 +0100 Subject: [PATCH 3/3] tweak wording --- src/vs/workbench/contrib/snippets/browser/insertSnippet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index 1c8beb17116f3..2af633dab9f0c 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -186,13 +186,13 @@ class InsertSnippetAction extends EditorAction { if (isEnabled) { pick.buttons = [{ iconClass: Codicon.eye.classNames, - tooltip: nls.localize('disableSnippet', 'Disable Snippet') + tooltip: nls.localize('disableSnippet', 'Hide from IntelliSense') }]; } else { - pick.description = nls.localize('isDisabled', "(disabled)"); + pick.description = nls.localize('isDisabled', "(hidden from IntelliSense)"); pick.buttons = [{ iconClass: Codicon.eyeClosed.classNames, - tooltip: nls.localize('enable.snippet', 'Enable Snippet') + tooltip: nls.localize('enable.snippet', 'Show in IntelliSense') }]; } }