Skip to content

Commit

Permalink
joh/familiar sparrow (#155613)
Browse files Browse the repository at this point in the history
* rename to `isFileTemplate`

* add code snippet provider for file templates, fix setting model mode

#145929
  • Loading branch information
jrieken committed Jul 19, 2022
1 parent 34f1bc6 commit a260dc7
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.$;

Expand Down Expand Up @@ -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();
Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
});
Expand All @@ -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;
}
Expand All @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
}
}]
}
};
}
}
139 changes: 139 additions & 0 deletions src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/snippets/browser/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ISnippetGetOptions {
includeDisabledSnippets?: boolean;
includeNoPrefixSnippets?: boolean;
noRecencySort?: boolean;
topLevelSnippets?: boolean;
fileTemplateSnippets?: boolean;
}

export interface ISnippetsService {
Expand Down
Loading

0 comments on commit a260dc7

Please sign in to comment.