Skip to content

Commit 241057e

Browse files
authored
Merge pull request microsoft#152718 from babakks/add-surround-with-snippet-to-quick-fixes
🎁 Add "Surround with snippet" to quick fix menu
2 parents 1e6ee2c + 4398625 commit 241057e

File tree

3 files changed

+158
-35
lines changed

3 files changed

+158
-35
lines changed

src/vs/editor/contrib/snippet/browser/snippetSession.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,17 @@ export class SnippetSession {
535535
const parser = new SnippetParser();
536536
const snippet = new TextmateSnippet();
537537

538+
// snippet variables resolver
539+
const resolver = new CompositeSnippetVariableResolver([
540+
editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model)),
541+
new ClipboardBasedVariableResolver(() => clipboardText, 0, editor.getSelections().length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'),
542+
new SelectionBasedVariableResolver(model, editor.getSelection(), 0, overtypingCapturer),
543+
new CommentBasedVariableResolver(model, editor.getSelection(), languageConfigurationService),
544+
new TimeBasedVariableResolver,
545+
new WorkspaceBasedVariableResolver(editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService))),
546+
new RandomBasedVariableResolver,
547+
]);
548+
538549
//
539550
snippetEdits = snippetEdits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
540551
let offset = 0;
@@ -553,6 +564,7 @@ export class SnippetSession {
553564
}
554565

555566
parser.parseFragment(template, snippet);
567+
snippet.resolveVariables(resolver);
556568

557569
const snippetText = snippet.toString();
558570
const snippetFragmentText = snippetText.slice(offset);
@@ -568,19 +580,6 @@ export class SnippetSession {
568580
//
569581
parser.ensureFinalTabstop(snippet, enforceFinalTabstop, true);
570582

571-
// snippet variables resolver
572-
const resolver = new CompositeSnippetVariableResolver([
573-
editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model)),
574-
new ClipboardBasedVariableResolver(() => clipboardText, 0, editor.getSelections().length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'),
575-
new SelectionBasedVariableResolver(model, editor.getSelection(), 0, overtypingCapturer),
576-
new CommentBasedVariableResolver(model, editor.getSelection(), languageConfigurationService),
577-
new TimeBasedVariableResolver,
578-
new WorkspaceBasedVariableResolver(editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService))),
579-
new RandomBasedVariableResolver,
580-
]);
581-
snippet.resolveVariables(resolver);
582-
583-
584583
return {
585584
edits,
586585
snippets: [new OneSnippet(editor, snippet, '')]

src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,15 @@ suite('SnippetSession', function () {
3838
languageConfigurationService = new TestLanguageConfigurationService();
3939
const serviceCollection = new ServiceCollection(
4040
[ILabelService, new class extends mock<ILabelService>() { }],
41-
[IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { }],
42-
[ILanguageConfigurationService, languageConfigurationService]
41+
[ILanguageConfigurationService, languageConfigurationService],
42+
[IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {
43+
override getWorkspace() {
44+
return {
45+
id: 'workspace-id',
46+
folders: [],
47+
};
48+
}
49+
}],
4350
);
4451
editor = createTestCodeEditor(model, { serviceCollection }) as IActiveCodeEditor;
4552
editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]);
@@ -774,5 +781,23 @@ suite('SnippetSession', function () {
774781
assert.strictEqual(result.snippets.length, 1);
775782
assert.strictEqual(result.snippets[0].isTrivialSnippet, false);
776783
});
784+
785+
test('with $SELECTION variable', function () {
786+
editor.getModel().setValue('Some text and a selection');
787+
editor.setSelections([new Selection(1, 17, 1, 26)]);
788+
789+
const result = SnippetSession.createEditsAndSnippetsFromEdits(
790+
editor,
791+
[{ range: new Range(1, 17, 1, 26), template: 'wrapped <$SELECTION>' }],
792+
true, true, undefined, undefined, languageConfigurationService
793+
);
794+
795+
assert.strictEqual(result.edits.length, 1);
796+
assert.deepStrictEqual(result.edits[0].range, new Range(1, 17, 1, 26));
797+
assert.deepStrictEqual(result.edits[0].text, 'wrapped <selection>');
798+
799+
assert.strictEqual(result.snippets.length, 1);
800+
assert.strictEqual(result.snippets[0].isTrivialSnippet, true);
801+
});
777802
});
778803
});

src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts

Lines changed: 119 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,48 +13,147 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
1313
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
1414
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
1515
import { pickSnippet } from 'vs/workbench/contrib/snippets/browser/snippetPicker';
16-
import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution';
16+
import { ISnippetsService } from './snippets.contribution';
17+
import { IDisposable } from 'vs/base/common/lifecycle';
18+
import { ITextModel } from 'vs/editor/common/model';
19+
import { CodeAction, CodeActionProvider, CodeActionList } from 'vs/editor/common/languages';
20+
import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types';
21+
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
22+
import { Range, IRange } from 'vs/editor/common/core/range';
23+
import { Selection } from 'vs/editor/common/core/selection';
24+
import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
25+
import { Registry } from 'vs/platform/registry/common/platform';
26+
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
27+
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
28+
import { Position } from 'vs/editor/common/core/position';
1729

30+
async function getSurroundableSnippets(snippetsService: ISnippetsService, model: ITextModel, position: Position): Promise<Snippet[]> {
1831

19-
registerAction2(class SurroundWithAction extends EditorAction2 {
32+
const { lineNumber, column } = position;
33+
model.tokenization.tokenizeIfCheap(lineNumber);
34+
const languageId = model.getLanguageIdAtPosition(lineNumber, column);
35+
36+
const allSnippets = await snippetsService.getSnippets(languageId, { includeNoPrefixSnippets: true, includeDisabledSnippets: true });
37+
return allSnippets.filter(snippet => snippet.usesSelection);
38+
}
39+
40+
class SurroundWithSnippetEditorAction extends EditorAction2 {
41+
42+
static readonly options = {
43+
id: 'editor.action.surroundWithSnippet',
44+
title: {
45+
value: localize('label', 'Surround With Snippet...'),
46+
original: 'Surround With Snippet...'
47+
}
48+
};
2049

2150
constructor() {
2251
super({
23-
id: 'editor.action.surroundWithSnippet',
24-
title: { value: localize('label', 'Surround With Snippet...'), original: 'Surround With Snippet...' },
25-
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasNonEmptySelection),
26-
f1: true
52+
...SurroundWithSnippetEditorAction.options,
53+
precondition: ContextKeyExpr.and(
54+
EditorContextKeys.writable,
55+
EditorContextKeys.hasNonEmptySelection
56+
),
57+
f1: true,
2758
});
2859
}
2960

30-
async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
31-
32-
const snippetService = accessor.get(ISnippetsService);
33-
const clipboardService = accessor.get(IClipboardService);
34-
const instaService = accessor.get(IInstantiationService);
35-
61+
async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor) {
3662
if (!editor.hasModel()) {
3763
return;
3864
}
3965

40-
const { lineNumber, column } = editor.getPosition();
41-
editor.getModel().tokenization.tokenizeIfCheap(lineNumber);
42-
const languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column);
66+
const instaService = accessor.get(IInstantiationService);
67+
const snippetsService = accessor.get(ISnippetsService);
68+
const clipboardService = accessor.get(IClipboardService);
4369

44-
const allSnippets = await snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true, includeDisabledSnippets: true });
45-
const surroundSnippets = allSnippets.filter(snippet => snippet.usesSelection);
46-
const snippet = await instaService.invokeFunction(pickSnippet, surroundSnippets);
70+
const snippets = await getSurroundableSnippets(snippetsService, editor.getModel(), editor.getPosition());
71+
if (!snippets.length) {
72+
return;
73+
}
4774

75+
const snippet = await instaService.invokeFunction(pickSnippet, snippets);
4876
if (!snippet) {
4977
return;
5078
}
5179

52-
5380
let clipboardText: string | undefined;
5481
if (snippet.needsClipboard) {
5582
clipboardText = await clipboardService.readText();
5683
}
5784

5885
SnippetController2.get(editor)?.insert(snippet.codeSnippet, { clipboardText });
5986
}
60-
});
87+
}
88+
89+
90+
class SurroundWithSnippetCodeActionProvider implements CodeActionProvider, IWorkbenchContribution {
91+
92+
private static readonly _MAX_CODE_ACTIONS = 4;
93+
94+
private static readonly _overflowCommandCodeAction: CodeAction = {
95+
kind: CodeActionKind.Refactor.value,
96+
title: SurroundWithSnippetEditorAction.options.title.value,
97+
command: {
98+
id: SurroundWithSnippetEditorAction.options.id,
99+
title: SurroundWithSnippetEditorAction.options.title.value,
100+
},
101+
};
102+
103+
private readonly _registration: IDisposable;
104+
105+
constructor(
106+
@ISnippetsService private readonly _snippetService: ISnippetsService,
107+
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
108+
) {
109+
this._registration = languageFeaturesService.codeActionProvider.register('*', this);
110+
}
111+
112+
dispose(): void {
113+
this._registration.dispose();
114+
}
115+
116+
async provideCodeActions(model: ITextModel, range: Range | Selection): Promise<CodeActionList | undefined> {
117+
118+
const snippets = await getSurroundableSnippets(this._snippetService, model, range.getEndPosition());
119+
if (!snippets.length) {
120+
return undefined;
121+
}
122+
123+
const actions: CodeAction[] = [];
124+
const hasMore = snippets.length > SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS;
125+
const len = Math.min(snippets.length, SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS);
126+
127+
for (let i = 0; i < len; i++) {
128+
actions.push(this._makeCodeActionForSnippet(snippets[i], model, range));
129+
}
130+
if (hasMore) {
131+
actions.push(SurroundWithSnippetCodeActionProvider._overflowCommandCodeAction);
132+
}
133+
return {
134+
actions,
135+
dispose() { }
136+
};
137+
}
138+
139+
private _makeCodeActionForSnippet(snippet: Snippet, model: ITextModel, range: IRange): CodeAction {
140+
return {
141+
title: localize('codeAction', "Surround With: {0}", snippet.name),
142+
kind: CodeActionKind.Refactor.value,
143+
edit: {
144+
edits: [{
145+
versionId: model.getVersionId(),
146+
resource: model.uri,
147+
textEdit: {
148+
range,
149+
text: snippet.body,
150+
insertAsSnippet: true,
151+
}
152+
}]
153+
}
154+
};
155+
}
156+
}
157+
158+
registerAction2(SurroundWithSnippetEditorAction);
159+
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SurroundWithSnippetCodeActionProvider, LifecyclePhase.Restored);

0 commit comments

Comments
 (0)