Clean up support for paste edits (#234240)

- Allow setting an array of preferences for paste as keybindings
- Clarifies kinds used for core and extensions
- Exports text kind as API
pull/211550/head^2
Matt Bierner 2024-11-19 22:14:10 -08:00 committed by GitHub
parent 8a4b2bb49b
commit c83b443da0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 261 additions and 105 deletions

View File

@ -9,7 +9,8 @@ import { getDocumentDir, Mimes, Schemes } from './shared';
import { UriList } from './uriList';
class DropOrPasteResourceProvider implements vscode.DocumentDropEditProvider, vscode.DocumentPasteEditProvider {
readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('css', 'url');
readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('css', 'link', 'url');
async provideDocumentDropEdits(
document: vscode.TextDocument,

View File

@ -48,7 +48,7 @@ function getImageMimeType(uri: vscode.Uri): string | undefined {
class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider {
public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'image', 'attachment');
public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment');
async provideDocumentPasteEdits(
document: vscode.TextDocument,
@ -68,7 +68,7 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod
}
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment'), DropOrPasteEditProvider.kind);
pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text')];
pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Text];
pasteEdit.additionalEdit = insert.additionalEdit;
return [pasteEdit];
}
@ -85,7 +85,7 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod
}
const dropEdit = new vscode.DocumentDropEdit(insert.insertText);
dropEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text')];
dropEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Text];
dropEdit.additionalEdit = insert.additionalEdit;
dropEdit.title = vscode.l10n.t('Insert Image as Attachment');
return dropEdit;

View File

@ -10,7 +10,7 @@ import { getParentDocumentUri } from '../../util/document';
import { Mime, mediaMimes } from '../../util/mimes';
import { Schemes } from '../../util/schemes';
import { NewFilePathGenerator } from './newFilePathGenerator';
import { DropOrPasteEdit, createInsertUriListEdit, createUriListSnippet, getSnippetLabel } from './shared';
import { DropOrPasteEdit, createInsertUriListEdit, createUriListSnippet, getSnippetLabelAndKind, baseLinkEditKind, linkEditKind, audioEditKind, videoEditKind, imageEditKind } from './shared';
import { InsertMarkdownLink, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste';
import { UriList } from '../../util/uriList';
@ -30,8 +30,6 @@ enum CopyFilesSettings {
*/
class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider {
public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link');
public static readonly mimeTypes = [
Mime.textUriList,
'files',
@ -39,8 +37,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
];
private readonly _yieldTo = [
vscode.DocumentDropOrPasteEditKind.Empty.append('text'),
vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'image', 'attachment'),
vscode.DocumentDropOrPasteEditKind.Text,
vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment'), // Prefer notebook attachments
];
constructor(
@ -64,7 +62,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
const dropEdit = new vscode.DocumentDropEdit(edit.snippet);
dropEdit.title = edit.label;
dropEdit.kind = ResourcePasteOrDropProvider.kind;
dropEdit.kind = edit.kind;
dropEdit.additionalEdit = edit.additionalEdits;
dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
return dropEdit;
@ -86,7 +84,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
return;
}
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, ResourcePasteOrDropProvider.kind);
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, edit.kind);
pasteEdit.additionalEdit = edit.additionalEdits;
pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
return [pasteEdit];
@ -162,7 +160,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
if (
uriList.entries.length === 1
&& (uriList.entries[0].uri.scheme === Schemes.http || uriList.entries[0].uri.scheme === Schemes.https)
&& !context?.only?.contains(ResourcePasteOrDropProvider.kind)
&& !context?.only?.contains(baseLinkEditKind)
) {
const text = await dataTransfer.get(Mime.textPlain)?.asString();
if (token.isCancellationRequested) {
@ -184,6 +182,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
return {
label: edit.label,
kind: edit.kind,
snippet: new vscode.SnippetString(''),
additionalEdits,
yieldTo: []
@ -254,9 +253,11 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
}
}
const { label, kind } = getSnippetLabelAndKind(snippet);
return {
snippet: snippet.snippet,
label: getSnippetLabel(snippet),
label,
kind,
additionalEdits,
yieldTo: [],
};
@ -277,13 +278,21 @@ function textMatchesUriList(text: string, uriList: UriList): boolean {
}
export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector, parser: IMdParser): vscode.Disposable {
const providedEditKinds = [
baseLinkEditKind,
linkEditKind,
imageEditKind,
audioEditKind,
videoEditKind,
];
return vscode.Disposable.from(
vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(parser), {
providedPasteEditKinds: [ResourcePasteOrDropProvider.kind],
providedPasteEditKinds: providedEditKinds,
pasteMimeTypes: ResourcePasteOrDropProvider.mimeTypes,
}),
vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(parser), {
providedDropEditKinds: [ResourcePasteOrDropProvider.kind],
providedDropEditKinds: providedEditKinds,
dropMimeTypes: ResourcePasteOrDropProvider.mimeTypes,
}),
);

View File

@ -6,9 +6,9 @@
import * as vscode from 'vscode';
import { IMdParser } from '../../markdownEngine';
import { Mime } from '../../util/mimes';
import { createInsertUriListEdit } from './shared';
import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste';
import { UriList } from '../../util/uriList';
import { createInsertUriListEdit, linkEditKind } from './shared';
import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste';
/**
* Adds support for pasting text uris to create markdown links.
@ -17,7 +17,7 @@ import { UriList } from '../../util/uriList';
*/
class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link');
public static readonly kind = linkEditKind;
public static readonly pasteMimeTypes = [Mime.textPlain];
@ -61,7 +61,7 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, pasteUrlSetting, ranges, token))) {
pasteEdit.yieldTo = [
vscode.DocumentDropOrPasteEditKind.Empty.append('text'),
vscode.DocumentDropOrPasteEditKind.Text,
vscode.DocumentDropOrPasteEditKind.Empty.append('uri')
];
}

View File

@ -12,6 +12,16 @@ import { Schemes } from '../../util/schemes';
import { UriList } from '../../util/uriList';
import { resolveSnippet } from './snippets';
/** Base kind for any sort of markdown link, including both path and media links */
export const baseLinkEditKind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link');
/** Kind for normal markdown links, i.e. `[text](path/to/file.md)` */
export const linkEditKind = baseLinkEditKind.append('uri');
export const imageEditKind = baseLinkEditKind.append('image');
export const audioEditKind = baseLinkEditKind.append('audio');
export const videoEditKind = baseLinkEditKind.append('video');
enum MediaKind {
Image,
Video,
@ -45,23 +55,71 @@ export const mediaFileExtensions = new Map<string, MediaKind>([
['wav', MediaKind.Audio],
]);
export function getSnippetLabel(counter: { insertedAudioVideoCount: number; insertedImageCount: number; insertedLinkCount: number }) {
if (counter.insertedAudioVideoCount > 0) {
export function getSnippetLabelAndKind(counter: { readonly insertedAudioCount: number; readonly insertedVideoCount: number; readonly insertedImageCount: number; readonly insertedLinkCount: number }): {
label: string;
kind: vscode.DocumentDropOrPasteEditKind;
} {
if (counter.insertedVideoCount > 0 || counter.insertedAudioCount > 0) {
// Any media plus links
if (counter.insertedLinkCount > 0) {
return vscode.l10n.t('Insert Markdown Media and Links');
} else {
return vscode.l10n.t('Insert Markdown Media');
return {
label: vscode.l10n.t('Insert Markdown Media and Links'),
kind: baseLinkEditKind,
};
}
} else if (counter.insertedImageCount > 0 && counter.insertedLinkCount > 0) {
return vscode.l10n.t('Insert Markdown Images and Links');
// Any media plus images
if (counter.insertedImageCount > 0) {
return {
label: vscode.l10n.t('Insert Markdown Media and Images'),
kind: baseLinkEditKind,
};
}
// Audio only
if (counter.insertedAudioCount > 0 && !counter.insertedVideoCount) {
return {
label: vscode.l10n.t('Insert Markdown Audio'),
kind: audioEditKind,
};
}
// Video only
if (counter.insertedVideoCount > 0 && !counter.insertedAudioCount) {
return {
label: vscode.l10n.t('Insert Markdown Video'),
kind: videoEditKind,
};
}
// Mix of audio and video
return {
label: vscode.l10n.t('Insert Markdown Media'),
kind: baseLinkEditKind,
};
} else if (counter.insertedImageCount > 0) {
return counter.insertedImageCount > 1
? vscode.l10n.t('Insert Markdown Images')
: vscode.l10n.t('Insert Markdown Image');
// Mix of images and links
if (counter.insertedLinkCount > 0) {
return {
label: vscode.l10n.t('Insert Markdown Images and Links'),
kind: baseLinkEditKind,
};
}
// Just images
return {
label: counter.insertedImageCount > 1
? vscode.l10n.t('Insert Markdown Images')
: vscode.l10n.t('Insert Markdown Image'),
kind: imageEditKind,
};
} else {
return counter.insertedLinkCount > 1
? vscode.l10n.t('Insert Markdown Links')
: vscode.l10n.t('Insert Markdown Link');
return {
label: counter.insertedLinkCount > 1
? vscode.l10n.t('Insert Markdown Links')
: vscode.l10n.t('Insert Markdown Link'),
kind: linkEditKind,
};
}
}
@ -70,7 +128,7 @@ export function createInsertUriListEdit(
ranges: readonly vscode.Range[],
urlList: UriList,
options?: UriListSnippetOptions,
): { edits: vscode.SnippetTextEdit[]; label: string } | undefined {
): { edits: vscode.SnippetTextEdit[]; label: string; kind: vscode.DocumentDropOrPasteEditKind } | undefined {
if (!ranges.length || !urlList.entries.length) {
return;
}
@ -79,7 +137,8 @@ export function createInsertUriListEdit(
let insertedLinkCount = 0;
let insertedImageCount = 0;
let insertedAudioVideoCount = 0;
let insertedAudioCount = 0;
let insertedVideoCount = 0;
// Use 1 for all empty ranges but give non-empty range unique indices starting after 1
let placeHolderStartIndex = 1 + urlList.entries.length;
@ -100,15 +159,16 @@ export function createInsertUriListEdit(
insertedLinkCount += snippet.insertedLinkCount;
insertedImageCount += snippet.insertedImageCount;
insertedAudioVideoCount += snippet.insertedAudioVideoCount;
insertedAudioCount += snippet.insertedAudioCount;
insertedVideoCount += snippet.insertedVideoCount;
placeHolderStartIndex += urlList.entries.length;
edits.push(new vscode.SnippetTextEdit(range, snippet.snippet));
}
const label = getSnippetLabel({ insertedAudioVideoCount, insertedImageCount, insertedLinkCount });
return { edits, label };
const { label, kind } = getSnippetLabelAndKind({ insertedAudioCount, insertedVideoCount, insertedImageCount, insertedLinkCount });
return { edits, label, kind };
}
interface UriListSnippetOptions {
@ -134,11 +194,12 @@ interface UriListSnippetOptions {
}
interface UriSnippet {
snippet: vscode.SnippetString;
insertedLinkCount: number;
insertedImageCount: number;
insertedAudioVideoCount: number;
export interface UriSnippet {
readonly snippet: vscode.SnippetString;
readonly insertedLinkCount: number;
readonly insertedImageCount: number;
readonly insertedVideoCount: number;
readonly insertedAudioCount: number;
}
export function createUriListSnippet(
@ -159,7 +220,8 @@ export function createUriListSnippet(
let insertedLinkCount = 0;
let insertedImageCount = 0;
let insertedAudioVideoCount = 0;
let insertedAudioCount = 0;
let insertedVideoCount = 0;
const snippet = new vscode.SnippetString();
let placeholderIndex = options?.placeholderStartIndex ?? 1;
@ -174,7 +236,11 @@ export function createUriListSnippet(
const insertAsVideo = mediaFileExtensions.get(ext) === MediaKind.Video;
const insertAsAudio = mediaFileExtensions.get(ext) === MediaKind.Audio;
if (insertAsVideo || insertAsAudio) {
insertedAudioVideoCount++;
if (insertAsVideo) {
insertedVideoCount++;
} else {
insertedAudioCount++;
}
const mediaSnippet = insertAsVideo
? config.get<string>('editor.filePaste.videoSnippet', '<video controls src="${src}" title="${title}"></video>')
: config.get<string>('editor.filePaste.audioSnippet', '<audio controls src="${src}" title="${title}"></audio>');
@ -201,7 +267,7 @@ export function createUriListSnippet(
}
});
return { snippet, insertedAudioVideoCount, insertedImageCount, insertedLinkCount };
return { snippet, insertedAudioCount, insertedVideoCount, insertedImageCount, insertedLinkCount };
}
@ -264,6 +330,7 @@ function needsBracketLink(mdPath: string): boolean {
export interface DropOrPasteEdit {
readonly snippet: vscode.SnippetString;
readonly kind: vscode.DocumentDropOrPasteEditKind;
readonly label: string;
readonly additionalEdits: vscode.WorkspaceEdit;
readonly yieldTo: vscode.DocumentDropOrPasteEditKind[];

View File

@ -9,9 +9,9 @@ import { Mime } from '../util/mimes';
class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider {
public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'updateLinks');
public static readonly kind = vscode.DocumentDropOrPasteEditKind.Text.append('updateLinks', 'markdown');
public static readonly metadataMime = 'vnd.vscode.markdown.updateLinksMetadata';
public static readonly metadataMime = 'application/vnd.vscode.markdown.updatelinks.metadata';
constructor(
private readonly _client: MdLanguageClient,
@ -67,7 +67,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider
pasteEdit.additionalEdit = workspaceEdit;
if (!context.only || !UpdatePastedLinksEditProvider.kind.contains(context.only)) {
pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text')];
pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Text];
}
return [pasteEdit];

View File

@ -42,7 +42,7 @@ const enabledSettingId = 'updateImportsOnPaste.enabled';
class DocumentPasteProvider implements vscode.DocumentPasteEditProvider {
static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'updateImports', 'jsts');
static readonly kind = vscode.DocumentDropOrPasteEditKind.Text.append('updateImports', 'jsts');
static readonly metadataMimeType = 'application/vnd.code.jsts.metadata';
constructor(
@ -127,7 +127,7 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider {
}
const edit = new vscode.DocumentPasteEdit('', vscode.l10n.t("Paste with imports"), DocumentPasteProvider.kind);
edit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'plain')];
edit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Text.append('plain')];
const additionalEdit = new vscode.WorkspaceEdit();
for (const edit of response.body.edits) {

View File

@ -937,9 +937,9 @@ export interface DocumentPasteEditsSession {
*/
export interface DocumentPasteEditProvider {
readonly id?: string;
readonly copyMimeTypes?: readonly string[];
readonly pasteMimeTypes?: readonly string[];
readonly providedPasteEditKinds?: readonly HierarchicalKind[];
readonly copyMimeTypes: readonly string[];
readonly pasteMimeTypes: readonly string[];
readonly providedPasteEditKinds: readonly HierarchicalKind[];
prepareDocumentPaste?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<undefined | IReadonlyVSDataTransfer>;

View File

@ -12,7 +12,7 @@ import { ICodeEditor } from '../../../browser/editorBrowser.js';
import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from '../../../browser/editorExtensions.js';
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
import { registerEditorFeature } from '../../../common/editorFeatures.js';
import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx } from './copyPasteController.js';
import { CopyPasteController, PastePreference, changePasteTypeCommandId, pasteWidgetVisibleCtx } from './copyPasteController.js';
import { DefaultPasteProvidersFeature, DefaultTextPasteOrDropEditProvider } from './defaultProviders.js';
export const pasteAsCommandId = 'editor.action.pasteAs';
@ -56,13 +56,29 @@ registerEditorCommand(new class extends EditorCommand {
registerEditorAction(class PasteAsAction extends EditorAction {
private static readonly argsSchema = {
type: 'object',
properties: {
kind: {
type: 'string',
description: nls.localize('pasteAs.kind', "The kind of the paste edit to try applying. If not provided or there are multiple edits for this kind, the editor will show a picker."),
oneOf: [
{
type: 'object',
required: ['kind'],
properties: {
kind: {
type: 'string',
description: nls.localize('pasteAs.kind', "The kind of the paste edit to try pasting with.\nIf there are multiple edits for this kind, the editor will show a picker. If there are no edits of this kind, the editor will show an error message."),
}
},
},
{
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'array',
description: nls.localize('pasteAs.preferences', "List of preferred paste edit kind to try applying.\nThe first edit matching the preferences will be applied."),
items: { type: 'string' }
}
},
}
},
]
} as const satisfies IJSONSchema;
constructor() {
@ -81,13 +97,15 @@ registerEditorAction(class PasteAsAction extends EditorAction {
}
public override run(_accessor: ServicesAccessor, editor: ICodeEditor, args?: SchemaToType<typeof PasteAsAction.argsSchema>) {
let kind = typeof args?.kind === 'string' ? args.kind : undefined;
if (!kind && args) {
// Support old id property
// TODO: remove this in the future
kind = typeof (args as any).id === 'string' ? (args as any).id : undefined;
let preference: PastePreference | undefined;
if (args) {
if ('kind' in args) {
preference = { only: new HierarchicalKind(args.kind) };
} else if ('preferences' in args) {
preference = { preferences: args.preferences.map(kind => new HierarchicalKind(kind)) };
}
}
return CopyPasteController.get(editor)?.pasteAs(kind ? new HierarchicalKind(kind) : undefined);
return CopyPasteController.get(editor)?.pasteAs(preference);
}
});

View File

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { addDisposableListener, getActiveDocument } from '../../../../base/browser/dom.js';
import { addDisposableListener } from '../../../../base/browser/dom.js';
import { IAction } from '../../../../base/common/actions.js';
import { coalesce } from '../../../../base/common/arrays.js';
import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js';
@ -18,6 +18,7 @@ import { upcast } from '../../../../base/common/types.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { localize } from '../../../../nls.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
@ -67,9 +68,11 @@ interface DocumentPasteWithProviderEditsSession {
dispose(): void;
}
type PastePreference =
| HierarchicalKind
| { providerId: string };
export type PastePreference =
| { readonly only: HierarchicalKind }
| { readonly preferences: readonly HierarchicalKind[] }
| { readonly providerId: string } // Only used internally
;
export class CopyPasteController extends Disposable implements IEditorContribution {
@ -110,6 +113,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
@IInstantiationService instantiationService: IInstantiationService,
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
@IClipboardService private readonly _clipboardService: IClipboardService,
@ICommandService private readonly _commandService: ICommandService,
@IConfigurationService private readonly _configService: IConfigurationService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@ -140,7 +144,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
this._editor.focus();
try {
this._pasteAsActionContext = { preferred };
getActiveDocument().execCommand('paste');
this._commandService.executeCommand('editor.action.clipboardPasteAction');
} finally {
this._pasteAsActionContext = undefined;
}
@ -289,7 +293,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
// Filter out providers that don't match the requested paste types
const preference = this._pasteAsActionContext?.preferred;
if (preference) {
if (provider.providedPasteEditKinds && !this.providerMatchesPreference(provider, preference)) {
if (!this.providerMatchesPreference(provider, preference)) {
return false;
}
}
@ -300,6 +304,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi
if (!allProviders.length) {
if (this._pasteAsActionContext?.preferred) {
this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext.preferred);
// Also prevent default paste from applying
e.preventDefault();
e.stopImmediatePropagation();
}
return;
}
@ -318,7 +326,13 @@ export class CopyPasteController extends Disposable implements IEditorContributi
}
private showPasteAsNoEditMessage(selections: readonly Selection[], preference: PastePreference) {
MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", preference instanceof HierarchicalKind ? preference.value : preference.providerId), selections[0].getStartPosition());
const kindLabel = 'only' in preference
? preference.only.value
: 'preferences' in preference
? (preference.preferences.length ? preference.preferences.map(preference => preference.value).join(', ') : localize('noPreferences', "empty"))
: preference.providerId;
MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", kindLabel), selections[0].getStartPosition());
}
private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void {
@ -448,7 +462,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
const context: DocumentPasteContext = {
triggerKind: DocumentPasteTriggerKind.PasteAs,
only: preference && preference instanceof HierarchicalKind ? preference : undefined,
only: preference && 'only' in preference ? preference.only : undefined,
};
let editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token));
if (tokenSource.token.isCancellationRequested) {
@ -459,8 +473,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi
if (preference) {
editSession = {
edits: editSession.edits.filter(edit => {
if (preference instanceof HierarchicalKind) {
return preference.contains(edit.kind);
if ('only' in preference) {
return preference.only.contains(edit.kind);
} else if ('preferences' in preference) {
return preference.preferences.some(preference => preference.contains(edit.kind));
} else {
return preference.providerId === edit.provider.id;
}
@ -470,8 +486,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi
}
if (!editSession.edits.length) {
if (context.only) {
this.showPasteAsNoEditMessage(selections, context.only);
if (preference) {
this.showPasteAsNoEditMessage(selections, preference);
}
return;
}
@ -650,11 +666,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi
}
private providerMatchesPreference(provider: DocumentPasteEditProvider, preference: PastePreference): boolean {
if (preference instanceof HierarchicalKind) {
if (!provider.providedPasteEditKinds) {
return true;
}
return provider.providedPasteEditKinds.some(providedKind => preference.contains(providedKind));
if ('only' in preference) {
return provider.providedPasteEditKinds.some(providedKind => preference.only.contains(providedKind));
} else if ('preferences' in preference) {
return preference.preferences.some(providedKind => preference.preferences.some(preferredKind => preferredKind.contains(providedKind)));
} else {
return provider.id === preference.providerId;
}

View File

@ -28,6 +28,7 @@ abstract class SimplePasteAndDropProvider implements DocumentDropEditProvider, D
readonly providedPasteEditKinds: HierarchicalKind[];
abstract readonly dropMimeTypes: readonly string[] | undefined;
readonly copyMimeTypes = [];
abstract readonly pasteMimeTypes: readonly string[];
constructor(kind: HierarchicalKind) {
@ -102,7 +103,7 @@ class PathProvider extends SimplePasteAndDropProvider {
readonly pasteMimeTypes = [Mimes.uriList];
constructor() {
super(HierarchicalKind.Empty.append('uri', 'absolute'));
super(HierarchicalKind.Empty.append('uri', 'path', 'absolute'));
}
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
@ -153,7 +154,7 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
constructor(
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService
) {
super(HierarchicalKind.Empty.append('uri', 'relative'));
super(HierarchicalKind.Empty.append('uri', 'path', 'relative'));
}
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
@ -185,9 +186,9 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
class PasteHtmlProvider implements DocumentPasteEditProvider {
public readonly kind = new HierarchicalKind('html');
public readonly providedPasteEditKinds = [this.kind];
public readonly copyMimeTypes = [];
public readonly pasteMimeTypes = ['text/html'];
private readonly _yieldTo = [{ mimeType: Mimes.text }];

View File

@ -1018,9 +1018,9 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider
private readonly dataTransfers = new DataTransferFileCache();
public readonly copyMimeTypes?: readonly string[];
public readonly pasteMimeTypes?: readonly string[];
public readonly providedPasteEditKinds?: readonly HierarchicalKind[];
public readonly copyMimeTypes: readonly string[];
public readonly pasteMimeTypes: readonly string[];
public readonly providedPasteEditKinds: readonly HierarchicalKind[];
readonly prepareDocumentPaste?: languages.DocumentPasteEditProvider['prepareDocumentPaste'];
readonly provideDocumentPasteEdits?: languages.DocumentPasteEditProvider['provideDocumentPasteEdits'];
@ -1032,9 +1032,9 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider
metadata: IPasteEditProviderMetadataDto,
@IUriIdentityService private readonly _uriIdentService: IUriIdentityService
) {
this.copyMimeTypes = metadata.copyMimeTypes;
this.pasteMimeTypes = metadata.pasteMimeTypes;
this.providedPasteEditKinds = metadata.providedPasteEditKinds?.map(kind => new HierarchicalKind(kind));
this.copyMimeTypes = metadata.copyMimeTypes ?? [];
this.pasteMimeTypes = metadata.pasteMimeTypes ?? [];
this.providedPasteEditKinds = metadata.providedPasteEditKinds?.map(kind => new HierarchicalKind(kind)) ?? [];
if (metadata.supportsCopy) {
this.prepareDocumentPaste = async (model: ITextModel, selections: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<IReadonlyVSDataTransfer | undefined> => {

View File

@ -2907,6 +2907,7 @@ export enum DocumentPasteTriggerKind {
export class DocumentDropOrPasteEditKind {
static Empty: DocumentDropOrPasteEditKind;
static Text: DocumentDropOrPasteEditKind;
private static sep = '.';
@ -2927,6 +2928,7 @@ export class DocumentDropOrPasteEditKind {
}
}
DocumentDropOrPasteEditKind.Empty = new DocumentDropOrPasteEditKind('');
DocumentDropOrPasteEditKind.Text = new DocumentDropOrPasteEditKind('text');
export class DocumentPasteEdit {

View File

@ -25,6 +25,9 @@ const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data';
export class PasteImageProvider implements DocumentPasteEditProvider {
public readonly kind = new HierarchicalKind('chat.attach.image');
public readonly providedPasteEditKinds = [this.kind];
public readonly copyMimeTypes = [];
public readonly pasteMimeTypes = ['image/*'];
constructor(
@ -91,7 +94,7 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
return;
}
return getCustomPaste(model, imageContext, mimeType, new HierarchicalKind('chat.attach.image'), localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService);
return getCustomPaste(model, imageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService);
}
}
@ -138,9 +141,9 @@ export function isImage(array: Uint8Array): boolean {
}
export class CopyTextProvider implements DocumentPasteEditProvider {
public readonly kind = new HierarchicalKind('chat.attach.text');
public readonly providedPasteEditKinds = [];
public readonly copyMimeTypes = [COPY_MIME_TYPES];
public readonly pasteMimeTypes = [];
async prepareDocumentPaste(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<undefined | IReadonlyVSDataTransfer> {
if (model.uri.scheme === ChatInputPart.INPUT_SCHEME) {
@ -156,6 +159,9 @@ export class CopyTextProvider implements DocumentPasteEditProvider {
export class PasteTextProvider implements DocumentPasteEditProvider {
public readonly kind = new HierarchicalKind('chat.attach.text');
public readonly providedPasteEditKinds = [this.kind];
public readonly copyMimeTypes = [];
public readonly pasteMimeTypes = [COPY_MIME_TYPES];
constructor(
@ -194,7 +200,7 @@ export class PasteTextProvider implements DocumentPasteEditProvider {
return;
}
return getCustomPaste(model, copiedContext, Mimes.text, new HierarchicalKind('chat.attach.text'), localize('pastedCodeAttachment', 'Pasted Code Attachment'), this.chatWidgetService);
return getCustomPaste(model, copiedContext, Mimes.text, this.kind, localize('pastedCodeAttachment', 'Pasted Code Attachment'), this.chatWidgetService);
}
}

View File

@ -139,15 +139,33 @@ export class DropOrPasteSchemaContribution extends Disposable implements IWorkbe
then: {
properties: {
'args': {
required: ['kind'],
properties: {
'kind': {
anyOf: [
{ enum: Array.from(this._allProvidedPasteKinds.map(x => x.value)) },
{ type: 'string' },
]
oneOf: [
{
required: ['kind'],
properties: {
'kind': {
anyOf: [
{ enum: Array.from(this._allProvidedPasteKinds.map(x => x.value)) },
{ type: 'string' },
]
}
}
},
{
required: ['preferences'],
properties: {
'preferences': {
type: 'array',
items: {
anyOf: [
{ enum: Array.from(this._allProvidedPasteKinds.map(x => x.value)) },
{ type: 'string' },
]
}
}
}
}
}
]
}
}
}

View File

@ -13,6 +13,20 @@ declare module 'vscode' {
class DocumentDropOrPasteEditKind {
static readonly Empty: DocumentDropOrPasteEditKind;
/**
* The root kind for basic text edits.
*
* This kind should be used for edits that insert basic text into the document. A good example of this is
* an edit that pastes the clipboard text while also updating imports in the file based on the pasted text.
* For this we could use a kind such as `text.updateImports.someLanguageId`.
*
* Even though most drop/paste edits ultimately insert text, you should not use {@linkcode Text} as the base kind
* for every edit as this is redundant. Instead a more specific kind that describes the type of content being
* inserted should be used instead For example, if the edit adds a Markdown link, use `markdown.link` since even
* though the content being inserted is text, it's more important to know that the edit inserts Markdown syntax.
*/
static readonly Text: DocumentDropOrPasteEditKind;
private constructor(value: string);
/**
@ -66,13 +80,18 @@ declare module 'vscode' {
/**
* Additional information about the paste operation.
*/
// TODO: Should we also have this for drop?
export interface DocumentPasteEditContext {
/**
* Requested kind of paste edits to return.
*
* When a explicit kind if requested by {@linkcode DocumentPasteTriggerKind.PasteAs PasteAs}, providers are
* encourage to be more flexible when generating an edit of the requested kind.
*/
readonly only: DocumentDropOrPasteEditKind | undefined;
// TODO: should we also expose preferences?
/**
* The reason why paste edits were requested.
*/