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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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());
}
const id = 'insertAttachment';
class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider {
private readonly id = 'insertAttachment';
async provideDocumentPasteEdits(
document: vscode.TextDocument,
@ -59,18 +60,16 @@ class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
return;
}
const insert = await createInsertImageAttachmentEdit(document, dataTransfer, token);
const insert = await this.createInsertImageAttachmentEdit(document, dataTransfer, token);
if (!insert) {
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;
return pasteEdit;
}
}
class DropEditProvider implements vscode.DocumentDropEditProvider {
async provideDocumentDropEdits(
document: vscode.TextDocument,
@ -78,58 +77,69 @@ class DropEditProvider implements vscode.DocumentDropEditProvider {
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentDropEdit | undefined> {
const insert = await createInsertImageAttachmentEdit(document, dataTransfer, token);
const insert = await this.createInsertImageAttachmentEdit(document, dataTransfer, token);
if (!insert) {
return;
}
const dropEdit = new vscode.DocumentDropEdit(insert.insertText);
dropEdit.id = id;
dropEdit.id = this.id;
dropEdit.priority = this.getPriority(dataTransfer);
dropEdit.additionalEdit = insert.additionalEdit;
dropEdit.label = vscode.l10n.t('Insert Image as Attachment');
return dropEdit;
}
}
async function 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(' ');
private getPriority(dataTransfer: vscode.DataTransfer): number {
if (dataTransfer.get('text/plain')) {
// Deprioritize in favor of normal text content
return -5;
}
});
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(
@ -296,14 +306,15 @@ function buildAttachment(
}
export function notebookImagePasteSetup(): vscode.Disposable {
const provider = new DropOrPasteEditProvider();
return vscode.Disposable.from(
vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new CopyPasteEditProvider(), {
vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, {
pasteMimeTypes: [
MimeType.png,
MimeType.uriList,
],
}),
vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new DropEditProvider(), {
vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, {
dropMimeTypes: [
...Object.values(imageExtToMime),
MimeType.uriList,

View file

@ -32,13 +32,19 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
return;
}
const edit = await this._makeCreateImagePasteEdit(document, dataTransfer, token);
if (edit) {
return edit;
const createEdit = await this._makeCreateImagePasteEdit(document, dataTransfer, token);
if (createEdit) {
return createEdit;
}
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> {
@ -89,10 +95,19 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
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.priority = this._getPriority(dataTransfer);
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,) {

View file

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

View file

@ -391,6 +391,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
providers.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token))
).then(coalesce),
token);
result?.sort((a, b) => b.priority - a.priority);
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> {
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> {
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>;
@ -61,6 +61,7 @@ class DefaultTextProvider extends SimplePasteAndDropProvider {
const insertText = await textEntry.asString();
return {
id: this.id,
priority: 0,
label: localize('text.label', "Insert Plain Text"),
detail: builtInLabel,
insertText
@ -107,6 +108,7 @@ class PathProvider extends SimplePasteAndDropProvider {
return {
id: this.id,
priority: 0,
insertText,
label,
detail: builtInLabel,
@ -143,6 +145,7 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
return {
id: this.id,
priority: 0,
insertText: relativeUris.join(' '),
label: entries.length > 1
? 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 { Range } from 'vs/editor/common/core/range';
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 { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd';
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));
});
const possibleDropEdits = await raceCancellation(Promise.all(providers.map(provider => {
return provider.provideDocumentOnDropEdits(model, position, ourDataTransfer, tokenSource.token);
})), tokenSource.token);
const edits = await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource);
if (tokenSource.token.isCancellationRequested) {
return;
}
if (possibleDropEdits) {
const allEdits = coalesce(possibleDropEdits);
// Pass in the parent token here as it tracks cancelling the entire drop operation.
if (edits.length) {
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 {
tokenSource.dispose();
@ -125,6 +123,15 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
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> {
if (!dragEvent.dataTransfer) {
return new VSDataTransfer();

View file

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

View file

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

View file

@ -541,6 +541,7 @@ class DocumentPasteEditProvider {
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),
detail: this._extension.displayName || this._extension.name,
priority: edit.priority ?? 0,
insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value },
additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined,
};
@ -1754,6 +1755,7 @@ class DocumentOnDropEditAdapter {
return {
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),
priority: edit.priority ?? 0,
insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value },
additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined,
};

View file

@ -56,6 +56,13 @@ declare module 'vscode' {
*/
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.
*/

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.
*/
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.