Skip to content

Commit 3d471d2

Browse files
authored
Merge pull request microsoft#151841 from microsoft/joh/cute-bobcat
2 parents 0b74e15 + 29567d1 commit 3d471d2

File tree

12 files changed

+105
-20
lines changed

12 files changed

+105
-20
lines changed

src/vs/editor/browser/services/bulkEditService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ export class ResourceEdit {
4545
export class ResourceTextEdit extends ResourceEdit {
4646
constructor(
4747
readonly resource: URI,
48-
readonly textEdit: TextEdit,
48+
readonly textEdit: TextEdit & { insertAsSnippet?: boolean },
4949
readonly versionId?: number,
50-
metadata?: WorkspaceEditMetadata
50+
metadata?: WorkspaceEditMetadata,
5151
) {
5252
super(metadata);
5353
}

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { CompletionItem, CompletionItemKind, CompletionItemProvider } from 'vs/e
1616
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
1717
import { ITextModel } from 'vs/editor/common/model';
1818
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
19-
import { Choice } from 'vs/editor/contrib/snippet/browser/snippetParser';
19+
import { Choice, SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
2020
import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/browser/suggest';
2121
import { OvertypingCapturer } from 'vs/editor/contrib/suggest/browser/suggestOvertypingCapturer';
2222
import { localize } from 'vs/nls';
@@ -346,3 +346,46 @@ export function performSnippetEdit(editor: ICodeEditor, snippet: string, selecti
346346
controller.insert(snippet);
347347
return controller.isInSnippet();
348348
}
349+
350+
351+
export type ISnippetEdit = {
352+
range: Range;
353+
snippet: string;
354+
};
355+
356+
// ---
357+
358+
export function performSnippetEdits(editor: ICodeEditor, edits: ISnippetEdit[]) {
359+
360+
if (!editor.hasModel()) {
361+
return false;
362+
}
363+
if (edits.length === 0) {
364+
return false;
365+
}
366+
367+
const model = editor.getModel();
368+
let newText = '';
369+
let last: ISnippetEdit | undefined;
370+
edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
371+
372+
for (const item of edits) {
373+
if (last) {
374+
const between = Range.fromPositions(last.range.getEndPosition(), item.range.getStartPosition());
375+
const text = model.getValueInRange(between);
376+
newText += SnippetParser.escape(text);
377+
}
378+
newText += item.snippet;
379+
last = item;
380+
}
381+
382+
const controller = SnippetController2.get(editor);
383+
if (!controller) {
384+
return false;
385+
}
386+
model.pushStackElement();
387+
const range = Range.plusRange(edits[0].range, edits[edits.length - 1].range);
388+
editor.setSelection(range);
389+
controller.insert(newText, { undoStopBefore: false });
390+
return controller.isInSnippet();
391+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class OneSnippet {
4949
this._placeholderGroupsIdx = -1;
5050
}
5151

52-
public initialize(textChange: TextChange): void {
52+
initialize(textChange: TextChange): void {
5353
this._offset = textChange.newPosition;
5454
}
5555

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
856856
return extHostWorkspace.saveAll(includeUntitled);
857857
},
858858
applyEdit(edit: vscode.WorkspaceEdit): Thenable<boolean> {
859-
return extHostBulkEdits.applyWorkspaceEdit(edit);
859+
return extHostBulkEdits.applyWorkspaceEdit(edit, extension);
860860
},
861861
createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => {
862862
return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, pattern, ignoreCreate, ignoreChange, ignoreDelete);

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1602,7 +1602,7 @@ export interface IWorkspaceFileEditDto {
16021602
export interface IWorkspaceTextEditDto {
16031603
_type: WorkspaceEditType.Text;
16041604
resource: UriComponents;
1605-
edit: languages.TextEdit;
1605+
edit: languages.TextEdit & { insertAsSnippet?: boolean };
16061606
modelVersionId?: number;
16071607
metadata?: IWorkspaceEditEntryMetadataDto;
16081608
}

src/vs/workbench/api/common/extHostBulkEdits.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
67
import { MainContext, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol';
78
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
89
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
910
import { WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters';
11+
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
1012
import type * as vscode from 'vscode';
1113

1214
export class ExtHostBulkEdits {
@@ -26,8 +28,9 @@ export class ExtHostBulkEdits {
2628
};
2729
}
2830

29-
applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Promise<boolean> {
30-
const dto = WorkspaceEdit.from(edit, this._versionInformationProvider);
31+
applyWorkspaceEdit(edit: vscode.WorkspaceEdit, extension: IExtensionDescription): Promise<boolean> {
32+
const allowSnippetTextEdit = isProposedApiEnabled(extension, 'snippetWorkspaceEdit');
33+
const dto = WorkspaceEdit.from(edit, this._versionInformationProvider, allowSnippetTextEdit);
3134
return this._proxy.$tryApplyWorkspaceEdit(dto);
3235
}
3336
}

src/vs/workbench/api/common/extHostTypeConverters.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ export namespace WorkspaceEdit {
569569
getNotebookDocumentVersion(uri: URI): number | undefined;
570570
}
571571

572-
export function from(value: vscode.WorkspaceEdit, versionInfo?: IVersionInformationProvider): extHostProtocol.IWorkspaceEditDto {
572+
export function from(value: vscode.WorkspaceEdit, versionInfo?: IVersionInformationProvider, allowSnippetTextEdit?: boolean): extHostProtocol.IWorkspaceEditDto {
573573
const result: extHostProtocol.IWorkspaceEditDto = {
574574
edits: []
575575
};
@@ -598,14 +598,21 @@ export namespace WorkspaceEdit {
598598
});
599599

600600
} else if (entry._type === types.FileEditType.Text) {
601+
601602
// text edits
602-
result.edits.push(<extHostProtocol.IWorkspaceTextEditDto>{
603+
const dto = <extHostProtocol.IWorkspaceTextEditDto>{
603604
_type: extHostProtocol.WorkspaceEditType.Text,
604605
resource: entry.uri,
605606
edit: TextEdit.from(entry.edit),
606607
modelVersionId: !toCreate.has(entry.uri) ? versionInfo?.getTextDocumentVersion(entry.uri) : undefined,
607608
metadata: entry.metadata
608-
});
609+
};
610+
if (allowSnippetTextEdit && entry.edit.newText2 instanceof types.SnippetString) {
611+
dto.edit.insertAsSnippet = true;
612+
dto.edit.text = entry.edit.newText2.value;
613+
}
614+
result.edits.push(dto);
615+
609616
} else if (entry._type === types.FileEditType.Cell) {
610617
result.edits.push(<extHostProtocol.IWorkspaceCellEditDto>{
611618
_type: extHostProtocol.WorkspaceEditType.Cell,

src/vs/workbench/api/common/extHostTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ export class TextEdit {
549549

550550
protected _range: Range;
551551
protected _newText: string | null;
552+
newText2?: string | SnippetString;
552553
protected _newEol?: EndOfLine;
553554

554555
get range(): Range {

src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/api/test/c
1212
import { NullLogService } from 'vs/platform/log/common/log';
1313
import { assertType } from 'vs/base/common/types';
1414
import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits';
15+
import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
1516

1617
suite('ExtHostBulkEdits.applyWorkspaceEdit', () => {
1718

@@ -46,7 +47,7 @@ suite('ExtHostBulkEdits.applyWorkspaceEdit', () => {
4647
test('uses version id if document available', async () => {
4748
const edit = new extHostTypes.WorkspaceEdit();
4849
edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello');
49-
await bulkEdits.applyWorkspaceEdit(edit);
50+
await bulkEdits.applyWorkspaceEdit(edit, nullExtensionDescription);
5051
assert.strictEqual(workspaceResourceEdits.edits.length, 1);
5152
const [first] = workspaceResourceEdits.edits;
5253
assertType(first._type === WorkspaceEditType.Text);
@@ -56,7 +57,7 @@ suite('ExtHostBulkEdits.applyWorkspaceEdit', () => {
5657
test('does not use version id if document is not available', async () => {
5758
const edit = new extHostTypes.WorkspaceEdit();
5859
edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello');
59-
await bulkEdits.applyWorkspaceEdit(edit);
60+
await bulkEdits.applyWorkspaceEdit(edit, nullExtensionDescription);
6061
assert.strictEqual(workspaceResourceEdits.edits.length, 1);
6162
const [first] = workspaceResourceEdits.edits;
6263
assertType(first._type === WorkspaceEditType.Text);

src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { ResourceMap } from 'vs/base/common/map';
1919
import { IModelService } from 'vs/editor/common/services/model';
2020
import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
2121
import { CancellationToken } from 'vs/base/common/cancellation';
22+
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
23+
import { performSnippetEdits } from 'vs/editor/contrib/snippet/browser/snippetController2';
2224

2325
type ValidationResult = { canApply: true } | { canApply: false; reason: URI };
2426

@@ -27,7 +29,7 @@ class ModelEditTask implements IDisposable {
2729
readonly model: ITextModel;
2830

2931
private _expectedModelVersionId: number | undefined;
30-
protected _edits: ISingleEditOperation[];
32+
protected _edits: (ISingleEditOperation & { insertAsSnippet?: boolean })[];
3133
protected _newEol: EndOfLineSequence | undefined;
3234

3335
constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) {
@@ -75,7 +77,7 @@ class ModelEditTask implements IDisposable {
7577
} else {
7678
range = Range.lift(textEdit.range);
7779
}
78-
this._edits.push(EditOperation.replaceMove(range, textEdit.text));
80+
this._edits.push({ ...EditOperation.replaceMove(range, textEdit.text), insertAsSnippet: textEdit.insertAsSnippet });
7981
}
8082

8183
validate(): ValidationResult {
@@ -91,7 +93,9 @@ class ModelEditTask implements IDisposable {
9193

9294
apply(): void {
9395
if (this._edits.length > 0) {
94-
this._edits = this._edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
96+
this._edits = this._edits
97+
.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range))
98+
.map(edit => ({ ...edit, text: edit.text && SnippetParser.escape(edit.text) }));
9599
this.model.pushEditOperations(null, this._edits, () => null);
96100
}
97101
if (this._newEol !== undefined) {
@@ -121,10 +125,19 @@ class EditorEditTask extends ModelEditTask {
121125
super.apply();
122126
return;
123127
}
124-
125128
if (this._edits.length > 0) {
126-
this._edits = this._edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
127-
this._editor.executeEdits('', this._edits);
129+
130+
const insertAsSnippet = this._edits.every(edit => edit.insertAsSnippet);
131+
if (insertAsSnippet) {
132+
// todo@jrieken what ABOUT EOL?
133+
performSnippetEdits(this._editor, this._edits.map(edit => ({ range: Range.lift(edit.range!), snippet: edit.text! })));
134+
135+
} else {
136+
this._edits = this._edits
137+
.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range))
138+
.map(edit => ({ ...edit, text: edit.text && SnippetParser.escape(edit.text) }));
139+
this._editor.executeEdits('', this._edits);
140+
}
128141
}
129142
if (this._newEol !== undefined) {
130143
if (this._editor.hasModel()) {
@@ -193,7 +206,7 @@ export class BulkTextEdits {
193206
let makeMinimal = false;
194207
if (this._editor?.getModel()?.uri.toString() === ref.object.textEditorModel.uri.toString()) {
195208
task = new EditorEditTask(ref, this._editor);
196-
makeMinimal = true;
209+
makeMinimal = true && false; // todo@jrieken HACK
197210
} else {
198211
task = new ModelEditTask(ref);
199212
}

0 commit comments

Comments
 (0)