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
This commit is contained in:
Johannes Rieken 2022-07-19 15:54:14 +02:00 committed by GitHub
parent 34f1bc679d
commit a260dc7b3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 166 additions and 106 deletions

View file

@ -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')) {
(<HTMLAnchorElement>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) => {

View file

@ -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));
}
}

View file

@ -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<Snippet[]> {
export async function getSurroundableSnippets(snippetsService: ISnippetsService, model: ITextModel, position: Position, includeDisabledSnippets: boolean): Promise<Snippet[]> {
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<CodeActionList | undefined> {
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,
}
}]
}
};
}
}

View file

@ -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<CodeActionList | undefined> {
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();
}
}

View file

@ -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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SurroundWithSnippetCodeActionProvider, LifecyclePhase.Restored);
Registry.as<IWorkbenchContributionsRegistry>(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: {

View file

@ -12,7 +12,7 @@ export interface ISnippetGetOptions {
includeDisabledSnippets?: boolean;
includeNoPrefixSnippets?: boolean;
noRecencySort?: boolean;
topLevelSnippets?: boolean;
fileTemplateSnippets?: boolean;
}
export interface ISnippetsService {

View file

@ -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,

View file

@ -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;
}