Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { Button } from '../../../../base/browser/ui/button/button.js';
8
import { IAction } from '../../../../base/common/actions.js';
9
import { raceCancellationError } from '../../../../base/common/async.js';
10
import { CancellationToken } from '../../../../base/common/cancellation.js';
11
import { Codicon } from '../../../../base/common/codicons.js';
12
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
13
import { isCancellationError } from '../../../../base/common/errors.js';
14
import { Event } from '../../../../base/common/event.js';
15
import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
16
import { ThemeIcon } from '../../../../base/common/themables.js';
17
import { localize } from '../../../../nls.js';
18
import { ActionListItemKind, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';
19
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
20
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
21
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
22
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
23
import { INotificationService } from '../../../../platform/notification/common/notification.js';
24
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js';
25
import { IBulkEditResult, IBulkEditService } from '../../../browser/services/bulkEditService.js';
26
import { Range } from '../../../common/core/range.js';
27
import { DocumentDropEdit, DocumentPasteEdit } from '../../../common/languages.js';
28
import { TrackedRangeStickiness } from '../../../common/model.js';
29
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';
30
import { createCombinedWorkspaceEdit } from './edit.js';
31
import './postEditWidget.css';
32
33
34
interface EditSet<Edit extends DocumentPasteEdit | DocumentDropEdit> {
35
readonly activeEditIndex: number;
36
readonly allEdits: ReadonlyArray<Edit>;
37
}
38
39
interface ShowCommand {
40
readonly id: string;
41
readonly label: string;
42
}
43
44
class PostEditWidget<T extends DocumentPasteEdit | DocumentDropEdit> extends Disposable implements IContentWidget {
45
private static readonly baseId = 'editor.widget.postEditWidget';
46
47
readonly allowEditorOverflow = true;
48
readonly suppressMouseDown = true;
49
50
private domNode!: HTMLElement;
51
private button!: Button;
52
53
private readonly visibleContext: IContextKey<boolean>;
54
55
constructor(
56
private readonly typeId: string,
57
private readonly editor: ICodeEditor,
58
visibleContext: RawContextKey<boolean>,
59
private readonly showCommand: ShowCommand,
60
private readonly range: Range,
61
private readonly edits: EditSet<T>,
62
private readonly onSelectNewEdit: (editIndex: number) => void,
63
private readonly additionalActions: readonly IAction[],
64
@IContextKeyService contextKeyService: IContextKeyService,
65
@IKeybindingService private readonly _keybindingService: IKeybindingService,
66
@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,
67
) {
68
super();
69
70
this.create();
71
72
this.visibleContext = visibleContext.bindTo(contextKeyService);
73
this.visibleContext.set(true);
74
this._register(toDisposable(() => this.visibleContext.reset()));
75
76
this.editor.addContentWidget(this);
77
this.editor.layoutContentWidget(this);
78
79
this._register(toDisposable((() => this.editor.removeContentWidget(this))));
80
81
this._register(this.editor.onDidChangeCursorPosition(e => {
82
this.dispose();
83
}));
84
85
this._register(Event.runAndSubscribe(_keybindingService.onDidUpdateKeybindings, () => {
86
this._updateButtonTitle();
87
}));
88
}
89
90
private _updateButtonTitle() {
91
const binding = this._keybindingService.lookupKeybinding(this.showCommand.id)?.getLabel();
92
this.button.element.title = this.showCommand.label + (binding ? ` (${binding})` : '');
93
}
94
95
private create(): void {
96
this.domNode = dom.$('.post-edit-widget');
97
98
this.button = this._register(new Button(this.domNode, {
99
supportIcons: true,
100
}));
101
this.button.label = '$(insert)';
102
103
this._register(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, () => this.showSelector()));
104
}
105
106
getId(): string {
107
return PostEditWidget.baseId + '.' + this.typeId;
108
}
109
110
getDomNode(): HTMLElement {
111
return this.domNode;
112
}
113
114
getPosition(): IContentWidgetPosition | null {
115
return {
116
position: this.range.getEndPosition(),
117
preference: [ContentWidgetPositionPreference.BELOW]
118
};
119
}
120
121
showSelector() {
122
const pos = dom.getDomNodePagePosition(this.button.element);
123
const anchor = { x: pos.left + pos.width, y: pos.top + pos.height };
124
125
this._actionWidgetService.show('postEditWidget', false,
126
this.edits.allEdits.map((edit, i): IActionListItem<T> => {
127
return {
128
kind: ActionListItemKind.Action,
129
item: edit,
130
label: edit.title,
131
disabled: false,
132
canPreview: false,
133
group: { title: '', icon: ThemeIcon.fromId(i === this.edits.activeEditIndex ? Codicon.check.id : Codicon.blank.id) },
134
};
135
}), {
136
onHide: () => {
137
this.editor.focus();
138
},
139
onSelect: (item) => {
140
this._actionWidgetService.hide(false);
141
142
const i = this.edits.allEdits.findIndex(edit => edit === item);
143
if (i !== this.edits.activeEditIndex) {
144
return this.onSelectNewEdit(i);
145
}
146
},
147
}, anchor, this.editor.getDomNode() ?? undefined, this.additionalActions);
148
}
149
}
150
151
export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdit> extends Disposable {
152
153
private readonly _currentWidget = this._register(new MutableDisposable<PostEditWidget<T>>());
154
155
constructor(
156
private readonly _id: string,
157
private readonly _editor: ICodeEditor,
158
private readonly _visibleContext: RawContextKey<boolean>,
159
private readonly _showCommand: ShowCommand,
160
private readonly _getAdditionalActions: () => readonly IAction[],
161
@IInstantiationService private readonly _instantiationService: IInstantiationService,
162
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
163
@INotificationService private readonly _notificationService: INotificationService,
164
) {
165
super();
166
167
this._register(Event.any(
168
_editor.onDidChangeModel,
169
_editor.onDidChangeModelContent,
170
)(() => this.clear()));
171
}
172
173
public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet<T>, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise<T>, token: CancellationToken) {
174
if (!ranges.length || !this._editor.hasModel()) {
175
return;
176
}
177
178
const model = this._editor.getModel();
179
const edit = edits.allEdits.at(edits.activeEditIndex);
180
if (!edit) {
181
return;
182
}
183
184
const onDidSelectEdit = async (newEditIndex: number) => {
185
const model = this._editor.getModel();
186
if (!model) {
187
return;
188
}
189
190
await model.undo();
191
this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token);
192
};
193
194
const handleError = (e: Error, message: string) => {
195
if (isCancellationError(e)) {
196
return;
197
}
198
199
this._notificationService.error(message);
200
if (canShowWidget) {
201
this.show(ranges[0], edits, onDidSelectEdit);
202
}
203
};
204
205
const editorStateCts = new EditorStateCancellationTokenSource(this._editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
206
let resolvedEdit: T;
207
try {
208
resolvedEdit = await raceCancellationError(resolve(edit, editorStateCts.token), editorStateCts.token);
209
} catch (e) {
210
return handleError(e, localize('resolveError', "Error resolving edit '{0}':\n{1}", edit.title, toErrorMessage(e)));
211
} finally {
212
editorStateCts.dispose();
213
}
214
215
if (token.isCancellationRequested) {
216
return;
217
}
218
219
const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, resolvedEdit);
220
221
// Use a decoration to track edits around the trigger range
222
const primaryRange = ranges[0];
223
const editTrackingDecoration = model.deltaDecorations([], [{
224
range: primaryRange,
225
options: { description: 'paste-line-suffix', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }
226
}]);
227
228
this._editor.focus();
229
let editResult: IBulkEditResult;
230
let editRange: Range | null;
231
try {
232
editResult = await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor, token });
233
editRange = model.getDecorationRange(editTrackingDecoration[0]);
234
} catch (e) {
235
return handleError(e, localize('applyError', "Error applying edit '{0}':\n{1}", edit.title, toErrorMessage(e)));
236
} finally {
237
model.deltaDecorations(editTrackingDecoration, []);
238
}
239
240
if (token.isCancellationRequested) {
241
return;
242
}
243
244
if (canShowWidget && editResult.isApplied && edits.allEdits.length > 1) {
245
this.show(editRange ?? primaryRange, edits, onDidSelectEdit);
246
}
247
}
248
249
public show(range: Range, edits: EditSet<T>, onDidSelectEdit: (newIndex: number) => void) {
250
this.clear();
251
252
if (this._editor.hasModel()) {
253
this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget<T>, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit, this._getAdditionalActions());
254
}
255
}
256
257
public clear() {
258
this._currentWidget.clear();
259
}
260
261
public tryShowSelector() {
262
this._currentWidget.value?.showSelector();
263
}
264
}
265
266