Skip to content

Commit

Permalink
Add widget to change how content is pasted (#181290)
Browse files Browse the repository at this point in the history
* Add widget to change how content is pasted

For #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
  • Loading branch information
mjbvz authored May 2, 2023
1 parent 166e09e commit 11ca8d7
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 171 deletions.
2 changes: 1 addition & 1 deletion extensions/ipynb/src/notebookImagePaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
return;
}

const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText);
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert image as attachment'));
pasteEdit.additionalEdit = insert.additionalEdit;
return pasteEdit;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
}

const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet) : undefined;
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, snippet.label) : undefined;
}

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

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

const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet);
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, snippet.label);
pasteEdit.additionalEdit = workspaceEdit;
return pasteEdit;
}
Expand Down
3 changes: 2 additions & 1 deletion src/vs/editor/common/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,8 @@ export interface CodeActionProvider {
* @internal
*/
export interface DocumentPasteEdit {
insertText: string | { snippet: string };
readonly label: string;
insertText: string | { readonly snippet: string };
additionalEdit?: WorkspaceEdit;
}

Expand Down
24 changes: 22 additions & 2 deletions src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema';
import { CopyPasteController } from 'vs/editor/contrib/copyPaste/browser/copyPasteController';
import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx } from 'vs/editor/contrib/copyPaste/browser/copyPasteController';
import * as nls from 'vs/nls';
import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';

registerEditorContribution(CopyPasteController.ID, CopyPasteController, EditorContributionInstantiation.Eager); // eager because it listens to events on the container dom node of the editor
Expand All @@ -23,3 +26,20 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat
},
}
});

registerEditorCommand(new class extends EditorCommand {
constructor() {
super({
id: changePasteTypeCommandId,
precondition: pasteWidgetVisibleCtx,
kbOpts: {
weight: KeybindingWeight.EditorContrib,
primary: KeyMod.CtrlCmd | KeyCode.Period,
}
});
}

public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor, _args: any) {
CopyPasteController.get(editor)?.changePasteType();
}
});
89 changes: 39 additions & 50 deletions src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { addDisposableListener } from 'vs/base/browser/dom';
import { coalesce } from 'vs/base/common/arrays';
import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { UriList, VSDataTransfer, createStringDataTransferItem } from 'vs/base/common/dataTransfer';
Expand All @@ -13,22 +14,27 @@ import { Schemas } from 'vs/base/common/network';
import { generateUuid } from 'vs/base/common/uuid';
import { toVSDataTransfer } from 'vs/editor/browser/dnd';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { Handler, IEditorContribution, PastePayload } from 'vs/editor/common/editorCommon';
import { DocumentPasteEdit, DocumentPasteEditProvider, WorkspaceEdit } from 'vs/editor/common/languages';
import { DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { registerDefaultPasteProviders } from 'vs/editor/contrib/copyPaste/browser/defaultPasteProviders';
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState';
import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress';
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { PostEditWidgetManager } from 'vs/editor/contrib/postEditWidget/browser/postEditWidget';
import { localize } from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';

export const changePasteTypeCommandId = 'editor.changePasteType';

export const pasteWidgetVisibleCtx = new RawContextKey<boolean>('pasteWidgetVisible', false, localize('pasteWidgetVisible', "Whether the paste widget is showing"));

const vscodeClipboardMime = 'application/vnd.code.copyMetadata';

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

private operationIdPool = 0;
private _currentOperation?: { readonly id: number; readonly promise: CancelablePromise<void> };
private _currentOperation?: CancelablePromise<void>;

private readonly _pasteProgressManager: InlineProgressManager;
private readonly _postPasteWidgetManager: PostEditWidgetManager;

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

this._pasteProgressManager = this._register(new InlineProgressManager('pasteIntoEditor', editor, instantiationService));

this._postPasteWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'pasteIntoEditor', editor, pasteWidgetVisibleCtx, { id: changePasteTypeCommandId, label: localize('postPasteWidgetTitle', "Show paste options...") }));

registerDefaultPasteProviders(_languageFeaturesService);
}

public changePasteType() {
this._postPasteWidgetManager.tryShowSelector();
}

public clearWidgets() {
this._postPasteWidgetManager.clear();
}

private arePasteActionsEnabled(model: ITextModel): boolean {
Expand Down Expand Up @@ -152,9 +169,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
return;
}

const operationId = this.operationIdPool++;
this._currentOperation?.promise.cancel();
this._pasteProgressManager.clear();
this._currentOperation?.cancel();

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


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

const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
try {
this._pasteProgressManager.setAtPosition(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel"), {
cancel: () => tokenSource.cancel()
});

const dataTransfer = toVSDataTransfer(e.clipboardData!);

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

dataTransfer.delete(vscodeClipboardMime);

const providerEdit = await this.getProviderPasteEdit(providers, dataTransfer, model, selections, tokenSource.token);
const providerEdits = await this.getPasteEdits(providers, dataTransfer, model, selections, tokenSource.token);
if (tokenSource.token.isCancellationRequested) {
return;
}

if (providerEdit) {
const snippet = typeof providerEdit.insertText === 'string' ? SnippetParser.escape(providerEdit.insertText) : providerEdit.insertText.snippet;
const combinedWorkspaceEdit: WorkspaceEdit = {
edits: [
new ResourceTextEdit(model.uri, {
range: Selection.liftSelection(editor.getSelection()),
text: snippet,
insertAsSnippet: true,
}),
...(providerEdit.additionalEdit?.edits ?? [])
]
};
await this._bulkEditService.apply(combinedWorkspaceEdit, { editor });
return;
if (providerEdits.length) {
return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections[0], { activeEditIndex: 0, allEdits: providerEdits }, tokenSource.token);
}

await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token);
} finally {
tokenSource.dispose();
if (this._currentOperation?.id === operationId) {
this._pasteProgressManager.clear();
if (this._currentOperation === p) {
this._currentOperation = undefined;
}
}
});

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

private getProviderPasteEdit(providers: DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: Selection[], token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
return raceCancellation((async () => {
for (const provider of providers) {
if (token.isCancellationRequested) {
return;
}

if (!isSupportedProvider(provider, dataTransfer)) {
continue;
}

const edit = await provider.provideDocumentPasteEdits(model, selections, dataTransfer, token);
if (edit) {
return edit;
}
}
return undefined;
})(), token);
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
const result = await raceCancellation(
Promise.all(
providers
.filter(provider => isSupportedProvider(provider, dataTransfer))
.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token))
).then(coalesce),
token);
return result ?? [];
}

private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken) {
Expand Down
45 changes: 45 additions & 0 deletions src/vs/editor/contrib/copyPaste/browser/defaultPasteProviders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from 'vs/base/common/cancellation';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Mimes } from 'vs/base/common/mime';
import { IRange } from 'vs/editor/common/core/range';
import { DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { localize } from 'vs/nls';

class DefaultTextPasteProvider implements DocumentPasteEditProvider {

readonly id = 'text';
readonly pasteMimeTypes = [Mimes.text, 'text'];

async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, _token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
const textEntry = dataTransfer.get('text') ?? dataTransfer.get(Mimes.text);
if (!textEntry) {
return;
}

const text = await textEntry.asString();
return {
label: localize('defaultPasteProvider.text.label', "Insert Plain Text"),
insertText: text
};
}
}


let registeredDefaultProviders = false;

export function registerDefaultPasteProviders(
languageFeaturesService: ILanguageFeaturesService
) {
if (!registeredDefaultProviders) {
registeredDefaultProviders = true;

languageFeaturesService.documentPasteEditProvider.register('*', new DefaultTextPasteProvider());
}
}
Loading

0 comments on commit 11ca8d7

Please sign in to comment.