Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.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 { addDisposableListener } from '../../../../base/browser/dom.js';
7
import { IAction } from '../../../../base/common/actions.js';
8
import { coalesce } from '../../../../base/common/arrays.js';
9
import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js';
10
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
11
import { createStringDataTransferItem, IReadonlyVSDataTransfer, matchesMimeType, UriList, VSDataTransfer } from '../../../../base/common/dataTransfer.js';
12
import { isCancellationError } from '../../../../base/common/errors.js';
13
import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';
14
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
15
import { Mimes } from '../../../../base/common/mime.js';
16
import * as platform from '../../../../base/common/platform.js';
17
import { upcast } from '../../../../base/common/types.js';
18
import { generateUuid } from '../../../../base/common/uuid.js';
19
import { localize } from '../../../../nls.js';
20
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
21
import { ICommandService } from '../../../../platform/commands/common/commands.js';
22
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
23
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
24
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
25
import { ILogService } from '../../../../platform/log/common/log.js';
26
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
27
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
28
import { ClipboardEventUtils, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js';
29
import { toExternalVSDataTransfer, toVSDataTransfer } from '../../../browser/dataTransfer.js';
30
import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js';
31
import { IBulkEditService } from '../../../browser/services/bulkEditService.js';
32
import { EditorOption } from '../../../common/config/editorOptions.js';
33
import { IRange, Range } from '../../../common/core/range.js';
34
import { Selection } from '../../../common/core/selection.js';
35
import { Handler, IEditorContribution } from '../../../common/editorCommon.js';
36
import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteTriggerKind } from '../../../common/languages.js';
37
import { ITextModel } from '../../../common/model.js';
38
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
39
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';
40
import { InlineProgressManager } from '../../inlineProgress/browser/inlineProgress.js';
41
import { MessageController } from '../../message/browser/messageController.js';
42
import { PreferredPasteConfiguration } from './copyPasteContribution.js';
43
import { DefaultTextPasteOrDropEditProvider } from './defaultProviders.js';
44
import { createCombinedWorkspaceEdit, sortEditsByYieldTo } from './edit.js';
45
import { PostEditWidgetManager } from './postEditWidget.js';
46
47
export const changePasteTypeCommandId = 'editor.changePasteType';
48
49
export const pasteAsPreferenceConfig = 'editor.pasteAs.preferences';
50
51
export const pasteWidgetVisibleCtx = new RawContextKey<boolean>('pasteWidgetVisible', false, localize('pasteWidgetVisible', "Whether the paste widget is showing"));
52
53
const vscodeClipboardMime = 'application/vnd.code.copymetadata';
54
55
interface CopyMetadata {
56
readonly id?: string;
57
readonly providerCopyMimeTypes?: readonly string[];
58
59
readonly defaultPastePayload: Omit<PastePayload, 'text'>;
60
}
61
62
type PasteEditWithProvider = DocumentPasteEdit & {
63
provider: DocumentPasteEditProvider;
64
};
65
66
67
interface DocumentPasteWithProviderEditsSession {
68
edits: readonly PasteEditWithProvider[];
69
dispose(): void;
70
}
71
72
export type PastePreference =
73
| { readonly only: HierarchicalKind }
74
| { readonly preferences: readonly HierarchicalKind[] }
75
| { readonly providerId: string } // Only used internally
76
;
77
78
interface CopyOperation {
79
readonly providerMimeTypes: readonly string[];
80
readonly operation: CancelablePromise<IReadonlyVSDataTransfer | undefined>;
81
}
82
83
export class CopyPasteController extends Disposable implements IEditorContribution {
84
85
public static readonly ID = 'editor.contrib.copyPasteActionController';
86
87
public static get(editor: ICodeEditor): CopyPasteController | null {
88
return editor.getContribution<CopyPasteController>(CopyPasteController.ID);
89
}
90
91
public static setConfigureDefaultAction(action: IAction) {
92
CopyPasteController._configureDefaultAction = action;
93
}
94
95
private static _configureDefaultAction?: IAction;
96
97
/**
98
* Global tracking the last copy operation.
99
*
100
* This is shared across all editors so that you can copy and paste between groups.
101
*
102
* TODO: figure out how to make this work with multiple windows
103
*/
104
private static _currentCopyOperation?: {
105
readonly handle: string;
106
readonly operations: ReadonlyArray<CopyOperation>;
107
};
108
109
private readonly _editor: ICodeEditor;
110
111
private _currentPasteOperation?: CancelablePromise<void>;
112
private _pasteAsActionContext?: { readonly preferred?: PastePreference };
113
114
private readonly _pasteProgressManager: InlineProgressManager;
115
private readonly _postPasteWidgetManager: PostEditWidgetManager<PasteEditWithProvider>;
116
117
constructor(
118
editor: ICodeEditor,
119
@IInstantiationService instantiationService: IInstantiationService,
120
@ILogService private readonly _logService: ILogService,
121
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
122
@IClipboardService private readonly _clipboardService: IClipboardService,
123
@ICommandService private readonly _commandService: ICommandService,
124
@IConfigurationService private readonly _configService: IConfigurationService,
125
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
126
@IQuickInputService private readonly _quickInputService: IQuickInputService,
127
@IProgressService private readonly _progressService: IProgressService,
128
) {
129
super();
130
131
this._editor = editor;
132
133
const container = editor.getContainerDomNode();
134
this._register(addDisposableListener(container, 'copy', e => this.handleCopy(e)));
135
this._register(addDisposableListener(container, 'cut', e => this.handleCopy(e)));
136
this._register(addDisposableListener(container, 'paste', e => this.handlePaste(e), true));
137
138
this._pasteProgressManager = this._register(new InlineProgressManager('pasteIntoEditor', editor, instantiationService));
139
140
this._postPasteWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'pasteIntoEditor', editor, pasteWidgetVisibleCtx,
141
{ id: changePasteTypeCommandId, label: localize('postPasteWidgetTitle', "Show paste options...") },
142
() => CopyPasteController._configureDefaultAction ? [CopyPasteController._configureDefaultAction] : []
143
));
144
}
145
146
public changePasteType() {
147
this._postPasteWidgetManager.tryShowSelector();
148
}
149
150
public async pasteAs(preferred?: PastePreference) {
151
this._logService.trace('CopyPasteController.pasteAs');
152
this._editor.focus();
153
try {
154
this._logService.trace('Before calling editor.action.clipboardPasteAction');
155
this._pasteAsActionContext = { preferred };
156
await this._commandService.executeCommand('editor.action.clipboardPasteAction');
157
} finally {
158
this._pasteAsActionContext = undefined;
159
}
160
}
161
162
public clearWidgets() {
163
this._postPasteWidgetManager.clear();
164
}
165
166
private isPasteAsEnabled(): boolean {
167
return this._editor.getOption(EditorOption.pasteAs).enabled;
168
}
169
170
public async finishedPaste(): Promise<void> {
171
await this._currentPasteOperation;
172
}
173
174
private handleCopy(e: ClipboardEvent) {
175
let id: string | null = null;
176
if (e.clipboardData) {
177
const [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData);
178
const storedMetadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text);
179
id = storedMetadata?.id || null;
180
this._logService.trace('CopyPasteController#handleCopy for id : ', id, ' with text.length : ', text.length);
181
} else {
182
this._logService.trace('CopyPasteController#handleCopy');
183
}
184
if (!this._editor.hasTextFocus()) {
185
return;
186
}
187
188
// Explicitly clear the clipboard internal state.
189
// This is needed because on web, the browser clipboard is faked out using an in-memory store.
190
// This means the resources clipboard is not properly updated when copying from the editor.
191
this._clipboardService.clearInternalState?.();
192
193
if (!e.clipboardData || !this.isPasteAsEnabled()) {
194
return;
195
}
196
197
const model = this._editor.getModel();
198
const selections = this._editor.getSelections();
199
if (!model || !selections?.length) {
200
return;
201
}
202
203
const enableEmptySelectionClipboard = this._editor.getOption(EditorOption.emptySelectionClipboard);
204
205
let ranges: readonly IRange[] = selections;
206
const wasFromEmptySelection = selections.length === 1 && selections[0].isEmpty();
207
if (wasFromEmptySelection) {
208
if (!enableEmptySelectionClipboard) {
209
return;
210
}
211
212
ranges = [new Range(ranges[0].startLineNumber, 1, ranges[0].startLineNumber, 1 + model.getLineLength(ranges[0].startLineNumber))];
213
}
214
215
const toCopy = this._editor._getViewModel()?.getPlainTextToCopy(selections, enableEmptySelectionClipboard, platform.isWindows);
216
const multicursorText = Array.isArray(toCopy) ? toCopy : null;
217
218
const defaultPastePayload = {
219
multicursorText,
220
pasteOnNewLine: wasFromEmptySelection,
221
mode: null
222
};
223
224
const providers = this._languageFeaturesService.documentPasteEditProvider
225
.ordered(model)
226
.filter(x => !!x.prepareDocumentPaste);
227
if (!providers.length) {
228
this.setCopyMetadata(e.clipboardData, { defaultPastePayload });
229
return;
230
}
231
232
const dataTransfer = toVSDataTransfer(e.clipboardData);
233
const providerCopyMimeTypes = providers.flatMap(x => x.copyMimeTypes ?? []);
234
235
// Save off a handle pointing to data that VS Code maintains.
236
const handle = id ?? generateUuid();
237
this.setCopyMetadata(e.clipboardData, {
238
id: handle,
239
providerCopyMimeTypes,
240
defaultPastePayload
241
});
242
243
const operations = providers.map((provider): CopyOperation => {
244
return {
245
providerMimeTypes: provider.copyMimeTypes,
246
operation: createCancelablePromise(token =>
247
provider.prepareDocumentPaste!(model, ranges, dataTransfer, token)
248
.catch(err => {
249
console.error(err);
250
return undefined;
251
}))
252
};
253
});
254
255
CopyPasteController._currentCopyOperation?.operations.forEach(entry => entry.operation.cancel());
256
CopyPasteController._currentCopyOperation = { handle, operations };
257
}
258
259
private async handlePaste(e: ClipboardEvent) {
260
if (e.clipboardData) {
261
const [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData);
262
const metadataComputed = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text);
263
this._logService.trace('CopyPasteController#handlePaste for id : ', metadataComputed?.id);
264
} else {
265
this._logService.trace('CopyPasteController#handlePaste');
266
}
267
if (!e.clipboardData || !this._editor.hasTextFocus()) {
268
return;
269
}
270
271
MessageController.get(this._editor)?.closeMessage();
272
this._currentPasteOperation?.cancel();
273
this._currentPasteOperation = undefined;
274
275
const model = this._editor.getModel();
276
const selections = this._editor.getSelections();
277
if (!selections?.length || !model) {
278
return;
279
}
280
281
if (
282
this._editor.getOption(EditorOption.readOnly) // Never enabled if editor is readonly.
283
|| (!this.isPasteAsEnabled() && !this._pasteAsActionContext) // Or feature disabled (but still enable if paste was explicitly requested)
284
) {
285
return;
286
}
287
288
const metadata = this.fetchCopyMetadata(e);
289
this._logService.trace('CopyPasteController#handlePaste with metadata : ', metadata?.id, ' and text.length : ', e.clipboardData.getData('text/plain').length);
290
const dataTransfer = toExternalVSDataTransfer(e.clipboardData);
291
dataTransfer.delete(vscodeClipboardMime);
292
293
const fileTypes = Array.from(e.clipboardData.files).map(file => file.type);
294
295
const allPotentialMimeTypes = [
296
...e.clipboardData.types,
297
...fileTypes,
298
...metadata?.providerCopyMimeTypes ?? [],
299
// TODO: always adds `uri-list` because this get set if there are resources in the system clipboard.
300
// However we can only check the system clipboard async. For this early check, just add it in.
301
// We filter providers again once we have the final dataTransfer we will use.
302
Mimes.uriList,
303
];
304
305
const allProviders = this._languageFeaturesService.documentPasteEditProvider
306
.ordered(model)
307
.filter(provider => {
308
// Filter out providers that don't match the requested paste types
309
const preference = this._pasteAsActionContext?.preferred;
310
if (preference) {
311
if (!this.providerMatchesPreference(provider, preference)) {
312
return false;
313
}
314
}
315
316
// And providers that don't handle any of mime types in the clipboard
317
return provider.pasteMimeTypes?.some(type => matchesMimeType(type, allPotentialMimeTypes));
318
});
319
if (!allProviders.length) {
320
if (this._pasteAsActionContext?.preferred) {
321
this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext.preferred);
322
323
// Also prevent default paste from applying
324
e.preventDefault();
325
e.stopImmediatePropagation();
326
}
327
return;
328
}
329
330
// Prevent the editor's default paste handler from running.
331
// Note that after this point, we are fully responsible for handling paste.
332
// If we can't provider a paste for any reason, we need to explicitly delegate pasting back to the editor.
333
e.preventDefault();
334
e.stopImmediatePropagation();
335
336
if (this._pasteAsActionContext) {
337
this.showPasteAsPick(this._pasteAsActionContext.preferred, allProviders, selections, dataTransfer, metadata);
338
} else {
339
this.doPasteInline(allProviders, selections, dataTransfer, metadata, e);
340
}
341
}
342
343
private showPasteAsNoEditMessage(selections: readonly Selection[], preference: PastePreference) {
344
const kindLabel = 'only' in preference
345
? preference.only.value
346
: 'preferences' in preference
347
? (preference.preferences.length ? preference.preferences.map(preference => preference.value).join(', ') : localize('noPreferences', "empty"))
348
: preference.providerId;
349
350
MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", kindLabel), selections[0].getStartPosition());
351
}
352
353
private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void {
354
this._logService.trace('CopyPasteController#doPasteInline');
355
const editor = this._editor;
356
if (!editor.hasModel()) {
357
return;
358
}
359
360
const editorStateCts = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined);
361
362
const p = createCancelablePromise(async (pToken) => {
363
const editor = this._editor;
364
if (!editor.hasModel()) {
365
return;
366
}
367
const model = editor.getModel();
368
369
const disposables = new DisposableStore();
370
const cts = disposables.add(new CancellationTokenSource(pToken));
371
disposables.add(editorStateCts.token.onCancellationRequested(() => cts.cancel()));
372
373
const token = cts.token;
374
try {
375
await this.mergeInDataFromCopy(allProviders, dataTransfer, metadata, token);
376
if (token.isCancellationRequested) {
377
return;
378
}
379
380
const supportedProviders = allProviders.filter(provider => this.isSupportedPasteProvider(provider, dataTransfer));
381
if (!supportedProviders.length
382
|| (supportedProviders.length === 1 && supportedProviders[0] instanceof DefaultTextPasteOrDropEditProvider) // Only our default text provider is active
383
) {
384
return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent);
385
}
386
387
const context: DocumentPasteContext = {
388
triggerKind: DocumentPasteTriggerKind.Automatic,
389
};
390
391
const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token);
392
disposables.add(editSession);
393
if (token.isCancellationRequested) {
394
return;
395
}
396
397
// If the only edit returned is our default text edit, use the default paste handler
398
if (editSession.edits.length === 1 && editSession.edits[0].provider instanceof DefaultTextPasteOrDropEditProvider) {
399
return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent);
400
}
401
402
if (editSession.edits.length) {
403
const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste';
404
return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: this.getInitialActiveEditIndex(model, editSession.edits), allEdits: editSession.edits }, canShowWidget, async (edit, resolveToken) => {
405
if (!edit.provider.resolveDocumentPasteEdit) {
406
return edit;
407
}
408
409
const resolveP = edit.provider.resolveDocumentPasteEdit(edit, resolveToken);
410
const showP = new DeferredPromise<void>();
411
const resolved = await this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('resolveProcess', "Resolving paste edit for '{0}'. Click to cancel", edit.title), raceCancellation(Promise.race([showP.p, resolveP]), resolveToken), {
412
cancel: () => showP.cancel()
413
}, 0);
414
415
if (resolved) {
416
edit.insertText = resolved.insertText;
417
edit.additionalEdit = resolved.additionalEdit;
418
}
419
return edit;
420
}, token);
421
}
422
423
await this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent);
424
} finally {
425
disposables.dispose();
426
if (this._currentPasteOperation === p) {
427
this._currentPasteOperation = undefined;
428
}
429
}
430
});
431
432
this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel and do basic paste"), p, {
433
cancel: async () => {
434
p.cancel();
435
if (editorStateCts.token.isCancellationRequested) {
436
return;
437
}
438
439
await this.applyDefaultPasteHandler(dataTransfer, metadata, editorStateCts.token, clipboardEvent);
440
}
441
}).finally(() => {
442
editorStateCts.dispose();
443
});
444
this._currentPasteOperation = p;
445
}
446
447
private showPasteAsPick(preference: PastePreference | undefined, allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void {
448
this._logService.trace('CopyPasteController#showPasteAsPick');
449
const p = createCancelablePromise(async (token) => {
450
const editor = this._editor;
451
if (!editor.hasModel()) {
452
return;
453
}
454
const model = editor.getModel();
455
456
const disposables = new DisposableStore();
457
const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token));
458
try {
459
await this.mergeInDataFromCopy(allProviders, dataTransfer, metadata, tokenSource.token);
460
if (tokenSource.token.isCancellationRequested) {
461
return;
462
}
463
464
// Filter out any providers the don't match the full data transfer we will send them.
465
let supportedProviders = allProviders.filter(provider => this.isSupportedPasteProvider(provider, dataTransfer, preference));
466
if (preference) {
467
// We are looking for a specific edit
468
supportedProviders = supportedProviders.filter(provider => this.providerMatchesPreference(provider, preference));
469
}
470
471
const context: DocumentPasteContext = {
472
triggerKind: DocumentPasteTriggerKind.PasteAs,
473
only: preference && 'only' in preference ? preference.only : undefined,
474
};
475
let editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token));
476
if (tokenSource.token.isCancellationRequested) {
477
return;
478
}
479
480
// Filter out any edits that don't match the requested kind
481
if (preference) {
482
editSession = {
483
edits: editSession.edits.filter(edit => {
484
if ('only' in preference) {
485
return preference.only.contains(edit.kind);
486
} else if ('preferences' in preference) {
487
return preference.preferences.some(preference => preference.contains(edit.kind));
488
} else {
489
return preference.providerId === edit.provider.id;
490
}
491
}),
492
dispose: editSession.dispose
493
};
494
}
495
496
if (!editSession.edits.length) {
497
if (preference) {
498
this.showPasteAsNoEditMessage(selections, preference);
499
}
500
return;
501
}
502
503
let pickedEdit: DocumentPasteEdit | undefined;
504
if (preference) {
505
pickedEdit = editSession.edits.at(0);
506
} else {
507
type ItemWithEdit = IQuickPickItem & { edit?: DocumentPasteEdit };
508
const configureDefaultItem: ItemWithEdit = {
509
id: 'editor.pasteAs.default',
510
label: localize('pasteAsDefault', "Configure default paste action"),
511
edit: undefined,
512
};
513
514
const selected = await this._quickInputService.pick<ItemWithEdit>(
515
[
516
...editSession.edits.map((edit): ItemWithEdit => ({
517
label: edit.title,
518
description: edit.kind?.value,
519
edit,
520
})),
521
...(CopyPasteController._configureDefaultAction ? [
522
upcast<IQuickPickSeparator>({ type: 'separator' }),
523
{
524
label: CopyPasteController._configureDefaultAction.label,
525
edit: undefined,
526
}
527
] : [])
528
], {
529
placeHolder: localize('pasteAsPickerPlaceholder', "Select Paste Action"),
530
});
531
532
if (selected === configureDefaultItem) {
533
CopyPasteController._configureDefaultAction?.run();
534
return;
535
}
536
537
pickedEdit = selected?.edit;
538
}
539
540
if (!pickedEdit) {
541
return;
542
}
543
544
const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, selections, pickedEdit);
545
await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor });
546
} finally {
547
disposables.dispose();
548
if (this._currentPasteOperation === p) {
549
this._currentPasteOperation = undefined;
550
}
551
}
552
});
553
554
this._progressService.withProgress({
555
location: ProgressLocation.Window,
556
title: localize('pasteAsProgress', "Running paste handlers"),
557
}, () => p);
558
}
559
560
private setCopyMetadata(dataTransfer: DataTransfer, metadata: CopyMetadata) {
561
this._logService.trace('CopyPasteController#setCopyMetadata new id : ', metadata.id);
562
dataTransfer.setData(vscodeClipboardMime, JSON.stringify(metadata));
563
}
564
565
private fetchCopyMetadata(e: ClipboardEvent): CopyMetadata | undefined {
566
this._logService.trace('CopyPasteController#fetchCopyMetadata');
567
if (!e.clipboardData) {
568
return;
569
}
570
571
// Prefer using the clipboard data we saved off
572
const rawMetadata = e.clipboardData.getData(vscodeClipboardMime);
573
if (rawMetadata) {
574
try {
575
return JSON.parse(rawMetadata);
576
} catch {
577
return undefined;
578
}
579
}
580
581
// Otherwise try to extract the generic text editor metadata
582
const [_, metadata] = ClipboardEventUtils.getTextData(e.clipboardData);
583
if (metadata) {
584
return {
585
defaultPastePayload: {
586
mode: metadata.mode,
587
multicursorText: metadata.multicursorText ?? null,
588
pasteOnNewLine: !!metadata.isFromEmptySelection,
589
},
590
};
591
}
592
593
return undefined;
594
}
595
596
private async mergeInDataFromCopy(allProviders: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken): Promise<void> {
597
this._logService.trace('CopyPasteController#mergeInDataFromCopy with metadata : ', metadata?.id);
598
if (metadata?.id && CopyPasteController._currentCopyOperation?.handle === metadata.id) {
599
// Only resolve providers that have data we may care about
600
const toResolve = CopyPasteController._currentCopyOperation.operations
601
.filter(op => allProviders.some(provider => provider.pasteMimeTypes.some(type => matchesMimeType(type, op.providerMimeTypes))))
602
.map(op => op.operation);
603
604
const toMergeResults = await Promise.all(toResolve);
605
if (token.isCancellationRequested) {
606
return;
607
}
608
609
// Values from higher priority providers should overwrite values from lower priority ones.
610
// Reverse the array to so that the calls to `DataTransfer.replace` later will do this
611
for (const toMergeData of toMergeResults.reverse()) {
612
if (toMergeData) {
613
for (const [key, value] of toMergeData) {
614
dataTransfer.replace(key, value);
615
}
616
}
617
}
618
}
619
620
if (!dataTransfer.has(Mimes.uriList)) {
621
const resources = await this._clipboardService.readResources();
622
if (token.isCancellationRequested) {
623
return;
624
}
625
626
if (resources.length) {
627
dataTransfer.append(Mimes.uriList, createStringDataTransferItem(UriList.create(resources)));
628
}
629
}
630
}
631
632
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise<DocumentPasteWithProviderEditsSession> {
633
const disposables = new DisposableStore();
634
635
const results = await raceCancellation(
636
Promise.all(providers.map(async provider => {
637
try {
638
const edits = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token);
639
if (edits) {
640
disposables.add(edits);
641
}
642
return edits?.edits?.map(edit => ({ ...edit, provider }));
643
} catch (err) {
644
if (!isCancellationError(err)) {
645
console.error(err);
646
}
647
return undefined;
648
}
649
})),
650
token);
651
const edits = coalesce(results ?? []).flat().filter(edit => {
652
return !context.only || context.only.contains(edit.kind);
653
});
654
return {
655
edits: sortEditsByYieldTo(edits),
656
dispose: () => disposables.dispose()
657
};
658
}
659
660
private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent) {
661
const textDataTransfer = dataTransfer.get(Mimes.text) ?? dataTransfer.get('text');
662
const text = (await textDataTransfer?.asString()) ?? '';
663
if (token.isCancellationRequested) {
664
return;
665
}
666
667
const payload: PastePayload = {
668
clipboardEvent,
669
text,
670
pasteOnNewLine: metadata?.defaultPastePayload.pasteOnNewLine ?? false,
671
multicursorText: metadata?.defaultPastePayload.multicursorText ?? null,
672
mode: null,
673
};
674
this._logService.trace('CopyPasteController#applyDefaultPasteHandler for id : ', metadata?.id);
675
this._editor.trigger('keyboard', Handler.Paste, payload);
676
}
677
678
/**
679
* Filter out providers if they:
680
* - Don't handle any of the data transfer types we have
681
* - Don't match the preferred paste kind
682
*/
683
private isSupportedPasteProvider(provider: DocumentPasteEditProvider, dataTransfer: VSDataTransfer, preference?: PastePreference): boolean {
684
if (!provider.pasteMimeTypes?.some(type => dataTransfer.matches(type))) {
685
return false;
686
}
687
688
return !preference || this.providerMatchesPreference(provider, preference);
689
}
690
691
private providerMatchesPreference(provider: DocumentPasteEditProvider, preference: PastePreference): boolean {
692
if ('only' in preference) {
693
return provider.providedPasteEditKinds.some(providedKind => preference.only.contains(providedKind));
694
} else if ('preferences' in preference) {
695
return preference.preferences.some(providedKind => preference.preferences.some(preferredKind => preferredKind.contains(providedKind)));
696
} else {
697
return provider.id === preference.providerId;
698
}
699
}
700
701
private getInitialActiveEditIndex(model: ITextModel, edits: readonly DocumentPasteEdit[]): number {
702
const preferredProviders = this._configService.getValue<PreferredPasteConfiguration[]>(pasteAsPreferenceConfig, { resource: model.uri });
703
for (const config of Array.isArray(preferredProviders) ? preferredProviders : []) {
704
const desiredKind = new HierarchicalKind(config);
705
const editIndex = edits.findIndex(edit => desiredKind.contains(edit.kind));
706
if (editIndex >= 0) {
707
return editIndex;
708
}
709
}
710
711
return 0;
712
}
713
}
714
715