diff --git a/extensions/ipynb/src/notebookImagePaste.ts b/extensions/ipynb/src/notebookImagePaste.ts index cdf520cb67d..4c3a47e1a8e 100644 --- a/extensions/ipynb/src/notebookImagePaste.ts +++ b/extensions/ipynb/src/notebookImagePaste.ts @@ -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 { - 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}`})`); - 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}`})`); + 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, diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts index ccbf0826c8d..7d09c006bcd 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts @@ -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 { @@ -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,) { diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 65172be9ed2..4b5caa0daae 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -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; } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 1540f1da373..03d7006f877 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -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 ?? []; } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 9ff92d1758f..6e86df3bcdf 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -29,12 +29,12 @@ abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise { 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 { 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; @@ -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") diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 3d67774ba2e..ea0a4016640 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -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 { if (!dragEvent.dataTransfer) { return new VSDataTransfer(); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 3a638926065..5143c07f080 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -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 { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6775341b89b..fa6e55a6622 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -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; } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index b4167665114..116eccced1a 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -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, }; diff --git a/src/vscode-dts/vscode.proposed.documentPaste.d.ts b/src/vscode-dts/vscode.proposed.documentPaste.d.ts index f5d97fa3361..fbebc1081dd 100644 --- a/src/vscode-dts/vscode.proposed.documentPaste.d.ts +++ b/src/vscode-dts/vscode.proposed.documentPaste.d.ts @@ -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. */ diff --git a/src/vscode-dts/vscode.proposed.dropMetadata.d.ts b/src/vscode-dts/vscode.proposed.dropMetadata.d.ts index 4ef651d4f46..33dd5a918cf 100644 --- a/src/vscode-dts/vscode.proposed.dropMetadata.d.ts +++ b/src/vscode-dts/vscode.proposed.dropMetadata.d.ts @@ -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.