From 212ef4b52a035d65ac25f226f56b1d7aa90d976c Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 19 Jul 2022 15:36:47 +0200 Subject: [PATCH] add code snippet provider for file templates, fix setting model mode https://github.com/microsoft/vscode/issues/145929 --- .../browser/untitledTextEditorHint.ts | 6 +- ...ileSnippets.ts => fileTemplateSnippets.ts} | 16 +- .../browser/commands/surroundWithSnippet.ts | 83 +---------- .../browser/snippetCodeActionProvider.ts | 139 ++++++++++++++++++ .../snippets/browser/snippets.contribution.ts | 10 +- 5 files changed, 157 insertions(+), 97 deletions(-) rename src/vs/workbench/contrib/snippets/browser/commands/{emptyFileSnippets.ts => fileTemplateSnippets.ts} (88%) create mode 100644 src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts index bac3e7535193d..99cd7d942c9c5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts @@ -20,7 +20,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IContentActionHandler, renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import { SelectSnippetForEmptyFile } from 'vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets'; +import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; const $ = dom.$; @@ -136,7 +136,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { this.domNode.append(hintElement); // ugly way to associate keybindings... - const keybindingsLookup = [ChangeLanguageAction.ID, SelectSnippetForEmptyFile.Id, 'welcome.showNewFileEntries']; + const keybindingsLookup = [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries']; for (const anchor of hintElement.querySelectorAll('A')) { (anchor).style.cursor = 'pointer'; const id = keybindingsLookup.shift(); @@ -156,7 +156,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { const snippetOnClickOrTab = async (e: MouseEvent) => { e.stopPropagation(); this.editor.focus(); - this.commandService.executeCommand(SelectSnippetForEmptyFile.Id, { from: 'hint' }); + this.commandService.executeCommand(ApplyFileSnippetAction.Id, { from: 'hint' }); }; const chooseEditorOnClickOrTap = async (e: MouseEvent) => { diff --git a/src/vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts similarity index 88% rename from src/vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets.ts rename to src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts index a6092e6fb971e..2074eef20bd6d 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts @@ -7,6 +7,7 @@ import { groupBy, isFalsyOrEmpty } from 'vs/base/common/arrays'; import { compare } from 'vs/base/common/strings'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IModelService } from 'vs/editor/common/services/model'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -16,16 +17,16 @@ import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -export class SelectSnippetForEmptyFile extends SnippetsAction { +export class ApplyFileSnippetAction extends SnippetsAction { - static readonly Id = 'workbench.action.populateFromSnippet'; + static readonly Id = 'workbench.action.populateFileFromSnippet'; constructor() { super({ - id: SelectSnippetForEmptyFile.Id, + id: ApplyFileSnippetAction.Id, title: { - value: localize('label', 'Populate from Snippet'), - original: 'Populate from Snippet' + value: localize('label', 'Populate File from Snippet'), + original: 'Populate File from Snippet' }, f1: true, }); @@ -36,6 +37,7 @@ export class SelectSnippetForEmptyFile extends SnippetsAction { const quickInputService = accessor.get(IQuickInputService); const editorService = accessor.get(IEditorService); const langService = accessor.get(ILanguageService); + const modelService = accessor.get(IModelService); const editor = getCodeEditor(editorService.activeTextEditorControl); if (!editor || !editor.hasModel()) { @@ -60,9 +62,7 @@ export class SelectSnippetForEmptyFile extends SnippetsAction { }]); // set language if possible - if (langService.isRegisteredLanguageId(selection.langId)) { - editor.getModel().setMode(selection.langId); - } + modelService.setMode(editor.getModel(), langService.createById(selection.langId)); } } diff --git a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts index 7a771724f3ab8..bdd368721580e 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts @@ -3,28 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CodeAction, CodeActionList, CodeActionProvider } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions'; import { pickSnippet } from 'vs/workbench/contrib/snippets/browser/snippetPicker'; import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { ISnippetsService } from '../snippets'; -async function getSurroundableSnippets(snippetsService: ISnippetsService, model: ITextModel, position: Position, includeDisabledSnippets: boolean): Promise { +export async function getSurroundableSnippets(snippetsService: ISnippetsService, model: ITextModel, position: Position, includeDisabledSnippets: boolean): Promise { const { lineNumber, column } = position; model.tokenization.tokenizeIfCheap(lineNumber); @@ -83,77 +76,3 @@ export class SurroundWithSnippetEditorAction extends SnippetEditorAction { snippetsService.updateUsageTimestamp(snippet); } } - - -export class SurroundWithSnippetCodeActionProvider implements CodeActionProvider, IWorkbenchContribution { - - private static readonly _MAX_CODE_ACTIONS = 4; - - private static readonly _overflowCommandCodeAction: CodeAction = { - kind: CodeActionKind.Refactor.value, - title: SurroundWithSnippetEditorAction.options.title.value, - command: { - id: SurroundWithSnippetEditorAction.options.id, - title: SurroundWithSnippetEditorAction.options.title.value, - }, - }; - - private readonly _registration: IDisposable; - - constructor( - @ISnippetsService private readonly _snippetService: ISnippetsService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, - ) { - this._registration = languageFeaturesService.codeActionProvider.register('*', this); - } - - dispose(): void { - this._registration.dispose(); - } - - async provideCodeActions(model: ITextModel, range: Range | Selection): Promise { - - if (range.isEmpty()) { - return undefined; - } - - const position = Selection.isISelection(range) ? range.getPosition() : range.getStartPosition(); - const snippets = await getSurroundableSnippets(this._snippetService, model, position, false); - if (!snippets.length) { - return undefined; - } - - const actions: CodeAction[] = []; - const hasMore = snippets.length > SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS; - const len = Math.min(snippets.length, SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS); - - for (let i = 0; i < len; i++) { - actions.push(this._makeCodeActionForSnippet(snippets[i], model, range)); - } - if (hasMore) { - actions.push(SurroundWithSnippetCodeActionProvider._overflowCommandCodeAction); - } - return { - actions, - dispose() { } - }; - } - - private _makeCodeActionForSnippet(snippet: Snippet, model: ITextModel, range: IRange): CodeAction { - return { - title: localize('codeAction', "Surround With: {0}", snippet.name), - kind: CodeActionKind.Refactor.value, - edit: { - edits: [{ - versionId: model.getVersionId(), - resource: model.uri, - textEdit: { - range, - text: snippet.body, - insertAsSnippet: true, - } - }] - } - }; - } -} diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts new file mode 100644 index 0000000000000..08afb39eba91f --- /dev/null +++ b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CodeAction, CodeActionList, CodeActionProvider, WorkspaceEdit } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; +import { getSurroundableSnippets, SurroundWithSnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet'; +import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; +import { ISnippetsService } from './snippets'; + +class SurroundWithSnippetCodeActionProvider implements CodeActionProvider { + + private static readonly _MAX_CODE_ACTIONS = 4; + + private static readonly _overflowCommandCodeAction: CodeAction = { + kind: CodeActionKind.Refactor.value, + title: SurroundWithSnippetEditorAction.options.title.value, + command: { + id: SurroundWithSnippetEditorAction.options.id, + title: SurroundWithSnippetEditorAction.options.title.value, + }, + }; + + constructor(@ISnippetsService private readonly _snippetService: ISnippetsService) { } + + async provideCodeActions(model: ITextModel, range: Range | Selection): Promise { + + if (range.isEmpty()) { + return undefined; + } + + const position = Selection.isISelection(range) ? range.getPosition() : range.getStartPosition(); + const snippets = await getSurroundableSnippets(this._snippetService, model, position, false); + if (!snippets.length) { + return undefined; + } + + const actions: CodeAction[] = []; + for (const snippet of snippets) { + if (actions.length >= SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS) { + actions.push(SurroundWithSnippetCodeActionProvider._overflowCommandCodeAction); + break; + } + actions.push({ + title: localize('codeAction', "Surround With: {0}", snippet.name), + kind: CodeActionKind.Refactor.value, + edit: asWorkspaceEdit(model, range, snippet) + }); + } + + return { + actions, + dispose() { } + }; + } +} + +class FileTemplateCodeActionProvider implements CodeActionProvider { + + private static readonly _MAX_CODE_ACTIONS = 4; + + private static readonly _overflowCommandCodeAction: CodeAction = { + title: localize('overflow.start.title', 'Start with Snippet'), + kind: CodeActionKind.Refactor.value, + command: { + id: ApplyFileSnippetAction.Id, + title: '' + } + }; + + readonly providedCodeActionKinds?: readonly string[] = [CodeActionKind.Refactor.value]; + + constructor(@ISnippetsService private readonly _snippetService: ISnippetsService) { } + + async provideCodeActions(model: ITextModel) { + if (model.getValueLength() !== 0) { + return undefined; + } + + const snippets = await this._snippetService.getSnippets(model.getLanguageId(), { fileTemplateSnippets: true, includeNoPrefixSnippets: true }); + const actions: CodeAction[] = []; + for (const snippet of snippets) { + if (actions.length >= FileTemplateCodeActionProvider._MAX_CODE_ACTIONS) { + actions.push(FileTemplateCodeActionProvider._overflowCommandCodeAction); + break; + } + actions.push({ + title: localize('title', 'Start with: {0}', snippet.name), + kind: CodeActionKind.Refactor.value, + edit: asWorkspaceEdit(model, model.getFullModelRange(), snippet) + }); + } + return { + actions, + dispose() { } + }; + } +} + +function asWorkspaceEdit(model: ITextModel, range: IRange, snippet: Snippet): WorkspaceEdit { + return { + edits: [{ + versionId: model.getVersionId(), + resource: model.uri, + textEdit: { + range, + text: snippet.body, + insertAsSnippet: true, + } + }] + }; +} + +export class SnippetCodeActions implements IWorkbenchContribution { + + private readonly _store = new DisposableStore(); + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + ) { + this._store.add(languageFeaturesService.codeActionProvider.register('*', instantiationService.createInstance(SurroundWithSnippetCodeActionProvider))); + this._store.add(languageFeaturesService.codeActionProvider.register('*', instantiationService.createInstance(FileTemplateCodeActionProvider))); + } + + dispose(): void { + this._store.dispose(); + } +} diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index d0ff0106ea020..dfd4bb7e37a4e 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -12,9 +12,10 @@ import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonCo import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ConfigureSnippets } from 'vs/workbench/contrib/snippets/browser/commands/configureSnippets'; -import { SelectSnippetForEmptyFile } from 'vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets'; +import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; import { InsertSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/insertSnippet'; -import { SurroundWithSnippetCodeActionProvider, SurroundWithSnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet'; +import { SurroundWithSnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet'; +import { SnippetCodeActions } from 'vs/workbench/contrib/snippets/browser/snippetCodeActionProvider'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets'; import { SnippetsService } from 'vs/workbench/contrib/snippets/browser/snippetsService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -29,10 +30,11 @@ registerAction2(InsertSnippetAction); CommandsRegistry.registerCommandAlias('editor.action.showSnippets', 'editor.action.insertSnippet'); registerAction2(SurroundWithSnippetEditorAction); registerAction2(ConfigureSnippets); -registerAction2(SelectSnippetForEmptyFile); +registerAction2(ApplyFileSnippetAction); // workbench contribs -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SurroundWithSnippetCodeActionProvider, LifecyclePhase.Restored); +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(SnippetCodeActions, LifecyclePhase.Restored); // schema const languageScopeSchemaId = 'vscode://schemas/snippets';