Add priority to paste / drop apis (#182109)

* Add priority to paste / drop apis

Fixes #181886

Replacement for #181453

* Make notebooks prefer text over creating attachments
This commit is contained in:
Matt Bierner
2023-05-10 14:18:05 -07:00
committed by GitHub
parent 46b7e7b02c
commit 4d38422afe
11 changed files with 124 additions and 72 deletions

View File

@@ -45,8 +45,9 @@ function getImageMimeType(uri: vscode.Uri): string | undefined {
return imageExtToMime.get(extname(uri.fsPath).toLowerCase()); return imageExtToMime.get(extname(uri.fsPath).toLowerCase());
} }
const id = 'insertAttachment'; class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider {
class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
private readonly id = 'insertAttachment';
async provideDocumentPasteEdits( async provideDocumentPasteEdits(
document: vscode.TextDocument, document: vscode.TextDocument,
@@ -59,18 +60,16 @@ class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
return; return;
} }
const insert = await createInsertImageAttachmentEdit(document, dataTransfer, token); const insert = await this.createInsertImageAttachmentEdit(document, dataTransfer, token);
if (!insert) { if (!insert) {
return; return;
} }
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, id, vscode.l10n.t('Insert Image as Attachment')); const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, this.id, vscode.l10n.t('Insert Image as Attachment'));
pasteEdit.priority = this.getPriority(dataTransfer);
pasteEdit.additionalEdit = insert.additionalEdit; pasteEdit.additionalEdit = insert.additionalEdit;
return pasteEdit; return pasteEdit;
} }
}
class DropEditProvider implements vscode.DocumentDropEditProvider {
async provideDocumentDropEdits( async provideDocumentDropEdits(
document: vscode.TextDocument, document: vscode.TextDocument,
@@ -78,58 +77,69 @@ class DropEditProvider implements vscode.DocumentDropEditProvider {
dataTransfer: vscode.DataTransfer, dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken, token: vscode.CancellationToken,
): Promise<vscode.DocumentDropEdit | undefined> { ): Promise<vscode.DocumentDropEdit | undefined> {
const insert = await createInsertImageAttachmentEdit(document, dataTransfer, token); const insert = await this.createInsertImageAttachmentEdit(document, dataTransfer, token);
if (!insert) { if (!insert) {
return; return;
} }
const dropEdit = new vscode.DocumentDropEdit(insert.insertText); const dropEdit = new vscode.DocumentDropEdit(insert.insertText);
dropEdit.id = id; dropEdit.id = this.id;
dropEdit.priority = this.getPriority(dataTransfer);
dropEdit.additionalEdit = insert.additionalEdit; dropEdit.additionalEdit = insert.additionalEdit;
dropEdit.label = vscode.l10n.t('Insert Image as Attachment'); dropEdit.label = vscode.l10n.t('Insert Image as Attachment');
return dropEdit; return dropEdit;
} }
}
async function createInsertImageAttachmentEdit( private getPriority(dataTransfer: vscode.DataTransfer): number {
document: vscode.TextDocument, if (dataTransfer.get('text/plain')) {
dataTransfer: vscode.DataTransfer, // Deprioritize in favor of normal text content
token: vscode.CancellationToken, return -5;
): Promise<{ insertText: vscode.SnippetString; additionalEdit: vscode.WorkspaceEdit } | undefined> {
const imageData = await getDroppedImageData(dataTransfer, token);
if (!imageData.length || token.isCancellationRequested) {
return;
}
const currentCell = getCellFromCellDocument(document);
if (!currentCell) {
return undefined;
}
// create updated metadata for cell (prep for WorkspaceEdit)
const newAttachment = buildAttachment(currentCell, imageData);
if (!newAttachment) {
return;
}
// build edits
const additionalEdit = new vscode.WorkspaceEdit();
const nbEdit = vscode.NotebookEdit.updateCellMetadata(currentCell.index, newAttachment.metadata);
const notebookUri = currentCell.notebook.uri;
additionalEdit.set(notebookUri, [nbEdit]);
// create a snippet for paste
const insertText = new vscode.SnippetString();
newAttachment.filenames.forEach((filename, i) => {
insertText.appendText('![');
insertText.appendPlaceholder(`${filename}`);
insertText.appendText(`](${/\s/.test(filename) ? `<attachment:${filename}>` : `attachment:${filename}`})`);
if (i !== newAttachment.filenames.length - 1) {
insertText.appendText(' ');
} }
});
return { insertText, additionalEdit }; // Otherwise boost priority so attachments are preferred
return 5;
}
private async createInsertImageAttachmentEdit(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<{ insertText: vscode.SnippetString; additionalEdit: vscode.WorkspaceEdit } | undefined> {
const imageData = await getDroppedImageData(dataTransfer, token);
if (!imageData.length || token.isCancellationRequested) {
return;
}
const currentCell = getCellFromCellDocument(document);
if (!currentCell) {
return undefined;
}
// create updated metadata for cell (prep for WorkspaceEdit)
const newAttachment = buildAttachment(currentCell, imageData);
if (!newAttachment) {
return;
}
// build edits
const additionalEdit = new vscode.WorkspaceEdit();
const nbEdit = vscode.NotebookEdit.updateCellMetadata(currentCell.index, newAttachment.metadata);
const notebookUri = currentCell.notebook.uri;
additionalEdit.set(notebookUri, [nbEdit]);
// create a snippet for paste
const insertText = new vscode.SnippetString();
newAttachment.filenames.forEach((filename, i) => {
insertText.appendText('![');
insertText.appendPlaceholder(`${filename}`);
insertText.appendText(`](${/\s/.test(filename) ? `<attachment:${filename}>` : `attachment:${filename}`})`);
if (i !== newAttachment.filenames.length - 1) {
insertText.appendText(' ');
}
});
return { insertText, additionalEdit };
}
} }
async function getDroppedImageData( async function getDroppedImageData(
@@ -296,14 +306,15 @@ function buildAttachment(
} }
export function notebookImagePasteSetup(): vscode.Disposable { export function notebookImagePasteSetup(): vscode.Disposable {
const provider = new DropOrPasteEditProvider();
return vscode.Disposable.from( return vscode.Disposable.from(
vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new CopyPasteEditProvider(), { vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, {
pasteMimeTypes: [ pasteMimeTypes: [
MimeType.png, MimeType.png,
MimeType.uriList, MimeType.uriList,
], ],
}), }),
vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new DropEditProvider(), { vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, {
dropMimeTypes: [ dropMimeTypes: [
...Object.values(imageExtToMime), ...Object.values(imageExtToMime),
MimeType.uriList, MimeType.uriList,

View File

@@ -32,13 +32,19 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
return; return;
} }
const edit = await this._makeCreateImagePasteEdit(document, dataTransfer, token); const createEdit = await this._makeCreateImagePasteEdit(document, dataTransfer, token);
if (edit) { if (createEdit) {
return edit; return createEdit;
} }
const snippet = await tryGetUriListSnippet(document, dataTransfer, token); const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label) : undefined; if (!snippet) {
return;
}
const uriEdit = new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label);
uriEdit.priority = this._getPriority(dataTransfer);
return uriEdit;
} }
private async _makeCreateImagePasteEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> { private async _makeCreateImagePasteEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
@@ -89,10 +95,19 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
return; return;
} }
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, '', snippet.label); const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label);
pasteEdit.additionalEdit = workspaceEdit; pasteEdit.additionalEdit = workspaceEdit;
pasteEdit.priority = this._getPriority(dataTransfer);
return pasteEdit; return pasteEdit;
} }
private _getPriority(dataTransfer: vscode.DataTransfer): number {
if (dataTransfer.get('text/plain')) {
// Deprioritize in favor of normal text content
return -10;
}
return 0;
}
} }
export function registerPasteSupport(selector: vscode.DocumentSelector,) { export function registerPasteSupport(selector: vscode.DocumentSelector,) {

View File

@@ -786,6 +786,7 @@ export interface DocumentPasteEdit {
readonly id: string; readonly id: string;
readonly label: string; readonly label: string;
readonly detail: string; readonly detail: string;
readonly priority: number;
insertText: string | { readonly snippet: string }; insertText: string | { readonly snippet: string };
additionalEdit?: WorkspaceEdit; additionalEdit?: WorkspaceEdit;
} }
@@ -1948,6 +1949,7 @@ export enum ExternalUriOpenerPriority {
export interface DocumentOnDropEdit { export interface DocumentOnDropEdit {
readonly id: string; readonly id: string;
readonly label: string; readonly label: string;
readonly priority: number;
insertText: string | { readonly snippet: string }; insertText: string | { readonly snippet: string };
additionalEdit?: WorkspaceEdit; additionalEdit?: WorkspaceEdit;
} }

View File

@@ -391,6 +391,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
providers.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token)) providers.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token))
).then(coalesce), ).then(coalesce),
token); token);
result?.sort((a, b) => b.priority - a.priority);
return result ?? []; return result ?? [];
} }

View File

@@ -29,12 +29,12 @@ abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider,
async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> { async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
const edit = await this.getEdit(dataTransfer, token); const edit = await this.getEdit(dataTransfer, token);
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, detail: edit.detail } : undefined; return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, detail: edit.detail, priority: edit.priority } : undefined;
} }
async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentOnDropEdit | undefined> { async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentOnDropEdit | undefined> {
const edit = await this.getEdit(dataTransfer, token); const edit = await this.getEdit(dataTransfer, token);
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label } : undefined; return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, priority: edit.priority } : undefined;
} }
protected abstract getEdit(dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>; protected abstract getEdit(dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
@@ -61,6 +61,7 @@ class DefaultTextProvider extends SimplePasteAndDropProvider {
const insertText = await textEntry.asString(); const insertText = await textEntry.asString();
return { return {
id: this.id, id: this.id,
priority: 0,
label: localize('text.label', "Insert Plain Text"), label: localize('text.label', "Insert Plain Text"),
detail: builtInLabel, detail: builtInLabel,
insertText insertText
@@ -107,6 +108,7 @@ class PathProvider extends SimplePasteAndDropProvider {
return { return {
id: this.id, id: this.id,
priority: 0,
insertText, insertText,
label, label,
detail: builtInLabel, detail: builtInLabel,
@@ -143,6 +145,7 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
return { return {
id: this.id, id: this.id,
priority: 0,
insertText: relativeUris.join(' '), insertText: relativeUris.join(' '),
label: entries.length > 1 label: entries.length > 1
? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths") ? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths")

View File

@@ -13,6 +13,8 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IPosition } from 'vs/editor/common/core/position'; import { IPosition } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { DocumentOnDropEditProvider } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd';
import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService';
@@ -99,19 +101,15 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime)); return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime));
}); });
const possibleDropEdits = await raceCancellation(Promise.all(providers.map(provider => { const edits = await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource);
return provider.provideDocumentOnDropEdits(model, position, ourDataTransfer, tokenSource.token);
})), tokenSource.token);
if (tokenSource.token.isCancellationRequested) { if (tokenSource.token.isCancellationRequested) {
return; return;
} }
if (possibleDropEdits) { if (edits.length) {
const allEdits = coalesce(possibleDropEdits);
// Pass in the parent token here as it tracks cancelling the entire drop operation.
const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop'; const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop';
await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex: 0, allEdits }, canShowWidget, token); // Pass in the parent token here as it tracks cancelling the entire drop operation
await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex: 0, allEdits: edits }, canShowWidget, token);
} }
} finally { } finally {
tokenSource.dispose(); tokenSource.dispose();
@@ -125,6 +123,15 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
this._currentOperation = p; this._currentOperation = p;
} }
private async getDropEdits(providers: DocumentOnDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) {
const results = await raceCancellation(Promise.all(providers.map(provider => {
return provider.provideDocumentOnDropEdits(model, position, dataTransfer, tokenSource.token);
})), tokenSource.token);
const edits = coalesce(results ?? []);
edits.sort((a, b) => b.priority - a.priority);
return edits;
}
private async extractDataTransferData(dragEvent: DragEvent): Promise<VSDataTransfer> { private async extractDataTransferData(dragEvent: DragEvent): Promise<VSDataTransfer> {
if (!dragEvent.dataTransfer) { if (!dragEvent.dataTransfer) {
return new VSDataTransfer(); return new VSDataTransfer();

View File

@@ -974,10 +974,7 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider
} }
return { return {
id: result.id, ...result,
label: result.label,
detail: result.detail,
insertText: result.insertText,
additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined, additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined,
}; };
} finally { } finally {
@@ -1014,9 +1011,7 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd
return undefined; return undefined;
} }
return { return {
id: edit.id, ...edit,
label: edit.label,
insertText: edit.insertText,
additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)),
}; };
} finally { } finally {

View File

@@ -1838,6 +1838,7 @@ export interface IPasteEditDto {
id: string; id: string;
label: string; label: string;
detail: string; detail: string;
priority: number;
insertText: string | { snippet: string }; insertText: string | { snippet: string };
additionalEdit?: IWorkspaceEditDto; additionalEdit?: IWorkspaceEditDto;
} }
@@ -1849,6 +1850,7 @@ export interface IDocumentDropEditProviderMetadata {
export interface IDocumentOnDropEditDto { export interface IDocumentOnDropEditDto {
id: string; id: string;
label: string; label: string;
priority: number;
insertText: string | { snippet: string }; insertText: string | { snippet: string };
additionalEdit?: IWorkspaceEditDto; additionalEdit?: IWorkspaceEditDto;
} }

View File

@@ -541,6 +541,7 @@ class DocumentPasteEditProvider {
id: edit.id ? this._extension.identifier.value + '.' + edit.id : this._extension.identifier.value, id: edit.id ? this._extension.identifier.value + '.' + edit.id : this._extension.identifier.value,
label: edit.label ?? localize('defaultPasteLabel', "Paste using '{0}' extension", this._extension.displayName || this._extension.name), label: edit.label ?? localize('defaultPasteLabel', "Paste using '{0}' extension", this._extension.displayName || this._extension.name),
detail: this._extension.displayName || this._extension.name, detail: this._extension.displayName || this._extension.name,
priority: edit.priority ?? 0,
insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value },
additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined,
}; };
@@ -1754,6 +1755,7 @@ class DocumentOnDropEditAdapter {
return { return {
id: edit.id ? this._extension.identifier.value + '.' + edit.id : this._extension.identifier.value, id: edit.id ? this._extension.identifier.value + '.' + edit.id : this._extension.identifier.value,
label: edit.label ?? localize('defaultDropLabel', "Drop using '{0}' extension", this._extension.displayName || this._extension.name), label: edit.label ?? localize('defaultDropLabel', "Drop using '{0}' extension", this._extension.displayName || this._extension.name),
priority: edit.priority ?? 0,
insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value },
additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined,
}; };

View File

@@ -56,6 +56,13 @@ declare module 'vscode' {
*/ */
label: string; label: string;
/**
* The relative priority of this edit. Higher priority items are shown first in the UI.
*
* Defaults to `0`.
*/
priority?: number;
/** /**
* The text or snippet to insert at the pasted locations. * The text or snippet to insert at the pasted locations.
*/ */

View File

@@ -13,7 +13,14 @@ declare module 'vscode' {
* *
* This id should be unique within the extension but does not need to be unique across extensions. * This id should be unique within the extension but does not need to be unique across extensions.
*/ */
id: string; id?: string;
/**
* The relative priority of this edit. Higher priority items are shown first in the UI.
*
* Defaults to `0`.
*/
priority?: number;
/** /**
* Human readable label that describes the edit. * Human readable label that describes the edit.