Skip to content

Commit

Permalink
Add widget to change how content is pasted
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mjbvz committed May 2, 2023
1 parent 1b5fffd commit 7788d9c
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 110 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
1 change: 1 addition & 0 deletions src/vs/editor/common/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ export interface CodeActionProvider {
* @internal
*/
export interface DocumentPasteEdit {
label: string;
insertText: string | { 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();
}
});
100 changes: 65 additions & 35 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,29 @@ 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 { 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 { 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 { PostEditWidgetManager } from 'vs/editor/contrib/postEditWidget/browser/postEditWidget';
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
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 @@ -55,11 +63,11 @@ export class CopyPasteController extends Disposable implements IEditorContributi
private _currentOperation?: { readonly id: number; readonly promise: 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 +82,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 @@ -219,25 +239,14 @@ 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) {
const selection = editor.getSelection();
return this.applyPasteEdit(editor, selection, 0, providerEdits, tokenSource.token);
}

await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token);
Expand All @@ -253,24 +262,15 @@ export class CopyPasteController extends Disposable implements IEditorContributi
this._currentOperation = { id: operationId, promise: 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 All @@ -290,6 +290,36 @@ export class CopyPasteController extends Disposable implements IEditorContributi
multicursorText: null
});
}

private async applyPasteEdit(editor: ICodeEditor, selection: Selection, activeEditIndex: number, allEdits: readonly DocumentPasteEdit[], token: CancellationToken): Promise<void> {
const model = editor.getModel();
if (!model) {
return;
}

const edit = allEdits[activeEditIndex];
if (!edit) {
return;
}

const snippet = typeof edit.insertText === 'string' ? SnippetParser.escape(edit.insertText) : edit.insertText.snippet;

const combinedWorkspaceEdit: WorkspaceEdit = {
edits: [
new ResourceTextEdit(model.uri, {
range: selection,
text: snippet,
insertAsSnippet: true,
}),
...(edit.additionalEdit?.edits ?? [])
]
};

await this._postPasteWidgetManager.applyEditAndShowIfNeeded(selection, combinedWorkspaceEdit, { activeEditIndex, allEdits }, async (newEditIndex) => {
await model.undo();
this.applyPasteEdit(editor, selection, newEditIndex, allEdits, token);
}, token);
}
}

function isSupportedProvider(provider: DocumentPasteEditProvider, dataTransfer: VSDataTransfer): boolean {
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 { DocumentOnDropEdit, 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 DefaultTextDropProvider implements DocumentPasteEditProvider {

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

async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, _token: CancellationToken): Promise<DocumentOnDropEdit | 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 DefaultTextDropProvider());
}
}
Loading

0 comments on commit 7788d9c

Please sign in to comment.