Skip to content

Commit ca51a4b

Browse files
authored
Add paste as command (microsoft#181959)
For microsoft#30066 This command shows a quick pick that lets you select how to paste content
1 parent 4182ec7 commit ca51a4b

File tree

14 files changed

+223
-33
lines changed

14 files changed

+223
-33
lines changed

extensions/ipynb/src/notebookImagePaste.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function getImageMimeType(uri: vscode.Uri): string | undefined {
4545
return imageExtToMime.get(extname(uri.fsPath).toLowerCase());
4646
}
4747

48+
const id = 'insertAttachment';
4849
class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
4950

5051
async provideDocumentPasteEdits(
@@ -63,7 +64,7 @@ class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
6364
return;
6465
}
6566

66-
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment'));
67+
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, id, vscode.l10n.t('Insert Image as Attachment'));
6768
pasteEdit.additionalEdit = insert.additionalEdit;
6869
return pasteEdit;
6970
}
@@ -83,6 +84,7 @@ class DropEditProvider implements vscode.DocumentDropEditProvider {
8384
}
8485

8586
const dropEdit = new vscode.DocumentDropEdit(insert.insertText);
87+
dropEdit.id = id;
8688
dropEdit.additionalEdit = insert.additionalEdit;
8789
dropEdit.label = vscode.l10n.t('Insert Image as Attachment');
8890
return dropEdit;
@@ -302,7 +304,6 @@ export function notebookImagePasteSetup(): vscode.Disposable {
302304
],
303305
}),
304306
vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new DropEditProvider(), {
305-
id: 'imageAttachment',
306307
dropMimeTypes: [
307308
...Object.values(imageExtToMime),
308309
MimeType.uriList,

extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const supportedImageMimes = new Set([
1919

2020
class PasteEditProvider implements vscode.DocumentPasteEditProvider {
2121

22+
private readonly _id = 'insertLink';
23+
2224
async provideDocumentPasteEdits(
2325
document: vscode.TextDocument,
2426
_ranges: readonly vscode.Range[],
@@ -36,7 +38,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
3638
}
3739

3840
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
39-
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, snippet.label) : undefined;
41+
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label) : undefined;
4042
}
4143

4244
private async _makeCreateImagePasteEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
@@ -87,7 +89,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
8789
return;
8890
}
8991

90-
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, snippet.label);
92+
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, '', snippet.label);
9193
pasteEdit.additionalEdit = workspaceEdit;
9294
return pasteEdit;
9395
}

extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector)
4343
}
4444

4545
const edit = new vscode.DocumentDropEdit(snippet.snippet);
46+
edit.id = 'insertLink';
4647
edit.label = snippet.label;
4748
return edit;
4849
}
4950
}, {
50-
id: 'insertLink',
5151
dropMimeTypes: [
5252
'text/uri-list'
5353
]

src/vs/editor/common/languages.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,9 @@ export interface CodeActionProvider {
783783
* @internal
784784
*/
785785
export interface DocumentPasteEdit {
786+
readonly id: string;
786787
readonly label: string;
788+
readonly detail: string;
787789
insertText: string | { readonly snippet: string };
788790
additionalEdit?: WorkspaceEdit;
789791
}
@@ -1944,8 +1946,8 @@ export enum ExternalUriOpenerPriority {
19441946
* @internal
19451947
*/
19461948
export interface DocumentOnDropEdit {
1949+
readonly id: string;
19471950
readonly label: string;
1948-
19491951
insertText: string | { readonly snippet: string };
19501952
additionalEdit?: WorkspaceEdit;
19511953
}
@@ -1954,7 +1956,6 @@ export interface DocumentOnDropEdit {
19541956
* @internal
19551957
*/
19561958
export interface DocumentOnDropEditProvider {
1957-
readonly id: string;
19581959
readonly dropMimeTypes?: readonly string[];
19591960

19601961
provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): ProviderResult<DocumentOnDropEdit>;

src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55

66
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
77
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
8-
import { EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
8+
import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
99
import { registerEditorFeature } from 'vs/editor/common/editorFeatures';
1010
import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController';
1111
import { DefaultPasteProvidersFeature } from 'vs/editor/contrib/dropOrPasteInto/browser/defaultProviders';
12+
import * as nls from 'vs/nls';
1213
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
1314

1415
registerEditorContribution(CopyPasteController.ID, CopyPasteController, EditorContributionInstantiation.Eager); // eager because it listens to events on the container dom node of the editor
1516

17+
registerEditorFeature(DefaultPasteProvidersFeature);
18+
1619
registerEditorCommand(new class extends EditorCommand {
1720
constructor() {
1821
super({
@@ -26,8 +29,37 @@ registerEditorCommand(new class extends EditorCommand {
2629
}
2730

2831
public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor, _args: any) {
29-
CopyPasteController.get(editor)?.changePasteType();
32+
return CopyPasteController.get(editor)?.changePasteType();
3033
}
3134
});
3235

33-
registerEditorFeature(DefaultPasteProvidersFeature);
36+
registerEditorAction(class extends EditorAction {
37+
constructor() {
38+
super({
39+
id: 'editor.action.pasteAs',
40+
label: nls.localize('pasteAs', "Paste As..."),
41+
alias: 'Paste As...',
42+
precondition: undefined,
43+
description: {
44+
description: 'Paste as',
45+
args: [{
46+
name: 'args',
47+
schema: {
48+
type: 'object',
49+
properties: {
50+
'id': {
51+
type: 'string',
52+
description: nls.localize('pasteAs.id', "The id of the paste edit to try applying. If not provided, the editor will show a picker."),
53+
}
54+
},
55+
}
56+
}]
57+
}
58+
});
59+
}
60+
61+
public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any) {
62+
const id = typeof args?.id === 'string' ? args.id : undefined;
63+
return CopyPasteController.get(editor)?.pasteAs(id);
64+
}
65+
});

src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,23 @@ import * as platform from 'vs/base/common/platform';
1414
import { generateUuid } from 'vs/base/common/uuid';
1515
import { toExternalVSDataTransfer, toVSDataTransfer } from 'vs/editor/browser/dnd';
1616
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
17+
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
1718
import { EditorOption } from 'vs/editor/common/config/editorOptions';
1819
import { IRange, Range } from 'vs/editor/common/core/range';
1920
import { Selection } from 'vs/editor/common/core/selection';
2021
import { Handler, IEditorContribution, PastePayload } from 'vs/editor/common/editorCommon';
2122
import { DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages';
2223
import { ITextModel } from 'vs/editor/common/model';
2324
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
25+
import { createCombinedWorkspaceEdit } from 'vs/editor/contrib/dropOrPasteInto/browser/edit';
2426
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState';
2527
import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress';
2628
import { localize } from 'vs/nls';
2729
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
2830
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
2931
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
32+
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
33+
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
3034
import { PostEditWidgetManager } from './postEditWidget';
3135

3236
export const changePasteTypeCommandId = 'editor.changePasteType';
@@ -58,15 +62,19 @@ export class CopyPasteController extends Disposable implements IEditorContributi
5862
};
5963

6064
private _currentPasteOperation?: CancelablePromise<void>;
65+
private _pasteAsActionContext?: { readonly preferredId: string | undefined };
6166

6267
private readonly _pasteProgressManager: InlineProgressManager;
6368
private readonly _postPasteWidgetManager: PostEditWidgetManager;
6469

6570
constructor(
6671
editor: ICodeEditor,
6772
@IInstantiationService instantiationService: IInstantiationService,
73+
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
6874
@IClipboardService private readonly _clipboardService: IClipboardService,
6975
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
76+
@IQuickInputService private readonly _quickInputService: IQuickInputService,
77+
@IProgressService private readonly _progressService: IProgressService,
7078
) {
7179
super();
7280

@@ -86,6 +94,16 @@ export class CopyPasteController extends Disposable implements IEditorContributi
8694
this._postPasteWidgetManager.tryShowSelector();
8795
}
8896

97+
public pasteAs(preferredId?: string) {
98+
this._editor.focus();
99+
try {
100+
this._pasteAsActionContext = { preferredId };
101+
document.execCommand('paste');
102+
} finally {
103+
this._pasteAsActionContext = undefined;
104+
}
105+
}
106+
89107
public clearWidgets() {
90108
this._postPasteWidgetManager.clear();
91109
}
@@ -208,11 +226,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi
208226
e.preventDefault();
209227
e.stopImmediatePropagation();
210228

229+
if (this._pasteAsActionContext) {
230+
this.showPasteAsPick(this._pasteAsActionContext.preferredId, allProviders, selections, dataTransfer, metadata);
231+
} else {
232+
this.doPasteInline(allProviders, selections, dataTransfer, metadata);
233+
}
234+
}
235+
236+
private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void {
211237
const p = createCancelablePromise(async (token) => {
212238
const editor = this._editor;
213239
if (!editor.hasModel()) {
214240
return;
215241
}
242+
const model = editor.getModel();
216243

217244
const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
218245
try {
@@ -253,6 +280,71 @@ export class CopyPasteController extends Disposable implements IEditorContributi
253280
this._currentPasteOperation = p;
254281
}
255282

283+
private showPasteAsPick(preferredId: string | undefined, allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void {
284+
const p = createCancelablePromise(async (token) => {
285+
const editor = this._editor;
286+
if (!editor.hasModel()) {
287+
return;
288+
}
289+
const model = editor.getModel();
290+
291+
const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
292+
try {
293+
await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token);
294+
if (tokenSource.token.isCancellationRequested) {
295+
return;
296+
}
297+
298+
// Filter out any providers the don't match the full data transfer we will send them.
299+
const supportedProviders = allProviders.filter(provider => isSupportedProvider(provider, dataTransfer));
300+
301+
const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, tokenSource.token);
302+
if (tokenSource.token.isCancellationRequested) {
303+
return;
304+
}
305+
306+
if (!providerEdits.length) {
307+
return;
308+
}
309+
310+
let pickedEdit: DocumentPasteEdit | undefined;
311+
if (typeof preferredId === 'string') {
312+
// We are looking for a specific edit
313+
pickedEdit = providerEdits.find(edit => edit.id === preferredId);
314+
} else {
315+
const selected = await this._quickInputService.pick(
316+
providerEdits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({
317+
label: edit.label,
318+
description: edit.id,
319+
detail: edit.detail,
320+
edit,
321+
})), {
322+
placeHolder: localize('pasteAsPickerPlaceholder', "Select Paste Action"),
323+
});
324+
pickedEdit = selected?.edit;
325+
}
326+
327+
if (!pickedEdit) {
328+
return;
329+
}
330+
331+
const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, selections, pickedEdit);
332+
await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor });
333+
} finally {
334+
tokenSource.dispose();
335+
if (this._currentPasteOperation === p) {
336+
this._currentPasteOperation = undefined;
337+
}
338+
}
339+
});
340+
341+
this._progressService.withProgress({
342+
location: ProgressLocation.Window,
343+
title: localize('pasteAsProgress', "Running paste handlers"),
344+
}, () => p);
345+
}
346+
347+
256348
private setCopyMetadata(dataTransfer: DataTransfer, metadata: CopyMetadata) {
257349
dataTransfer.setData(vscodeClipboardMime, JSON.stringify(metadata));
258350
}
@@ -293,7 +385,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
293385
}
294386
}
295387

296-
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
388+
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
297389
const result = await raceCancellation(
298390
Promise.all(
299391
providers.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token))

src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat
1919
import { localize } from 'vs/nls';
2020
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
2121

22+
const builtInLabel = localize('builtIn', 'Built-in');
23+
2224
abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, DocumentPasteEditProvider {
2325

2426
abstract readonly id: string;
@@ -27,12 +29,12 @@ abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider,
2729

2830
async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
2931
const edit = await this.getEdit(dataTransfer, token);
30-
return edit ? { insertText: edit.insertText, label: edit.label } : undefined;
32+
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, detail: edit.detail } : undefined;
3133
}
3234

3335
async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentOnDropEdit | undefined> {
3436
const edit = await this.getEdit(dataTransfer, token);
35-
return edit ? { insertText: edit.insertText, label: edit.label } : undefined;
37+
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label } : undefined;
3638
}
3739

3840
protected abstract getEdit(dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
@@ -58,7 +60,9 @@ class DefaultTextProvider extends SimplePasteAndDropProvider {
5860

5961
const insertText = await textEntry.asString();
6062
return {
63+
id: this.id,
6164
label: localize('text.label', "Insert Plain Text"),
65+
detail: builtInLabel,
6266
insertText
6367
};
6468
}
@@ -101,7 +105,12 @@ class PathProvider extends SimplePasteAndDropProvider {
101105
: localize('defaultDropProvider.uriList.path', "Insert Path");
102106
}
103107

104-
return { insertText, label };
108+
return {
109+
id: this.id,
110+
insertText,
111+
label,
112+
detail: builtInLabel,
113+
};
105114
}
106115
}
107116

@@ -133,10 +142,12 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
133142
}
134143

135144
return {
145+
id: this.id,
136146
insertText: relativeUris.join(' '),
137147
label: entries.length > 1
138148
? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths")
139-
: localize('defaultDropProvider.uriList.relativePath', "Insert Relative Path")
149+
: localize('defaultDropProvider.uriList.relativePath', "Insert Relative Path"),
150+
detail: builtInLabel,
140151
};
141152
}
142153
}

0 commit comments

Comments
 (0)