Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.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 { IAction } from '../../../../base/common/actions.js';
7
import { coalesce } from '../../../../base/common/arrays.js';
8
import { CancelablePromise, createCancelablePromise, raceCancellation } from '../../../../base/common/async.js';
9
import { CancellationToken } from '../../../../base/common/cancellation.js';
10
import { VSDataTransfer } from '../../../../base/common/dataTransfer.js';
11
import { isCancellationError } from '../../../../base/common/errors.js';
12
import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';
13
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
14
import { localize } from '../../../../nls.js';
15
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
17
import { LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { toExternalVSDataTransfer } from '../../../browser/dataTransfer.js';
20
import { ICodeEditor } from '../../../browser/editorBrowser.js';
21
import { EditorOption } from '../../../common/config/editorOptions.js';
22
import { IPosition } from '../../../common/core/position.js';
23
import { Range } from '../../../common/core/range.js';
24
import { IEditorContribution } from '../../../common/editorCommon.js';
25
import { DocumentDropEdit, DocumentDropEditProvider } from '../../../common/languages.js';
26
import { ITextModel } from '../../../common/model.js';
27
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
28
import { DraggedTreeItemsIdentifier } from '../../../common/services/treeViewsDnd.js';
29
import { ITreeViewsDnDService } from '../../../common/services/treeViewsDndService.js';
30
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';
31
import { InlineProgressManager } from '../../inlineProgress/browser/inlineProgress.js';
32
import { PreferredDropConfiguration } from './dropIntoEditorContribution.js';
33
import { sortEditsByYieldTo } from './edit.js';
34
import { PostEditWidgetManager } from './postEditWidget.js';
35
36
export const dropAsPreferenceConfig = 'editor.dropIntoEditor.preferences';
37
38
export const changeDropTypeCommandId = 'editor.changeDropType';
39
40
export const dropWidgetVisibleCtx = new RawContextKey<boolean>('dropWidgetVisible', false, localize('dropWidgetVisible', "Whether the drop widget is showing"));
41
42
export class DropIntoEditorController extends Disposable implements IEditorContribution {
43
44
public static readonly ID = 'editor.contrib.dropIntoEditorController';
45
46
public static get(editor: ICodeEditor): DropIntoEditorController | null {
47
return editor.getContribution<DropIntoEditorController>(DropIntoEditorController.ID);
48
}
49
50
public static setConfigureDefaultAction(action: IAction) {
51
this._configureDefaultAction = action;
52
}
53
54
private static _configureDefaultAction?: IAction;
55
56
/**
57
* Global tracking the current drop operation.
58
*
59
* TODO: figure out how to make this work with multiple windows
60
*/
61
private static _currentDropOperation?: CancelablePromise<void>;
62
63
private readonly _dropProgressManager: InlineProgressManager;
64
private readonly _postDropWidgetManager: PostEditWidgetManager<DocumentDropEdit>;
65
66
private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>();
67
68
constructor(
69
editor: ICodeEditor,
70
@IInstantiationService instantiationService: IInstantiationService,
71
@IConfigurationService private readonly _configService: IConfigurationService,
72
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
73
@ITreeViewsDnDService private readonly _treeViewsDragAndDropService: ITreeViewsDnDService
74
) {
75
super();
76
77
this._dropProgressManager = this._register(instantiationService.createInstance(InlineProgressManager, 'dropIntoEditor', editor));
78
this._postDropWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'dropIntoEditor', editor, dropWidgetVisibleCtx,
79
{ id: changeDropTypeCommandId, label: localize('postDropWidgetTitle', "Show drop options...") },
80
() => DropIntoEditorController._configureDefaultAction ? [DropIntoEditorController._configureDefaultAction] : []));
81
82
this._register(editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event)));
83
}
84
85
public clearWidgets() {
86
this._postDropWidgetManager.clear();
87
}
88
89
public changeDropType() {
90
this._postDropWidgetManager.tryShowSelector();
91
}
92
93
private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) {
94
if (!dragEvent.dataTransfer || !editor.hasModel()) {
95
return;
96
}
97
98
DropIntoEditorController._currentDropOperation?.cancel();
99
100
editor.focus();
101
editor.setPosition(position);
102
103
const p = createCancelablePromise(async (token) => {
104
const disposables = new DisposableStore();
105
106
const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token));
107
try {
108
const ourDataTransfer = await this.extractDataTransferData(dragEvent);
109
if (ourDataTransfer.size === 0 || tokenSource.token.isCancellationRequested) {
110
return;
111
}
112
113
const model = editor.getModel();
114
if (!model) {
115
return;
116
}
117
118
const providers = this._languageFeaturesService.documentDropEditProvider
119
.ordered(model)
120
.filter(provider => {
121
if (!provider.dropMimeTypes) {
122
// Keep all providers that don't specify mime types
123
return true;
124
}
125
return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime));
126
});
127
128
const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource.token));
129
if (tokenSource.token.isCancellationRequested) {
130
return;
131
}
132
133
if (editSession.edits.length) {
134
const activeEditIndex = this.getInitialActiveEditIndex(model, editSession.edits);
135
const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop';
136
// Pass in the parent token here as it tracks cancelling the entire drop operation
137
await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: editSession.edits }, canShowWidget, async edit => edit, token);
138
}
139
} finally {
140
disposables.dispose();
141
if (DropIntoEditorController._currentDropOperation === p) {
142
DropIntoEditorController._currentDropOperation = undefined;
143
}
144
}
145
});
146
147
this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p, { cancel: () => p.cancel() });
148
DropIntoEditorController._currentDropOperation = p;
149
}
150
151
private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken) {
152
const disposables = new DisposableStore();
153
154
const results = await raceCancellation(Promise.all(providers.map(async provider => {
155
try {
156
const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, token);
157
if (edits) {
158
disposables.add(edits);
159
}
160
return edits?.edits.map(edit => ({ ...edit, providerId: provider.id }));
161
} catch (err) {
162
if (!isCancellationError(err)) {
163
console.error(err);
164
}
165
console.error(err);
166
}
167
return undefined;
168
})), token);
169
170
const edits = coalesce(results ?? []).flat();
171
return {
172
edits: sortEditsByYieldTo(edits),
173
dispose: () => disposables.dispose()
174
};
175
}
176
177
private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray<DocumentDropEdit>): number {
178
const preferredProviders = this._configService.getValue<PreferredDropConfiguration[]>(dropAsPreferenceConfig, { resource: model.uri });
179
for (const config of Array.isArray(preferredProviders) ? preferredProviders : []) {
180
const desiredKind = new HierarchicalKind(config);
181
const editIndex = edits.findIndex(edit => edit.kind && desiredKind.contains(edit.kind));
182
if (editIndex >= 0) {
183
return editIndex;
184
}
185
}
186
return 0;
187
}
188
189
private async extractDataTransferData(dragEvent: DragEvent): Promise<VSDataTransfer> {
190
if (!dragEvent.dataTransfer) {
191
return new VSDataTransfer();
192
}
193
194
const dataTransfer = toExternalVSDataTransfer(dragEvent.dataTransfer);
195
196
if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) {
197
const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype);
198
if (Array.isArray(data)) {
199
for (const id of data) {
200
const treeDataTransfer = await this._treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier);
201
if (treeDataTransfer) {
202
for (const [type, value] of treeDataTransfer) {
203
dataTransfer.replace(type, value);
204
}
205
}
206
}
207
}
208
}
209
210
return dataTransfer;
211
}
212
}
213
214