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
5272 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
this.button.element.title = this._keybindingService.appendKeybinding(this.showCommand.label, this.showCommand.id);
92
}
93
94
private create(): void {
95
this.domNode = dom.$('.post-edit-widget');
96
97
this.button = this._register(new Button(this.domNode, {
98
supportIcons: true,
99
}));
100
this.button.label = '$(insert)';
101
102
this._register(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, () => this.showSelector()));
103
}
104
105
getId(): string {
106
return PostEditWidget.baseId + '.' + this.typeId;
107
}
108
109
getDomNode(): HTMLElement {
110
return this.domNode;
111
}
112
113
getPosition(): IContentWidgetPosition | null {
114
return {
115
position: this.range.getEndPosition(),
116
preference: [ContentWidgetPositionPreference.BELOW]
117
};
118
}
119
120
showSelector() {
121
const pos = dom.getDomNodePagePosition(this.button.element);
122
const anchor = { x: pos.left + pos.width, y: pos.top + pos.height };
123
124
this._actionWidgetService.show('postEditWidget', false,
125
this.edits.allEdits.map((edit, i): IActionListItem<T> => {
126
return {
127
kind: ActionListItemKind.Action,
128
item: edit,
129
label: edit.title,
130
disabled: false,
131
canPreview: false,
132
group: { title: '', icon: ThemeIcon.fromId(i === this.edits.activeEditIndex ? Codicon.check.id : Codicon.blank.id) },
133
};
134
}), {
135
onHide: () => {
136
this.editor.focus();
137
},
138
onSelect: (item) => {
139
this._actionWidgetService.hide(false);
140
141
const i = this.edits.allEdits.findIndex(edit => edit === item);
142
if (i !== this.edits.activeEditIndex) {
143
return this.onSelectNewEdit(i);
144
}
145
},
146
}, anchor, this.editor.getDomNode() ?? undefined, this.additionalActions);
147
}
148
}
149
150
export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdit> extends Disposable {
151
152
private readonly _currentWidget = this._register(new MutableDisposable<PostEditWidget<T>>());
153
154
constructor(
155
private readonly _id: string,
156
private readonly _editor: ICodeEditor,
157
private readonly _visibleContext: RawContextKey<boolean>,
158
private readonly _showCommand: ShowCommand,
159
private readonly _getAdditionalActions: () => readonly IAction[],
160
@IInstantiationService private readonly _instantiationService: IInstantiationService,
161
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
162
@INotificationService private readonly _notificationService: INotificationService,
163
) {
164
super();
165
166
this._register(Event.any(
167
_editor.onDidChangeModel,
168
_editor.onDidChangeModelContent,
169
)(() => this.clear()));
170
}
171
172
public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet<T>, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise<T>, token: CancellationToken) {
173
if (!ranges.length || !this._editor.hasModel()) {
174
return;
175
}
176
177
const model = this._editor.getModel();
178
const edit = edits.allEdits.at(edits.activeEditIndex);
179
if (!edit) {
180
return;
181
}
182
183
const onDidSelectEdit = async (newEditIndex: number) => {
184
const model = this._editor.getModel();
185
if (!model) {
186
return;
187
}
188
189
await model.undo();
190
this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token);
191
};
192
193
const handleError = (e: Error, message: string) => {
194
if (isCancellationError(e)) {
195
return;
196
}
197
198
this._notificationService.error(message);
199
if (canShowWidget) {
200
this.show(ranges[0], edits, onDidSelectEdit);
201
}
202
};
203
204
const editorStateCts = new EditorStateCancellationTokenSource(this._editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
205
let resolvedEdit: T;
206
try {
207
resolvedEdit = await raceCancellationError(resolve(edit, editorStateCts.token), editorStateCts.token);
208
} catch (e) {
209
return handleError(e, localize('resolveError', "Error resolving edit '{0}':\n{1}", edit.title, toErrorMessage(e)));
210
} finally {
211
editorStateCts.dispose();
212
}
213
214
if (token.isCancellationRequested) {
215
return;
216
}
217
218
const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, resolvedEdit);
219
220
// Use a decoration to track edits around the trigger range
221
const primaryRange = ranges[0];
222
const editTrackingDecoration = model.deltaDecorations([], [{
223
range: primaryRange,
224
options: { description: 'paste-line-suffix', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }
225
}]);
226
227
this._editor.focus();
228
let editResult: IBulkEditResult;
229
let editRange: Range | null;
230
try {
231
editResult = await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor, token });
232
editRange = model.getDecorationRange(editTrackingDecoration[0]);
233
} catch (e) {
234
return handleError(e, localize('applyError', "Error applying edit '{0}':\n{1}", edit.title, toErrorMessage(e)));
235
} finally {
236
model.deltaDecorations(editTrackingDecoration, []);
237
}
238
239
if (token.isCancellationRequested) {
240
return;
241
}
242
243
if (canShowWidget && editResult.isApplied && edits.allEdits.length > 1) {
244
this.show(editRange ?? primaryRange, edits, onDidSelectEdit);
245
}
246
}
247
248
public show(range: Range, edits: EditSet<T>, onDidSelectEdit: (newIndex: number) => void) {
249
this.clear();
250
251
if (this._editor.hasModel()) {
252
this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget<T>, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit, this._getAdditionalActions());
253
}
254
}
255
256
public clear() {
257
this._currentWidget.clear();
258
}
259
260
public tryShowSelector() {
261
this._currentWidget.value?.showSelector();
262
}
263
}
264
265