Path: blob/main/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { coalesce } from '../../../../base/common/arrays.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { IReadonlyVSDataTransfer, UriList } from '../../../../base/common/dataTransfer.js';8import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';9import { Disposable } from '../../../../base/common/lifecycle.js';10import { Mimes } from '../../../../base/common/mime.js';11import { Schemas } from '../../../../base/common/network.js';12import { relativePath } from '../../../../base/common/resources.js';13import { URI } from '../../../../base/common/uri.js';14import { localize } from '../../../../nls.js';15import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';16import { IPosition } from '../../../common/core/position.js';17import { IRange } from '../../../common/core/range.js';18import { DocumentDropEditProvider, DocumentDropEditsSession, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from '../../../common/languages.js';19import { LanguageFilter } from '../../../common/languageSelector.js';20import { ITextModel } from '../../../common/model.js';21import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';222324abstract class SimplePasteAndDropProvider implements DocumentDropEditProvider, DocumentPasteEditProvider {2526readonly kind: HierarchicalKind;27readonly providedDropEditKinds: HierarchicalKind[];28readonly providedPasteEditKinds: HierarchicalKind[];2930abstract readonly dropMimeTypes: readonly string[] | undefined;31readonly copyMimeTypes = [];32abstract readonly pasteMimeTypes: readonly string[];3334constructor(kind: HierarchicalKind) {35this.kind = kind;36this.providedDropEditKinds = [this.kind];37this.providedPasteEditKinds = [this.kind];38}3940async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise<DocumentPasteEditsSession | undefined> {41const edit = await this.getEdit(dataTransfer, token);42if (!edit) {43return undefined;44}4546return {47edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }],48dispose() { },49};50}5152async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentDropEditsSession | undefined> {53const edit = await this.getEdit(dataTransfer, token);54if (!edit) {55return;56}57return {58edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }],59dispose() { },60};61}6263protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;64}6566export class DefaultTextPasteOrDropEditProvider extends SimplePasteAndDropProvider {6768static readonly id = 'text';6970readonly id = DefaultTextPasteOrDropEditProvider.id;71readonly dropMimeTypes = [Mimes.text];72readonly pasteMimeTypes = [Mimes.text];7374constructor() {75super(HierarchicalKind.Empty.append('text', 'plain'));76}7778protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, _token: CancellationToken): Promise<DocumentPasteEdit | undefined> {79const textEntry = dataTransfer.get(Mimes.text);80if (!textEntry) {81return;82}8384// Suppress if there's also a uriList entry.85// Typically the uri-list contains the same text as the text entry so showing both is confusing.86if (dataTransfer.has(Mimes.uriList)) {87return;88}8990const insertText = await textEntry.asString();91return {92handledMimeType: Mimes.text,93title: localize('text.label', "Insert Plain Text"),94insertText,95kind: this.kind,96};97}98}99100class PathProvider extends SimplePasteAndDropProvider {101102readonly dropMimeTypes = [Mimes.uriList];103readonly pasteMimeTypes = [Mimes.uriList];104105constructor() {106super(HierarchicalKind.Empty.append('uri', 'path', 'absolute'));107}108109protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {110const entries = await extractUriList(dataTransfer);111if (!entries.length || token.isCancellationRequested) {112return;113}114115let uriCount = 0;116const insertText = entries117.map(({ uri, originalText }) => {118if (uri.scheme === Schemas.file) {119return uri.fsPath;120} else {121uriCount++;122return originalText;123}124})125.join(' ');126127let label: string;128if (uriCount > 0) {129// Dropping at least one generic uri (such as https) so use most generic label130label = entries.length > 1131? localize('defaultDropProvider.uriList.uris', "Insert Uris")132: localize('defaultDropProvider.uriList.uri', "Insert Uri");133} else {134// All the paths are file paths135label = entries.length > 1136? localize('defaultDropProvider.uriList.paths', "Insert Paths")137: localize('defaultDropProvider.uriList.path', "Insert Path");138}139140return {141handledMimeType: Mimes.uriList,142insertText,143title: label,144kind: this.kind,145};146}147}148149class RelativePathProvider extends SimplePasteAndDropProvider {150151readonly dropMimeTypes = [Mimes.uriList];152readonly pasteMimeTypes = [Mimes.uriList];153154constructor(155@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService156) {157super(HierarchicalKind.Empty.append('uri', 'path', 'relative'));158}159160protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {161const entries = await extractUriList(dataTransfer);162if (!entries.length || token.isCancellationRequested) {163return;164}165166const relativeUris = coalesce(entries.map(({ uri }) => {167const root = this._workspaceContextService.getWorkspaceFolder(uri);168return root ? relativePath(root.uri, uri) : undefined;169}));170171if (!relativeUris.length) {172return;173}174175return {176handledMimeType: Mimes.uriList,177insertText: relativeUris.join(' '),178title: entries.length > 1179? localize('defaultDropProvider.uriList.relativePaths', "Insert Relative Paths")180: localize('defaultDropProvider.uriList.relativePath', "Insert Relative Path"),181kind: this.kind,182};183}184}185186class PasteHtmlProvider implements DocumentPasteEditProvider {187188public readonly kind = new HierarchicalKind('html');189public readonly providedPasteEditKinds = [this.kind];190191public readonly copyMimeTypes = [];192public readonly pasteMimeTypes = ['text/html'];193194private readonly _yieldTo = [{ mimeType: Mimes.text }];195196async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise<DocumentPasteEditsSession | undefined> {197if (context.triggerKind !== DocumentPasteTriggerKind.PasteAs && !context.only?.contains(this.kind)) {198return;199}200201const entry = dataTransfer.get('text/html');202const htmlText = await entry?.asString();203if (!htmlText || token.isCancellationRequested) {204return;205}206207return {208dispose() { },209edits: [{210insertText: htmlText,211yieldTo: this._yieldTo,212title: localize('pasteHtmlLabel', 'Insert HTML'),213kind: this.kind,214}],215};216}217}218219async function extractUriList(dataTransfer: IReadonlyVSDataTransfer): Promise<{ readonly uri: URI; readonly originalText: string }[]> {220const urlListEntry = dataTransfer.get(Mimes.uriList);221if (!urlListEntry) {222return [];223}224225const strUriList = await urlListEntry.asString();226const entries: { readonly uri: URI; readonly originalText: string }[] = [];227for (const entry of UriList.parse(strUriList)) {228try {229entries.push({ uri: URI.parse(entry), originalText: entry });230} catch {231// noop232}233}234return entries;235}236237const genericLanguageSelector: LanguageFilter = { scheme: '*', hasAccessToAllModels: true };238239export class DefaultDropProvidersFeature extends Disposable {240constructor(241@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,242@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,243) {244super();245246this._register(languageFeaturesService.documentDropEditProvider.register(genericLanguageSelector, new DefaultTextPasteOrDropEditProvider()));247this._register(languageFeaturesService.documentDropEditProvider.register(genericLanguageSelector, new PathProvider()));248this._register(languageFeaturesService.documentDropEditProvider.register(genericLanguageSelector, new RelativePathProvider(workspaceContextService)));249}250}251252export class DefaultPasteProvidersFeature extends Disposable {253constructor(254@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,255@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,256) {257super();258259this._register(languageFeaturesService.documentPasteEditProvider.register(genericLanguageSelector, new DefaultTextPasteOrDropEditProvider()));260this._register(languageFeaturesService.documentPasteEditProvider.register(genericLanguageSelector, new PathProvider()));261this._register(languageFeaturesService.documentPasteEditProvider.register(genericLanguageSelector, new RelativePathProvider(workspaceContextService)));262this._register(languageFeaturesService.documentPasteEditProvider.register(genericLanguageSelector, new PasteHtmlProvider()));263}264}265266267