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