Skip to content

Commit 11ca8d7

Browse files
authored
Add widget to change how content is pasted (microsoft#181290)
* Add widget to change how content is pasted For microsoft#30066 This adds a widget that lets you change how content is pasted if there are multiple ways it could be pasted To do this, I've made the post drop widget generic and reused it for pasting too * Update types * More code deduplication
1 parent 166e09e commit 11ca8d7

File tree

16 files changed

+261
-171
lines changed

16 files changed

+261
-171
lines changed

extensions/ipynb/src/notebookImagePaste.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
5353
return;
5454
}
5555

56-
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText);
56+
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert image as attachment'));
5757
pasteEdit.additionalEdit = insert.additionalEdit;
5858
return pasteEdit;
5959
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
4646
}
4747

4848
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
49-
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet) : undefined;
49+
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, snippet.label) : undefined;
5050
}
5151

5252
private async _makeCreateImagePasteEdit(document: vscode.TextDocument, file: vscode.DataTransferFile, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
@@ -55,7 +55,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
5555
const workspaceFolder = vscode.workspace.getWorkspaceFolder(file.uri);
5656
if (workspaceFolder) {
5757
const snippet = createUriListSnippet(document, [file.uri]);
58-
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet) : undefined;
58+
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, snippet.label) : undefined;
5959
}
6060
}
6161

@@ -73,7 +73,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
7373
const workspaceEdit = new vscode.WorkspaceEdit();
7474
workspaceEdit.createFile(uri, { contents: file });
7575

76-
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet);
76+
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, snippet.label);
7777
pasteEdit.additionalEdit = workspaceEdit;
7878
return pasteEdit;
7979
}

src/vs/editor/common/languages.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,8 @@ export interface CodeActionProvider {
783783
* @internal
784784
*/
785785
export interface DocumentPasteEdit {
786-
insertText: string | { snippet: string };
786+
readonly label: string;
787+
insertText: string | { readonly snippet: string };
787788
additionalEdit?: WorkspaceEdit;
788789
}
789790

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
6+
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
7+
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
8+
import { EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
79
import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema';
8-
import { CopyPasteController } from 'vs/editor/contrib/copyPaste/browser/copyPasteController';
10+
import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx } from 'vs/editor/contrib/copyPaste/browser/copyPasteController';
911
import * as nls from 'vs/nls';
1012
import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
13+
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
1114
import { Registry } from 'vs/platform/registry/common/platform';
1215

1316
registerEditorContribution(CopyPasteController.ID, CopyPasteController, EditorContributionInstantiation.Eager); // eager because it listens to events on the container dom node of the editor
@@ -23,3 +26,20 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat
2326
},
2427
}
2528
});
29+
30+
registerEditorCommand(new class extends EditorCommand {
31+
constructor() {
32+
super({
33+
id: changePasteTypeCommandId,
34+
precondition: pasteWidgetVisibleCtx,
35+
kbOpts: {
36+
weight: KeybindingWeight.EditorContrib,
37+
primary: KeyMod.CtrlCmd | KeyCode.Period,
38+
}
39+
});
40+
}
41+
42+
public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor, _args: any) {
43+
CopyPasteController.get(editor)?.changePasteType();
44+
}
45+
});

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

Lines changed: 39 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { addDisposableListener } from 'vs/base/browser/dom';
7+
import { coalesce } from 'vs/base/common/arrays';
78
import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async';
89
import { CancellationToken } from 'vs/base/common/cancellation';
910
import { UriList, VSDataTransfer, createStringDataTransferItem } from 'vs/base/common/dataTransfer';
@@ -13,22 +14,27 @@ import { Schemas } from 'vs/base/common/network';
1314
import { generateUuid } from 'vs/base/common/uuid';
1415
import { toVSDataTransfer } from 'vs/editor/browser/dnd';
1516
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
16-
import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
1717
import { EditorOption } from 'vs/editor/common/config/editorOptions';
1818
import { IRange, Range } from 'vs/editor/common/core/range';
1919
import { Selection } from 'vs/editor/common/core/selection';
2020
import { Handler, IEditorContribution, PastePayload } from 'vs/editor/common/editorCommon';
21-
import { DocumentPasteEdit, DocumentPasteEditProvider, WorkspaceEdit } from 'vs/editor/common/languages';
21+
import { DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages';
2222
import { ITextModel } from 'vs/editor/common/model';
2323
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
24+
import { registerDefaultPasteProviders } from 'vs/editor/contrib/copyPaste/browser/defaultPasteProviders';
2425
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState';
2526
import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress';
26-
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
27+
import { PostEditWidgetManager } from 'vs/editor/contrib/postEditWidget/browser/postEditWidget';
2728
import { localize } from 'vs/nls';
2829
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
2930
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
31+
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
3032
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
3133

34+
export const changePasteTypeCommandId = 'editor.changePasteType';
35+
36+
export const pasteWidgetVisibleCtx = new RawContextKey<boolean>('pasteWidgetVisible', false, localize('pasteWidgetVisible', "Whether the paste widget is showing"));
37+
3238
const vscodeClipboardMime = 'application/vnd.code.copyMetadata';
3339

3440
interface CopyMetadata {
@@ -51,15 +57,14 @@ export class CopyPasteController extends Disposable implements IEditorContributi
5157
readonly dataTransferPromise: CancelablePromise<VSDataTransfer>;
5258
};
5359

54-
private operationIdPool = 0;
55-
private _currentOperation?: { readonly id: number; readonly promise: CancelablePromise<void> };
60+
private _currentOperation?: CancelablePromise<void>;
5661

5762
private readonly _pasteProgressManager: InlineProgressManager;
63+
private readonly _postPasteWidgetManager: PostEditWidgetManager;
5864

5965
constructor(
6066
editor: ICodeEditor,
6167
@IInstantiationService instantiationService: IInstantiationService,
62-
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
6368
@IClipboardService private readonly _clipboardService: IClipboardService,
6469
@IConfigurationService private readonly _configurationService: IConfigurationService,
6570
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@@ -74,6 +79,18 @@ export class CopyPasteController extends Disposable implements IEditorContributi
7479
this._register(addDisposableListener(container, 'paste', e => this.handlePaste(e), true));
7580

7681
this._pasteProgressManager = this._register(new InlineProgressManager('pasteIntoEditor', editor, instantiationService));
82+
83+
this._postPasteWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'pasteIntoEditor', editor, pasteWidgetVisibleCtx, { id: changePasteTypeCommandId, label: localize('postPasteWidgetTitle', "Show paste options...") }));
84+
85+
registerDefaultPasteProviders(_languageFeaturesService);
86+
}
87+
88+
public changePasteType() {
89+
this._postPasteWidgetManager.tryShowSelector();
90+
}
91+
92+
public clearWidgets() {
93+
this._postPasteWidgetManager.clear();
7794
}
7895

7996
private arePasteActionsEnabled(model: ITextModel): boolean {
@@ -152,9 +169,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
152169
return;
153170
}
154171

155-
const operationId = this.operationIdPool++;
156-
this._currentOperation?.promise.cancel();
157-
this._pasteProgressManager.clear();
172+
this._currentOperation?.cancel();
158173

159174
const selections = this._editor.getSelections();
160175
if (!selections?.length || !this._editor.hasModel()) {
@@ -180,7 +195,6 @@ export class CopyPasteController extends Disposable implements IEditorContributi
180195
e.preventDefault();
181196
e.stopImmediatePropagation();
182197

183-
184198
const p = createCancelablePromise(async (token) => {
185199
const editor = this._editor;
186200
if (!editor.hasModel()) {
@@ -189,10 +203,6 @@ export class CopyPasteController extends Disposable implements IEditorContributi
189203

190204
const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
191205
try {
192-
this._pasteProgressManager.setAtPosition(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel"), {
193-
cancel: () => tokenSource.cancel()
194-
});
195-
196206
const dataTransfer = toVSDataTransfer(e.clipboardData!);
197207

198208
if (metadata?.id && this._currentClipboardItem?.handle === metadata.id) {
@@ -219,58 +229,37 @@ export class CopyPasteController extends Disposable implements IEditorContributi
219229

220230
dataTransfer.delete(vscodeClipboardMime);
221231

222-
const providerEdit = await this.getProviderPasteEdit(providers, dataTransfer, model, selections, tokenSource.token);
232+
const providerEdits = await this.getPasteEdits(providers, dataTransfer, model, selections, tokenSource.token);
223233
if (tokenSource.token.isCancellationRequested) {
224234
return;
225235
}
226236

227-
if (providerEdit) {
228-
const snippet = typeof providerEdit.insertText === 'string' ? SnippetParser.escape(providerEdit.insertText) : providerEdit.insertText.snippet;
229-
const combinedWorkspaceEdit: WorkspaceEdit = {
230-
edits: [
231-
new ResourceTextEdit(model.uri, {
232-
range: Selection.liftSelection(editor.getSelection()),
233-
text: snippet,
234-
insertAsSnippet: true,
235-
}),
236-
...(providerEdit.additionalEdit?.edits ?? [])
237-
]
238-
};
239-
await this._bulkEditService.apply(combinedWorkspaceEdit, { editor });
240-
return;
237+
if (providerEdits.length) {
238+
return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections[0], { activeEditIndex: 0, allEdits: providerEdits }, tokenSource.token);
241239
}
242240

243241
await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token);
244242
} finally {
245243
tokenSource.dispose();
246-
if (this._currentOperation?.id === operationId) {
247-
this._pasteProgressManager.clear();
244+
if (this._currentOperation === p) {
248245
this._currentOperation = undefined;
249246
}
250247
}
251248
});
252249

253-
this._currentOperation = { id: operationId, promise: p };
250+
this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel"), p);
251+
this._currentOperation = p;
254252
}
255253

256-
private getProviderPasteEdit(providers: DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: Selection[], token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
257-
return raceCancellation((async () => {
258-
for (const provider of providers) {
259-
if (token.isCancellationRequested) {
260-
return;
261-
}
262-
263-
if (!isSupportedProvider(provider, dataTransfer)) {
264-
continue;
265-
}
266-
267-
const edit = await provider.provideDocumentPasteEdits(model, selections, dataTransfer, token);
268-
if (edit) {
269-
return edit;
270-
}
271-
}
272-
return undefined;
273-
})(), token);
254+
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
255+
const result = await raceCancellation(
256+
Promise.all(
257+
providers
258+
.filter(provider => isSupportedProvider(provider, dataTransfer))
259+
.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token))
260+
).then(coalesce),
261+
token);
262+
return result ?? [];
274263
}
275264

276265
private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken) {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 { CancellationToken } from 'vs/base/common/cancellation';
7+
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
8+
import { Mimes } from 'vs/base/common/mime';
9+
import { IRange } from 'vs/editor/common/core/range';
10+
import { DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages';
11+
import { ITextModel } from 'vs/editor/common/model';
12+
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
13+
import { localize } from 'vs/nls';
14+
15+
class DefaultTextPasteProvider implements DocumentPasteEditProvider {
16+
17+
readonly id = 'text';
18+
readonly pasteMimeTypes = [Mimes.text, 'text'];
19+
20+
async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, _token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
21+
const textEntry = dataTransfer.get('text') ?? dataTransfer.get(Mimes.text);
22+
if (!textEntry) {
23+
return;
24+
}
25+
26+
const text = await textEntry.asString();
27+
return {
28+
label: localize('defaultPasteProvider.text.label', "Insert Plain Text"),
29+
insertText: text
30+
};
31+
}
32+
}
33+
34+
35+
let registeredDefaultProviders = false;
36+
37+
export function registerDefaultPasteProviders(
38+
languageFeaturesService: ILanguageFeaturesService
39+
) {
40+
if (!registeredDefaultProviders) {
41+
registeredDefaultProviders = true;
42+
43+
languageFeaturesService.documentPasteEditProvider.register('*', new DefaultTextPasteProvider());
44+
}
45+
}

0 commit comments

Comments
 (0)