Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paste as command #181959

Merged
merged 5 commits into from May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions extensions/ipynb/src/notebookImagePaste.ts
Expand Up @@ -45,6 +45,7 @@ function getImageMimeType(uri: vscode.Uri): string | undefined {
return imageExtToMime.get(extname(uri.fsPath).toLowerCase());
}

const id = 'insertAttachment';
class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {

async provideDocumentPasteEdits(
Expand All @@ -63,7 +64,7 @@ class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
return;
}

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

const dropEdit = new vscode.DocumentDropEdit(insert.insertText);
dropEdit.id = id;
dropEdit.additionalEdit = insert.additionalEdit;
dropEdit.label = vscode.l10n.t('Insert Image as Attachment');
return dropEdit;
Expand Down Expand Up @@ -302,7 +304,6 @@ export function notebookImagePasteSetup(): vscode.Disposable {
],
}),
vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new DropEditProvider(), {
id: 'imageAttachment',
dropMimeTypes: [
...Object.values(imageExtToMime),
MimeType.uriList,
Expand Down
Expand Up @@ -19,6 +19,8 @@ const supportedImageMimes = new Set([

class PasteEditProvider implements vscode.DocumentPasteEditProvider {

private readonly _id = 'insertLink';

async provideDocumentPasteEdits(
document: vscode.TextDocument,
_ranges: readonly vscode.Range[],
Expand All @@ -36,7 +38,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
}

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

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

const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, snippet.label);
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, '', snippet.label);
pasteEdit.additionalEdit = workspaceEdit;
return pasteEdit;
}
Expand Down
Expand Up @@ -43,11 +43,11 @@ export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector)
}

const edit = new vscode.DocumentDropEdit(snippet.snippet);
edit.id = 'insertLink';
edit.label = snippet.label;
return edit;
}
}, {
id: 'insertLink',
dropMimeTypes: [
'text/uri-list'
]
Expand Down
5 changes: 3 additions & 2 deletions src/vs/editor/common/languages.ts
Expand Up @@ -783,7 +783,9 @@ export interface CodeActionProvider {
* @internal
*/
export interface DocumentPasteEdit {
readonly id: string;
readonly label: string;
readonly detail: string;
insertText: string | { readonly snippet: string };
additionalEdit?: WorkspaceEdit;
}
Expand Down Expand Up @@ -1944,8 +1946,8 @@ export enum ExternalUriOpenerPriority {
* @internal
*/
export interface DocumentOnDropEdit {
readonly id: string;
readonly label: string;

insertText: string | { readonly snippet: string };
additionalEdit?: WorkspaceEdit;
}
Expand All @@ -1954,7 +1956,6 @@ export interface DocumentOnDropEdit {
* @internal
*/
export interface DocumentOnDropEditProvider {
readonly id: string;
readonly dropMimeTypes?: readonly string[];

provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): ProviderResult<DocumentOnDropEdit>;
Expand Down
Expand Up @@ -5,14 +5,17 @@

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 { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { registerEditorFeature } from 'vs/editor/common/editorFeatures';
import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController';
import { DefaultPasteProvidersFeature } from 'vs/editor/contrib/dropOrPasteInto/browser/defaultProviders';
import * as nls from 'vs/nls';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';

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

registerEditorFeature(DefaultPasteProvidersFeature);

registerEditorCommand(new class extends EditorCommand {
constructor() {
super({
Expand All @@ -26,8 +29,37 @@ registerEditorCommand(new class extends EditorCommand {
}

public override runEditorCommand(_accessor: ServicesAccessor | null, editor: ICodeEditor, _args: any) {
CopyPasteController.get(editor)?.changePasteType();
return CopyPasteController.get(editor)?.changePasteType();
}
});

registerEditorFeature(DefaultPasteProvidersFeature);
registerEditorAction(class extends EditorAction {
constructor() {
super({
id: 'editor.action.pasteAs',
label: nls.localize('pasteAs', "Paste As..."),
alias: 'Paste As...',
precondition: undefined,
description: {
description: 'Paste as',
args: [{
name: 'args',
schema: {
type: 'object',
properties: {
'id': {
mjbvz marked this conversation as resolved.
Show resolved Hide resolved
type: 'string',
mjbvz marked this conversation as resolved.
Show resolved Hide resolved
description: nls.localize('pasteAs.id', "The id of the paste edit to try applying. If not provided, the editor will show a picker."),
}
},
}
}]
}
});
}

public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any) {
const id = typeof args?.id === 'string' ? args.id : undefined;
return CopyPasteController.get(editor)?.pasteAs(id);
}
});
Expand Up @@ -14,19 +14,23 @@ import * as platform from 'vs/base/common/platform';
import { generateUuid } from 'vs/base/common/uuid';
import { toExternalVSDataTransfer, toVSDataTransfer } from 'vs/editor/browser/dnd';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IBulkEditService } 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 } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { createCombinedWorkspaceEdit } from 'vs/editor/contrib/dropOrPasteInto/browser/edit';
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState';
import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress';
import { localize } from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { PostEditWidgetManager } from './postEditWidget';

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

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

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

constructor(
editor: ICodeEditor,
@IInstantiationService instantiationService: IInstantiationService,
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
@IClipboardService private readonly _clipboardService: IClipboardService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IProgressService private readonly _progressService: IProgressService,
) {
super();

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

public pasteAs(preferredId?: string) {
this._editor.focus();
try {
this._pasteAsActionContext = { preferredId };
document.execCommand('paste');
} finally {
this._pasteAsActionContext = undefined;
}
}

public clearWidgets() {
this._postPasteWidgetManager.clear();
}
Expand Down Expand Up @@ -208,11 +226,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi
e.preventDefault();
e.stopImmediatePropagation();

if (this._pasteAsActionContext) {
this.showPasteAsPick(this._pasteAsActionContext.preferredId, allProviders, selections, dataTransfer, metadata);
} else {
this.doPasteInline(allProviders, selections, dataTransfer, metadata);
}
}

private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void {
const p = createCancelablePromise(async (token) => {
const editor = this._editor;
if (!editor.hasModel()) {
return;
}
const model = editor.getModel();

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

private showPasteAsPick(preferredId: string | undefined, allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined): void {
const p = createCancelablePromise(async (token) => {
const editor = this._editor;
if (!editor.hasModel()) {
return;
}
const model = editor.getModel();

const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
try {
await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token);
if (tokenSource.token.isCancellationRequested) {
return;
}

// Filter out any providers the don't match the full data transfer we will send them.
const supportedProviders = allProviders.filter(provider => isSupportedProvider(provider, dataTransfer));

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

if (!providerEdits.length) {
return;
}

let pickedEdit: DocumentPasteEdit | undefined;
if (typeof preferredId === 'string') {
// We are looking for a specific edit
pickedEdit = providerEdits.find(edit => edit.id === preferredId);
} else {
const selected = await this._quickInputService.pick(
providerEdits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({
label: edit.label,
description: edit.id,
detail: edit.detail,
edit,
})), {
placeHolder: localize('pasteAsPickerPlaceholder', "Select Paste Action"),
});
pickedEdit = selected?.edit;
}

if (!pickedEdit) {
return;
}

const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, selections, pickedEdit);
await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor });
} finally {
tokenSource.dispose();
if (this._currentPasteOperation === p) {
this._currentPasteOperation = undefined;
}
}
});

this._progressService.withProgress({
location: ProgressLocation.Window,
title: localize('pasteAsProgress', "Running paste handlers"),
}, () => p);
}


private setCopyMetadata(dataTransfer: DataTransfer, metadata: CopyMetadata) {
dataTransfer.setData(vscodeClipboardMime, JSON.stringify(metadata));
}
Expand Down Expand Up @@ -293,7 +385,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
}
}

private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
const result = await raceCancellation(
Promise.all(
providers.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token))
Expand Down
19 changes: 15 additions & 4 deletions src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts
Expand Up @@ -19,6 +19,8 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat
import { localize } from 'vs/nls';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';

const builtInLabel = localize('builtIn', 'Built-in');

abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, DocumentPasteEditProvider {

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

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

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

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

const insertText = await textEntry.asString();
return {
id: this.id,
label: localize('text.label', "Insert Plain Text"),
detail: builtInLabel,
insertText
};
}
Expand Down Expand Up @@ -101,7 +105,12 @@ class PathProvider extends SimplePasteAndDropProvider {
: localize('defaultDropProvider.uriList.path', "Insert Path");
}

return { insertText, label };
return {
id: this.id,
insertText,
label,
detail: builtInLabel,
};
}
}

Expand Down Expand Up @@ -133,10 +142,12 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
}

return {
id: this.id,
insertText: relativeUris.join(' '),
label: entries.length > 1
? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths")
: localize('defaultDropProvider.uriList.relativePath', "Insert Relative Path")
: localize('defaultDropProvider.uriList.relativePath', "Insert Relative Path"),
detail: builtInLabel,
};
}
}
Expand Down