From a260dc7b3effcbd1a635ee8df49bd968ccb76be5 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 19 Jul 2022 15:54:14 +0200 Subject: [PATCH] joh/familiar sparrow (#155613) * rename to `isFileTemplate` * 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} | 18 +-- .../browser/commands/surroundWithSnippet.ts | 83 +---------- .../browser/snippetCodeActionProvider.ts | 139 ++++++++++++++++++ .../snippets/browser/snippets.contribution.ts | 14 +- .../contrib/snippets/browser/snippets.ts | 2 +- .../contrib/snippets/browser/snippetsFile.ts | 8 +- .../snippets/browser/snippetsService.ts | 2 +- 8 files changed, 166 insertions(+), 106 deletions(-) rename src/vs/workbench/contrib/snippets/browser/commands/{emptyFileSnippets.ts => fileTemplateSnippets.ts} (86%) 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 86% rename from src/vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets.ts rename to src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts index 963c92e60cb7b..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,13 +37,14 @@ 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()) { return; } - const snippets = await snippetService.getSnippets(undefined, { topLevelSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); + const snippets = await snippetService.getSnippets(undefined, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); if (snippets.length === 0) { return; } @@ -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 39f0c8233c5dc..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'; @@ -42,8 +44,8 @@ const snippetSchemaProperties: IJSONSchemaMap = { description: nls.localize('snippetSchema.json.prefix', 'The prefix to use when selecting the snippet in intellisense'), type: ['string', 'array'] }, - isTopLevel: { - description: nls.localize('snippetSchema.json.isTopLevel', 'The snippet is only applicable to empty files.'), + isFileTemplate: { + description: nls.localize('snippetSchema.json.isFileTemplate', 'The snippet is meant to populate or replace a whole file'), type: 'boolean' }, body: { diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.ts b/src/vs/workbench/contrib/snippets/browser/snippets.ts index fa485ab0f2fb7..a11c4ccf6a873 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.ts @@ -12,7 +12,7 @@ export interface ISnippetGetOptions { includeDisabledSnippets?: boolean; includeNoPrefixSnippets?: boolean; noRecencySort?: boolean; - topLevelSnippets?: boolean; + fileTemplateSnippets?: boolean; } export interface ISnippetsService { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index b6ce272ef288f..070fd986cdf1a 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -105,7 +105,7 @@ export class Snippet { readonly prefixLow: string; constructor( - readonly isTopLevel: boolean, + readonly isFileTemplate: boolean, readonly scopes: string[], readonly name: string, readonly prefix: string, @@ -143,7 +143,7 @@ export class Snippet { interface JsonSerializedSnippet { - isTopLevel?: boolean; + isFileTemplate?: boolean; body: string | string[]; scope?: string; prefix: string | string[] | undefined; @@ -261,7 +261,7 @@ export class SnippetFile { private _parseSnippet(name: string, snippet: JsonSerializedSnippet, bucket: Snippet[]): void { - let { isTopLevel, prefix, body, description } = snippet; + let { isFileTemplate, prefix, body, description } = snippet; if (!prefix) { prefix = ''; @@ -306,7 +306,7 @@ export class SnippetFile { for (const _prefix of Array.isArray(prefix) ? prefix : Iterable.single(prefix)) { bucket.push(new Snippet( - Boolean(isTopLevel), + Boolean(isFileTemplate), scopes, name, _prefix, diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 3c5ed85f665c3..069f3bf44f32e 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -318,7 +318,7 @@ export class SnippetsService implements ISnippetsService { // enabled or disabled wanted continue; } - if (typeof opts?.topLevelSnippets === 'boolean' && opts.topLevelSnippets !== snippet.isTopLevel) { + if (typeof opts?.fileTemplateSnippets === 'boolean' && opts.fileTemplateSnippets !== snippet.isFileTemplate) { // isTopLevel requested but mismatching continue; }