priority -> yieldTo for drop/paste API proposals (#189881)

Move await from `priority` for drop/paste API proposals

For #179430, #30066

Switching to use `yieldTo` instead of `priority` to let an extension de-rank itself in the list of edits. `priority` was an arbitrary number while `yieldTo` gives more control over how the ranking takes place
This commit is contained in:
Matt Bierner 2023-08-07 18:32:03 -07:00 committed by GitHub
parent 4f66a0c2c1
commit 39df243d21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 124 additions and 88 deletions

View File

@ -15,6 +15,7 @@ enum MimeType {
png = 'image/png',
tiff = 'image/tiff',
webp = 'image/webp',
plain = 'text/plain',
uriList = 'text/uri-list',
}
@ -49,8 +50,6 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod
private readonly id = 'insertAttachment';
private readonly defaultPriority = 5;
async provideDocumentPasteEdits(
document: vscode.TextDocument,
_ranges: readonly vscode.Range[],
@ -68,7 +67,7 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod
}
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, this.id, vscode.l10n.t('Insert Image as Attachment'));
pasteEdit.priority = this.getPastePriority(dataTransfer);
pasteEdit.yieldTo = [{ mimeType: MimeType.plain }];
pasteEdit.additionalEdit = insert.additionalEdit;
return pasteEdit;
}
@ -86,22 +85,12 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod
const dropEdit = new vscode.DocumentDropEdit(insert.insertText);
dropEdit.id = this.id;
dropEdit.priority = this.defaultPriority;
dropEdit.yieldTo = [{ mimeType: MimeType.plain }];
dropEdit.additionalEdit = insert.additionalEdit;
dropEdit.label = vscode.l10n.t('Insert Image as Attachment');
return dropEdit;
}
private getPastePriority(dataTransfer: vscode.DataTransfer): number {
if (dataTransfer.get('text/plain')) {
// Deprioritize in favor of normal text content
return -5;
}
// Otherwise boost priority so attachments are preferred
return this.defaultPriority;
}
private async createInsertImageAttachmentEdit(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,

View File

@ -11,6 +11,11 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
private readonly _id = 'insertLink';
private readonly _yieldTo = [
{ mimeType: 'text/plain' },
{ extensionId: 'vscode.ipynb', editId: 'insertAttachment' },
];
async provideDocumentPasteEdits(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
@ -32,7 +37,8 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
if (!urlList) {
return;
}
const pasteUrlSetting = await getPasteUrlAsFormattedLinkSetting(document);
const pasteUrlSetting = getPasteUrlAsFormattedLinkSetting(document);
const pasteEdit = await createEditAddingLinksForUriList(document, ranges, urlList, false, pasteUrlSetting === PasteUrlAsFormattedLink.Smart, token);
if (!pasteEdit) {
return;
@ -40,7 +46,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
uriEdit.label = pasteEdit.label;
uriEdit.additionalEdit = pasteEdit.additionalEdits;
uriEdit.priority = this._getPriority(dataTransfer);
uriEdit.yieldTo = this._yieldTo;
return uriEdit;
}
@ -61,17 +67,9 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, this._id, edit.label);
pasteEdit.additionalEdit = edit.additionalEdits;
pasteEdit.priority = this._getPriority(dataTransfer);
pasteEdit.yieldTo = this._yieldTo;
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

@ -5,60 +5,44 @@
import * as vscode from 'vscode';
import { createEditAddingLinksForUriList, getPasteUrlAsFormattedLinkSetting, PasteUrlAsFormattedLink, validateLink } from './shared';
const textPlainMime = 'text/plain';
class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider {
readonly id = 'insertMarkdownLink';
async provideDocumentPasteEdits(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit | undefined> {
const pasteUrlSetting = await getPasteUrlAsFormattedLinkSetting(document);
const pasteUrlSetting = getPasteUrlAsFormattedLinkSetting(document);
if (pasteUrlSetting === PasteUrlAsFormattedLink.Never) {
return;
}
const item = dataTransfer.get('text/plain');
const item = dataTransfer.get(textPlainMime);
const urlList = await item?.asString();
if (urlList === undefined) {
if (token.isCancellationRequested || !urlList || !validateLink(urlList).isValid) {
return;
}
if (!validateLink(urlList).isValid) {
return;
}
const uriEdit = new vscode.DocumentPasteEdit('', this.id, '');
if (!urlList) {
return undefined;
}
const pasteEdit = await createEditAddingLinksForUriList(document, ranges, validateLink(urlList).cleanedUrlList, true, pasteUrlSetting === PasteUrlAsFormattedLink.Smart, token);
if (!pasteEdit) {
return;
}
uriEdit.label = pasteEdit.label;
uriEdit.additionalEdit = pasteEdit.additionalEdits;
uriEdit.priority = this._getPriority(pasteEdit.markdownLink);
return uriEdit;
}
private _getPriority(pasteAsMarkdownLink: boolean): number {
if (!pasteAsMarkdownLink) {
// Deprioritize in favor of default paste
return -10;
}
return 0;
const edit = new vscode.DocumentPasteEdit('', this.id, pasteEdit.label);
edit.additionalEdit = pasteEdit.additionalEdits;
edit.yieldTo = pasteEdit.markdownLink ? undefined : [{ mimeType: textPlainMime }];
return edit;
}
}
export function registerLinkPasteSupport(selector: vscode.DocumentSelector,) {
return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteLinkEditProvider(), {
pasteMimeTypes: [
'text/plain',
]
pasteMimeTypes: [textPlainMime]
});
}

View File

@ -9,8 +9,14 @@ import { Schemes } from '../../util/schemes';
class MarkdownImageDropProvider implements vscode.DocumentDropEditProvider {
private readonly _id = 'insertLink';
private readonly _yieldTo = [
{ mimeType: 'text/plain' },
{ extensionId: 'vscode.ipynb', editId: 'insertAttachment' },
];
async provideDocumentDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
if (!enabled) {
@ -42,6 +48,7 @@ class MarkdownImageDropProvider implements vscode.DocumentDropEditProvider {
const edit = new vscode.DocumentDropEdit(snippet.snippet);
edit.id = this._id;
edit.label = snippet.label;
edit.yieldTo = this._yieldTo;
return edit;
}
@ -64,6 +71,7 @@ class MarkdownImageDropProvider implements vscode.DocumentDropEditProvider {
edit.id = this._id;
edit.label = filesEdit.label;
edit.additionalEdit = filesEdit.additionalEdits;
edit.yieldTo = this._yieldTo;
return edit;
}
}

View File

@ -83,7 +83,7 @@ export enum PasteUrlAsFormattedLink {
Never = 'never'
}
export async function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument): Promise<PasteUrlAsFormattedLink> {
export function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument): PasteUrlAsFormattedLink {
return vscode.workspace.getConfiguration('markdown', document).get<PasteUrlAsFormattedLink>('editor.pasteUrlAsFormattedLink.enabled', PasteUrlAsFormattedLink.Smart);
}

View File

@ -805,7 +805,8 @@ export interface DocumentPasteEdit {
readonly id: string;
readonly label: string;
readonly detail: string;
readonly priority: number;
readonly handledMimeType?: string;
readonly yieldTo?: readonly DropYieldTo[];
insertText: string | { readonly snippet: string };
additionalEdit?: WorkspaceEdit;
}
@ -2008,13 +2009,19 @@ export enum ExternalUriOpenerPriority {
Preferred = 3,
}
/**
* @internal
*/
export type DropYieldTo = { readonly editId: string } | { readonly mimeType: string };
/**
* @internal
*/
export interface DocumentOnDropEdit {
readonly id: string;
readonly label: string;
readonly priority: number;
readonly handledMimeType?: string;
readonly yieldTo?: readonly DropYieldTo[];
insertText: string | { readonly snippet: string };
additionalEdit?: WorkspaceEdit;
}

View File

@ -24,7 +24,7 @@ import { Handler, IEditorContribution, PastePayload } from 'vs/editor/common/edi
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 { createCombinedWorkspaceEdit, sortEditsByYieldTo } 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';
@ -432,7 +432,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
}
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
const result = await raceCancellation(
const results = await raceCancellation(
Promise.all(providers.map(provider => {
try {
return provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, token);
@ -440,10 +440,11 @@ export class CopyPasteController extends Disposable implements IEditorContributi
console.error(err);
return undefined;
}
})).then(coalesce),
})),
token);
result?.sort((a, b) => b.priority - a.priority);
return result ?? [];
const edits = coalesce(results ?? []);
sortEditsByYieldTo(edits);
return edits;
}
private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken) {

View File

@ -29,12 +29,12 @@ abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider,
async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, 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, priority: edit.priority } : undefined;
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, detail: edit.detail, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo } : undefined;
}
async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentOnDropEdit | undefined> {
const edit = await this.getEdit(dataTransfer, token);
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, priority: edit.priority } : undefined;
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo } : undefined;
}
protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
@ -46,7 +46,7 @@ class DefaultTextProvider extends SimplePasteAndDropProvider {
readonly dropMimeTypes = [Mimes.text];
readonly pasteMimeTypes = [Mimes.text];
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, _token: CancellationToken) {
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, _token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
const textEntry = dataTransfer.get(Mimes.text);
if (!textEntry) {
return;
@ -61,7 +61,7 @@ class DefaultTextProvider extends SimplePasteAndDropProvider {
const insertText = await textEntry.asString();
return {
id: this.id,
priority: 0,
handledMimeType: Mimes.text,
label: localize('text.label', "Insert Plain Text"),
detail: builtInLabel,
insertText
@ -75,7 +75,7 @@ class PathProvider extends SimplePasteAndDropProvider {
readonly dropMimeTypes = [Mimes.uriList];
readonly pasteMimeTypes = [Mimes.uriList];
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken) {
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
const entries = await extractUriList(dataTransfer);
if (!entries.length || token.isCancellationRequested) {
return;
@ -108,7 +108,7 @@ class PathProvider extends SimplePasteAndDropProvider {
return {
id: this.id,
priority: 0,
handledMimeType: Mimes.uriList,
insertText,
label,
detail: builtInLabel,
@ -128,7 +128,7 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
super();
}
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken) {
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
const entries = await extractUriList(dataTransfer);
if (!entries.length || token.isCancellationRequested) {
return;
@ -145,7 +145,7 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
return {
id: this.id,
priority: 0,
handledMimeType: Mimes.uriList,
insertText: relativeUris.join(' '),
label: entries.length > 1
? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths")

View File

@ -25,6 +25,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { PostEditWidgetManager } from './postEditWidget';
import { sortEditsByYieldTo as sortEditsByYieldTo } from './edit';
export const changeDropTypeCommandId = 'editor.changeDropType';
@ -128,7 +129,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
return provider.provideDocumentOnDropEdits(model, position, dataTransfer, tokenSource.token);
})), tokenSource.token);
const edits = coalesce(results ?? []);
edits.sort((a, b) => b.priority - a.priority);
sortEditsByYieldTo(edits);
return edits;
}

View File

@ -5,7 +5,7 @@
import { URI } from 'vs/base/common/uri';
import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { WorkspaceEdit } from 'vs/editor/common/languages';
import { DropYieldTo, WorkspaceEdit } from 'vs/editor/common/languages';
import { Range } from 'vs/editor/common/core/range';
export interface DropOrPasteEdit {
@ -27,3 +27,26 @@ export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[],
]
};
}
export function sortEditsByYieldTo<T extends {
readonly id: string;
readonly handledMimeType?: string;
readonly yieldTo?: readonly DropYieldTo[];
}>(edits: T[]): void {
function yieldsTo(yTo: DropYieldTo, other: T): boolean {
return ('editId' in yTo && yTo.editId === other.id)
|| ('mimeType' in yTo && yTo.mimeType === other.handledMimeType);
}
edits.sort((a, b) => {
if (a.yieldTo?.some(yTo => yieldsTo(yTo, b))) {
return 1;
}
if (b.yieldTo?.some(yTo => yieldsTo(yTo, a))) {
return -1;
}
return 0;
});
}

View File

@ -1897,7 +1897,7 @@ export interface IPasteEditDto {
id: string;
label: string;
detail: string;
priority: number;
yieldTo?: readonly languages.DropYieldTo[];
insertText: string | { snippet: string };
additionalEdit?: IWorkspaceEditDto;
}
@ -1909,7 +1909,7 @@ export interface IDocumentDropEditProviderMetadata {
export interface IDocumentOnDropEditDto {
id: string;
label: string;
priority: number;
yieldTo?: readonly languages.DropYieldTo[];
insertText: string | { snippet: string };
additionalEdit?: IWorkspaceEditDto;
}

View File

@ -553,14 +553,20 @@ class DocumentPasteEditProvider {
}
return {
id: edit.id ? this._extension.identifier.value + '.' + edit.id : this._extension.identifier.value,
id: edit.id ? this.toInternalId(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,
yieldTo: edit.yieldTo?.map(yTo => {
return 'mimeType' in yTo ? yTo : { editId: this.toInternalId(yTo.extensionId, yTo.editId) };
}),
insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value },
additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined,
};
}
private toInternalId(extId: string, editId: string): string {
return extId + '.' + editId;
}
}
class DocumentFormattingAdapter {
@ -1799,13 +1805,19 @@ class DocumentOnDropEditAdapter {
return undefined;
}
return {
id: edit.id ? this._extension.identifier.value + '.' + edit.id : this._extension.identifier.value,
id: edit.id ? this.toInternalId(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,
yieldTo: edit.yieldTo?.map(yTo => {
return 'mimeType' in yTo ? yTo : { editId: this.toInternalId(yTo.extensionId, yTo.editId) };
}),
insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value },
additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined,
};
}
private toInternalId(extId: string, editId: string): string {
return extId + '.' + editId;
}
}
type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter

View File

@ -57,11 +57,12 @@ declare module 'vscode' {
label: string;
/**
* The relative priority of this edit. Higher priority items are shown first in the UI.
*
* Defaults to `0`.
* Controls the ordering or multiple paste edits. If this provider yield to edits, it will be shown lower in the list.
*/
priority?: number;
yieldTo?: ReadonlyArray<
| { readonly extensionId: string; readonly editId: string }
| { readonly mimeType: string }
>;
/**
* The text or snippet to insert at the pasted locations.

View File

@ -12,20 +12,32 @@ declare module 'vscode' {
* Identifies the type of edit.
*
* This id should be unique within the extension but does not need to be unique across extensions.
*
* TODO: Should this live on the provider instead? That way we could call just the provider we want (however it would
* prevent extending providers in the future to allow returning multiple edits)
*/
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.
*/
label?: string;
/**
* The mime type from the {@link DataTransfer} that this edit applies.
*
* TODO: Should this be taken from `dropMimeTypes` instead?
*/
handledMimeType?: string;
/**
* Controls the ordering or multiple paste edits. If this provider yield to edits, it will be shown lower in the list.
*/
yieldTo?: ReadonlyArray<
// TODO: what about built-in providers?
| { readonly extensionId: string; readonly editId: string }
| { readonly mimeType: string }
>;
}
export interface DocumentDropEditProviderMetadata {