Cleanup data transfer types (#149774)

This change attempts to clean up our internal data transfer types

- Replace `IDataTransfer` (which was just an alias for map) with a `VSDataTransfer` helper class.

    This lets us add helpers and also restrict what you can do on a datatransfer

- Move `DataTransferDTO` types into `protocol`

- Move `DataTransferTypeConverter` into `typeConvert`

- Don't return internal types to ext host callers

     For example, previously we leaked `IDataTransfer` out into providers / controllers. Instead we should always return an instance of `vscode.DataTransfer` to extensions
This commit is contained in:
Matt Bierner
2022-05-23 16:28:02 -07:00
committed by GitHub
parent 45304da73d
commit 91923bab48
16 changed files with 176 additions and 168 deletions

View File

@@ -44,7 +44,6 @@ import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel';
import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust';
import * as tasks from 'vs/workbench/api/common/shared/tasks';
import { DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer';
import { SaveReason } from 'vs/workbench/common/editor';
import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views';
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
@@ -1369,6 +1368,20 @@ export interface ExtHostDocumentsAndEditorsShape {
$acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void;
}
export interface IDataTransferFileDTO {
readonly name: string;
readonly uri?: UriComponents;
}
export interface DataTransferItemDTO {
readonly asString: string;
readonly fileData: IDataTransferFileDTO | undefined;
}
export interface DataTransferDTO {
readonly items: Array<[/* type */string, DataTransferItemDTO]>;
}
export interface ExtHostTreeViewsShape {
$getChildren(treeViewId: string, treeItemHandle?: string): Promise<ITreeItem[] | undefined>;
$handleDrop(destinationViewId: string, requestId: number, treeDataTransfer: DataTransferDTO, targetHandle: string | undefined, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise<void>;

View File

@@ -34,7 +34,6 @@ import { StopWatch } from 'vs/base/common/stopwatch';
import { isCancellationError } from 'vs/base/common/errors';
import { raceCancellationError } from 'vs/base/common/async';
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer';
import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier';
// --- adapter
@@ -1754,10 +1753,10 @@ class DocumentOnDropEditAdapter {
private readonly _handle: number,
) { }
async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> {
async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> {
const doc = this._documents.getDocument(uri);
const pos = typeConvert.Position.to(position);
const dataTransfer = DataTransferConverter.toDataTransfer(dataTransferDto, async (index) => {
const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (index) => {
return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, index)).buffer;
});
@@ -2409,7 +2408,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
return this._createDisposable(handle);
}
$provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> {
$provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> {
return this._withAdapter(handle, DocumentOnDropEditAdapter, adapter =>
Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined);
}

View File

@@ -5,11 +5,12 @@
import { localize } from 'vs/nls';
import type * as vscode from 'vscode';
import * as types from './extHostTypes';
import { basename } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol';
import { DataTransferDTO, ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol';
import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions } from 'vs/workbench/common/views';
import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
import { asPromise } from 'vs/base/common/async';
@@ -18,14 +19,12 @@ import { isUndefinedOrNull, isString } from 'vs/base/common/types';
import { equals, coalesce } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { MarkdownString, ViewBadge } from 'vs/workbench/api/common/extHostTypeConverters';
import { MarkdownString, ViewBadge, DataTransfer } from 'vs/workbench/api/common/extHostTypeConverters';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Command } from 'vs/editor/common/languages';
import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer';
import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { IDataTransfer } from 'vs/base/common/dataTransfer';
type TreeItemHandle = string;
@@ -151,7 +150,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', destinationViewId)));
}
const treeDataTransfer = DataTransferConverter.toDataTransfer(treeDataTransferDTO, async dataItemIndex => {
const treeDataTransfer = DataTransfer.toDataTransfer(treeDataTransferDTO, async dataItemIndex => {
return (await this._proxy.$resolveDropFileData(destinationViewId, requestId, dataItemIndex)).buffer;
});
if ((sourceViewId === destinationViewId) && sourceTreeItemHandles) {
@@ -160,27 +159,13 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return treeView.onDrop(treeDataTransfer, targetItemHandle, token);
}
private async addAdditionalTransferItems(treeDataTransfer: IDataTransfer, treeView: ExtHostTreeView<any>,
sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise<IDataTransfer | undefined> {
private async addAdditionalTransferItems(treeDataTransfer: vscode.DataTransfer, treeView: ExtHostTreeView<any>,
sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise<vscode.DataTransfer | undefined> {
const existingTransferOperation = this.treeDragAndDropService.removeDragOperationTransfer(operationUuid);
if (existingTransferOperation) {
(await existingTransferOperation)?.forEach((value, key) => {
if (value) {
const file = value.asFile();
treeDataTransfer.set(key, {
value: value.value,
asString: value.asString,
asFile() {
if (!file) {
return undefined;
}
return {
name: file.name,
uri: file.uri,
data: async () => await file.data()
};
},
});
treeDataTransfer.set(key, value);
}
});
} else if (operationUuid && treeView.handleDrag) {
@@ -197,12 +182,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', sourceViewId)));
}
const treeDataTransfer = await this.addAdditionalTransferItems(new Map(), treeView, sourceTreeItemHandles, token, operationUuid);
const treeDataTransfer = await this.addAdditionalTransferItems(new types.DataTransfer(), treeView, sourceTreeItemHandles, token, operationUuid);
if (!treeDataTransfer) {
return;
}
return DataTransferConverter.toDataTransferDTO(treeDataTransfer);
return DataTransfer.toDataTransferDTO(treeDataTransfer);
}
async $hasResolve(treeViewId: string): Promise<boolean> {
@@ -472,7 +457,7 @@ class ExtHostTreeView<T> extends Disposable {
}
}
async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: IDataTransfer, token: CancellationToken): Promise<vscode.DataTransfer | undefined> {
async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: vscode.DataTransfer, token: CancellationToken): Promise<vscode.DataTransfer | undefined> {
const extensionTreeItems: T[] = [];
for (const sourceHandle of sourceTreeItemHandles) {
const extensionItem = this.getExtensionElement(sourceHandle);

View File

@@ -38,6 +38,7 @@ import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGro
import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import type * as vscode from 'vscode';
import * as types from './extHostTypes';
import { once } from 'vs/base/common/functional';
export namespace Command {
@@ -1956,3 +1957,52 @@ export namespace ViewBadge {
};
}
}
export namespace DataTransferItem {
export function toDataTransferItem(item: extHostProtocol.DataTransferItemDTO, resolveFileData: () => Promise<Uint8Array>): types.DataTransferItem {
const file = item.fileData;
if (file) {
return new class extends types.DataTransferItem {
override asFile(): vscode.DataTransferFile {
return {
name: file.name,
uri: URI.revive(file.uri),
data: once(() => resolveFileData()),
};
}
}('');
} else {
return new types.DataTransferItem(item.asString);
}
}
}
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)));
});
return newDataTransfer;
}
export async function toDataTransferDTO(value: vscode.DataTransfer): Promise<extHostProtocol.DataTransferDTO> {
const newDTO: extHostProtocol.DataTransferDTO = { items: [] };
const promises: Promise<any>[] = [];
value.forEach((value, key) => {
promises.push((async () => {
const stringValue = await value.asString();
const fileValue = value.asFile();
newDTO.items.push([key, {
asString: stringValue,
fileData: fileValue ? { name: fileValue.name, uri: fileValue.uri } : undefined,
}]);
})());
});
await Promise.all(promises);
return newDTO;
}
}

View File

@@ -2442,7 +2442,7 @@ export class DataTransferItem {
return typeof this.value === 'string' ? this.value : JSON.stringify(this.value);
}
asFile(): undefined {
asFile(): undefined | vscode.DataTransferFile {
return undefined;
}
@@ -2451,15 +2451,18 @@ export class DataTransferItem {
@es5ClassCompat
export class DataTransfer {
private readonly _items: Map<string, DataTransferItem> = new Map();
#items = new Map<string, DataTransferItem>();
get(mimeType: string): DataTransferItem | undefined {
return this._items.get(mimeType);
return this.#items.get(mimeType);
}
set(mimeType: string, value: DataTransferItem): void {
this._items.set(mimeType, value);
this.#items.set(mimeType, value);
}
forEach(callbackfn: (value: DataTransferItem, key: string) => void): void {
this._items.forEach(callbackfn);
this.#items.forEach(callbackfn);
}
}

View File

@@ -1,66 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { once } from 'vs/base/common/functional';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer';
export interface IDataTransferFileDTO {
readonly name: string;
readonly uri?: UriComponents;
}
interface DataTransferItemDTO {
readonly asString: string;
readonly fileData: IDataTransferFileDTO | undefined;
}
export interface DataTransferDTO {
readonly types: string[];
readonly items: DataTransferItemDTO[];
}
export namespace DataTransferConverter {
export function toDataTransfer(value: DataTransferDTO, resolveFileData: (dataItemIndex: number) => Promise<Uint8Array>): IDataTransfer {
const newDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>();
value.types.forEach((type, index) => {
newDataTransfer.set(type, {
asString: async () => value.items[index].asString,
asFile: () => {
const file = value.items[index].fileData;
if (!file) {
return undefined;
}
return {
name: file.name,
uri: URI.revive(file.uri),
data: once(() => resolveFileData(index)),
};
},
value: undefined
});
});
return newDataTransfer;
}
export async function toDataTransferDTO(value: IDataTransfer): Promise<DataTransferDTO> {
const newDTO: DataTransferDTO = {
types: [],
items: []
};
const entries = Array.from(value.entries());
for (const entry of entries) {
newDTO.types.push(entry[0]);
const stringValue = await entry[1].asString();
const fileValue = entry[1].asFile();
newDTO.items.push({
asString: stringValue,
fileData: fileValue ? { name: fileValue.name, uri: fileValue.uri } : undefined,
});
}
return newDTO;
}
}

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from 'vs/base/common/buffer';
import { IDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer';
import { VSDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer';
export class DataTransferCache {
private requestIdPool = 0;
private readonly dataTransfers = new Map</* requestId */ number, ReadonlyArray<IDataTransferItem>>();
public add(dataTransfer: IDataTransfer): { id: number; dispose: () => void } {
public add(dataTransfer: VSDataTransfer): { id: number; dispose: () => void } {
const requestId = this.requestIdPool++;
this.dataTransfers.set(requestId, [...dataTransfer.values()]);
return {