Skip to content

Commit

Permalink
Allow multiple entries with the same mimetype in dataTransfer (#150425)
Browse files Browse the repository at this point in the history
Currently our data transfer implementation only allows a single entry of each mimeType. There can only be a single `image/gif` file for example.

However this doesn't match how the DOM apis work. If you drop multiple gifs into VS Code for example, the DataTransfer you get contains entries for each of the gifs.

This change allows us to also support DataTransfers that have multiple entries with the same mime type. Just like with the DOM, we support constructing these duplicate mime data transfers internally, but do not allow extensions to create them

As part of this change, I've also made a few clean ups:

- Add helpers for creating dataTransfer items
- Clarify when adding a data transfer item should `append` or `replace`
- Adopt some helper functions in a few more places
  • Loading branch information
mjbvz authored May 26, 2022
1 parent e262c88 commit 528ee1a
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 84 deletions.
71 changes: 45 additions & 26 deletions src/vs/base/common/dataTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,74 @@ export interface IDataTransferItem {
value: any;
}

export function createStringDataTransferItem(stringOrPromise: string | Promise<string>): IDataTransferItem {
return {
asString: async () => stringOrPromise,
asFile: () => undefined,
value: typeof stringOrPromise === 'string' ? stringOrPromise : undefined,
};
}

export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise<Uint8Array>): IDataTransferItem {
return {
asString: async () => '',
asFile: () => ({ name: fileName, uri, data }),
value: undefined,
};
}

export class VSDataTransfer {

private readonly _data = new Map<string, IDataTransferItem>();
private readonly _entries = new Map<string, IDataTransferItem[]>();

public get size(): number {
return this._data.size;
return this._entries.size;
}

public has(mimeType: string): boolean {
return this._data.has(mimeType);
return this._entries.has(this.toKey(mimeType));
}

public get(mimeType: string): IDataTransferItem | undefined {
return this._data.get(mimeType);
return this._entries.get(this.toKey(mimeType))?.[0];
}

public set(mimeType: string, value: IDataTransferItem): void {
this._data.set(mimeType, value);
public append(mimeType: string, value: IDataTransferItem): void {
const existing = this._entries.get(mimeType);
if (existing) {
existing.push(value);
} else {
this._entries.set(this.toKey(mimeType), [value]);
}
}

public delete(mimeType: string) {
this._data.delete(mimeType);
public replace(mimeType: string, value: IDataTransferItem): void {
this._entries.set(this.toKey(mimeType), [value]);
}

public setString(mimeType: string, stringOrPromise: string | Promise<string>) {
this.set(mimeType, {
asString: async () => stringOrPromise,
asFile: () => undefined,
value: typeof stringOrPromise === 'string' ? stringOrPromise : undefined,
});
public delete(mimeType: string) {
this._entries.delete(this.toKey(mimeType));
}

public setFile(mimeType: string, fileName: string, uri: URI | undefined, data: () => Promise<Uint8Array>) {
this.set(mimeType, {
asString: async () => '',
asFile: () => ({ name: fileName, uri, data }),
value: undefined,
});
public *entries(): Iterable<[string, IDataTransferItem]> {
for (const [mine, items] of this._entries.entries()) {
for (const item of items) {
yield [mine, item];
}
}
}

public entries(): IterableIterator<[string, IDataTransferItem]> {
return this._data.entries();
public values(): Iterable<IDataTransferItem> {
return Array.from(this._entries.values()).flat();
}

public values(): IterableIterator<IDataTransferItem> {
return this._data.values();
public forEach(f: (value: IDataTransferItem, key: string) => void) {
for (const [mime, item] of this.entries()) {
f(item, mime);
}
}

public forEach(f: (value: IDataTransferItem, key: string) => void) {
this._data.forEach(f);
private toKey(mimeType: string): string {
return mimeType.toLowerCase();
}
}
19 changes: 12 additions & 7 deletions src/vs/editor/browser/dnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { createFileDataTransferItem, createStringDataTransferItem, IDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { URI } from 'vs/base/common/uri';
import { FileAdditionalNativeProperties } from 'vs/platform/dnd/browser/dnd';


export function toVSDataTransfer(dataTransfer: DataTransfer) {
Expand All @@ -13,16 +14,20 @@ export function toVSDataTransfer(dataTransfer: DataTransfer) {
const type = item.type;
if (item.kind === 'string') {
const asStringValue = new Promise<string>(resolve => item.getAsString(resolve));
vsDataTransfer.setString(type, asStringValue);
vsDataTransfer.append(type, createStringDataTransferItem(asStringValue));
} else if (item.kind === 'file') {
const file = item.getAsFile() as null | (File & { path?: string });
const file = item.getAsFile();
if (file) {
const uri = file.path ? URI.parse(file.path) : undefined;
vsDataTransfer.setFile(type, file.name, uri, async () => {
return new Uint8Array(await file.arrayBuffer());
});
vsDataTransfer.append(type, createFileDataTransferItemFromFile(file));
}
}
}
return vsDataTransfer;
}

export function createFileDataTransferItemFromFile(file: File): IDataTransferItem {
const uri = (file as FileAdditionalNativeProperties).path ? URI.parse((file as FileAdditionalNativeProperties).path!) : undefined;
return createFileDataTransferItem(file.name, uri, async () => {
return new Uint8Array(await file.arrayBuffer());
});
}
12 changes: 4 additions & 8 deletions src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { addDisposableListener } from 'vs/base/browser/dom';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Disposable } from 'vs/base/common/lifecycle';
import { Mimes } from 'vs/base/common/mime';
import { generateUuid } from 'vs/base/common/uuid';
Expand Down Expand Up @@ -103,7 +103,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi

for (const result of results) {
result?.forEach((value, key) => {
dataTransfer.set(key, value);
dataTransfer.replace(key, value);
});
}

Expand Down Expand Up @@ -148,19 +148,15 @@ export class CopyPasteController extends Disposable implements IEditorContributi
if (handle && this._currentClipboardItem?.handle === handle) {
const toMergeDataTransfer = await this._currentClipboardItem.dataTransferPromise;
toMergeDataTransfer.forEach((value, key) => {
dataTransfer.set(key, value);
dataTransfer.append(key, value);
});
}

if (!dataTransfer.has(Mimes.uriList)) {
const resources = await this._clipboardService.readResources();
if (resources.length) {
const value = resources.join('\n');
dataTransfer.set(Mimes.uriList, {
value,
asString: async () => value,
asFile: () => undefined,
});
dataTransfer.append(Mimes.uriList, createStringDataTransferItem(value));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

import { distinct } from 'vs/base/common/arrays';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Disposable } from 'vs/base/common/lifecycle';
import { Mimes } from 'vs/base/common/mime';
import { relativePath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { toVSDataTransfer } from 'vs/editor/browser/dnd';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { IPosition } from 'vs/editor/common/core/position';
Expand All @@ -20,7 +21,7 @@ import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { extractEditorsDropData, FileAdditionalNativeProperties } from 'vs/platform/dnd/browser/dnd';
import { extractEditorsDropData } from 'vs/platform/dnd/browser/dnd';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';

Expand Down Expand Up @@ -94,35 +95,19 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
}

public async extractDataTransferData(dragEvent: DragEvent): Promise<VSDataTransfer> {
const textEditorDataTransfer = new VSDataTransfer();
if (!dragEvent.dataTransfer) {
return textEditorDataTransfer;
}

for (const item of dragEvent.dataTransfer.items) {
const type = item.type;
if (item.kind === 'string') {
const asStringValue = new Promise<string>(resolve => item.getAsString(resolve));
textEditorDataTransfer.setString(type, asStringValue);
} else if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
const uri = (file as FileAdditionalNativeProperties).path ? URI.parse((file as FileAdditionalNativeProperties).path!) : undefined;
textEditorDataTransfer.setFile(type, file.name, uri, async () => {
return new Uint8Array(await file.arrayBuffer());
});
}
}
return new VSDataTransfer();
}

const textEditorDataTransfer = toVSDataTransfer(dragEvent.dataTransfer);
if (!textEditorDataTransfer.has(Mimes.uriList)) {
const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent))
.filter(input => input.resource)
.map(input => input.resource!.toString());

if (editorData.length) {
const str = distinct(editorData).join('\n');
textEditorDataTransfer.setString(Mimes.uriList.toLowerCase(), str);
textEditorDataTransfer.replace(Mimes.uriList, createStringDataTransferItem(str));
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { CancellationError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
Expand Down Expand Up @@ -385,7 +385,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread

const dataTransferOut = new VSDataTransfer();
result.items.forEach(([type, item]) => {
dataTransferOut.setString(type, item.asString);
dataTransferOut.replace(type, createStringDataTransferItem(item.asString));
});
return dataTransferOut;
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/browser/mainThreadTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { VSBuffer } from 'vs/base/common/buffer';
import { DataTransferCache } from 'vs/workbench/api/common/shared/dataTransferCache';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
Expand Down Expand Up @@ -225,7 +225,7 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController {

const additionalDataTransfer = new VSDataTransfer();
additionalDataTransferDTO.items.forEach(([type, item]) => {
additionalDataTransfer.setString(type, item.asString);
additionalDataTransfer.replace(type, createStringDataTransferItem(item.asString));
});
return additionalDataTransfer;
}
Expand Down
10 changes: 5 additions & 5 deletions src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed
import type * as vscode from 'vscode';
import * as types from './extHostTypes';
import { once } from 'vs/base/common/functional';
import { VSDataTransfer } from 'vs/base/common/dataTransfer';

export namespace Command {

Expand Down Expand Up @@ -1980,14 +1981,13 @@ export namespace DataTransferItem {

export namespace DataTransfer {
export function toDataTransfer(value: extHostProtocol.DataTransferDTO, resolveFileData: (dataItemIndex: number) => Promise<Uint8Array>): types.DataTransfer {
const newDataTransfer = new types.DataTransfer();
value.items.forEach(([type, item], index) => {
newDataTransfer.set(type, DataTransferItem.toDataTransferItem(item, () => resolveFileData(index)));
const init = value.items.map(([type, item], index) => {
return [type, DataTransferItem.toDataTransferItem(item, () => resolveFileData(index))] as const;
});
return newDataTransfer;
return new types.DataTransfer(init);
}

export async function toDataTransferDTO(value: vscode.DataTransfer): Promise<extHostProtocol.DataTransferDTO> {
export async function toDataTransferDTO(value: vscode.DataTransfer | VSDataTransfer): Promise<extHostProtocol.DataTransferDTO> {
const newDTO: extHostProtocol.DataTransferDTO = { items: [] };

const promises: Promise<any>[] = [];
Expand Down
23 changes: 19 additions & 4 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2451,18 +2451,33 @@ export class DataTransferItem {

@es5ClassCompat
export class DataTransfer {
#items = new Map<string, DataTransferItem>();
#items = new Map<string, DataTransferItem[]>();

constructor(init?: Iterable<readonly [string, DataTransferItem]>) {
for (const [mime, item] of init ?? []) {
const existing = this.#items.get(mime);
if (existing) {
existing.push(item);
} else {
this.#items.set(mime, [item]);
}
}
}

get(mimeType: string): DataTransferItem | undefined {
return this.#items.get(mimeType);
return this.#items.get(mimeType)?.[0];
}

set(mimeType: string, value: DataTransferItem): void {
this.#items.set(mimeType, value);
// This intentionally overwrites all entries for a given mimetype.
// This is similar to how the DOM DataTransfer type works
this.#items.set(mimeType, [value]);
}

forEach(callbackfn: (value: DataTransferItem, key: string) => void): void {
this.#items.forEach(callbackfn);
for (const [mime, items] of this.#items) {
items.forEach(item => callbackfn(item, mime));
}
}
}

Expand Down
Loading

0 comments on commit 528ee1a

Please sign in to comment.