Skip to content

Commit

Permalink
Preserve original data transfer value if still in same ext host context
Browse files Browse the repository at this point in the history
  • Loading branch information
mjbvz committed Feb 24, 2025
1 parent 21a561c commit 4fb6e4a
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ import FileConfigurationManager from './fileConfigurationManager';
import { conditionalRegistration, requireGlobalConfiguration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration';

class CopyMetadata {

static parse(data: string): CopyMetadata | undefined {
try {

const parsedData = JSON.parse(data);
const resource = vscode.Uri.parse(parsedData.resource);
const ranges = parsedData.ranges.map((range: any) => new vscode.Range(range.start, range.end));
const copyOperation = parsedData.copyOperation ? Promise.resolve(parsedData.copyOperation) : undefined;
return new CopyMetadata(resource, ranges, copyOperation);
} catch (error) {
return undefined;
}
}

constructor(
public readonly resource: vscode.Uri,
public readonly ranges: readonly vscode.Range[],
Expand Down Expand Up @@ -213,7 +227,15 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider<TsPasteE
return undefined;
}

return metadata instanceof CopyMetadata ? metadata : undefined;
if (metadata instanceof CopyMetadata) {
return metadata;
}

if (typeof metadata === 'string') {
return CopyMetadata.parse(metadata);
}

return undefined;
}

private isEnabled(document: vscode.TextDocument) {
Expand Down
7 changes: 5 additions & 2 deletions src/vs/base/common/dataTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@ export interface IDataTransferFile {
}

export interface IDataTransferItem {
id?: string;
asString(): Thenable<string>;
asFile(): IDataTransferFile | undefined;
value: any;
}

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

export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise<Uint8Array>): IDataTransferItem {
export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise<Uint8Array>, id?: string): IDataTransferItem {
const file = { id: generateUuid(), name: fileName, uri, data };
return {
id,
asString: async () => '',
asFile: () => file,
value: undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider

const dataTransferOut = new VSDataTransfer();
for (const [type, item] of newDataTransfer.items) {
dataTransferOut.replace(type, createStringDataTransferItem(item.asString));
dataTransferOut.replace(type, createStringDataTransferItem(item.asString, item.id));
}
return dataTransferOut;
};
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1889,13 +1889,14 @@ export interface IDataTransferFileDTO {
}

export interface DataTransferItemDTO {
id: string;
readonly asString: string;
readonly fileData: IDataTransferFileDTO | undefined;
readonly uriListData?: ReadonlyArray<string | UriComponents>;
}

export interface DataTransferDTO {
readonly items: Array<[/* type */string, DataTransferItemDTO]>;
items: Array<readonly [/* type */string, DataTransferItemDTO]>;
}

export interface CheckboxUpdate {
Expand Down
55 changes: 42 additions & 13 deletions src/vs/workbench/api/common/extHostLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type * as vscode from 'vscode';
import { asArray, coalesce, isFalsyOrEmpty, isNonEmptyArray } from '../../../base/common/arrays.js';
import { raceCancellationError } from '../../../base/common/async.js';
import { VSBuffer } from '../../../base/common/buffer.js';
Expand All @@ -16,6 +17,7 @@ import { regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';
import { assertType, isObject } from '../../../base/common/types.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { IURITransformer } from '../../../base/common/uriIpc.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IPosition } from '../../../editor/common/core/position.js';
import { Range as EditorRange, IRange } from '../../../editor/common/core/range.js';
import { ISelection, Selection } from '../../../editor/common/core/selection.js';
Expand All @@ -25,17 +27,16 @@ import { encodeSemanticTokensDto } from '../../../editor/common/services/semanti
import { localize } from '../../../nls.js';
import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
import { Cache } from './cache.js';
import * as extHostProtocol from './extHost.protocol.js';
import { IExtHostApiDeprecationService } from './extHostApiDeprecationService.js';
import { CommandsConverter, ExtHostCommands } from './extHostCommands.js';
import { ExtHostDiagnostics } from './extHostDiagnostics.js';
import { ExtHostDocuments } from './extHostDocuments.js';
import { ExtHostTelemetry, IExtHostTelemetry } from './extHostTelemetry.js';
import * as typeConvert from './extHostTypeConverters.js';
import { CodeAction, CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js';
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
import type * as vscode from 'vscode';
import { Cache } from './cache.js';
import * as extHostProtocol from './extHost.protocol.js';
import { CodeAction, CodeActionKind, CompletionList, DataTransfer, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js';

// --- adapter

Expand Down Expand Up @@ -586,7 +587,9 @@ class CodeActionAdapter {

class DocumentPasteEditProvider {

private readonly _cache = new Cache<vscode.DocumentPasteEdit>('DocumentPasteEdit');
private _cachedPrepare?: Map<string, vscode.DataTransferItem>;

private readonly _editsCache = new Cache<vscode.DocumentPasteEdit>('DocumentPasteEdit.edits');

constructor(
private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape,
Expand All @@ -601,6 +604,8 @@ class DocumentPasteEditProvider {
return;
}

this._cachedPrepare = undefined;

const doc = this._documents.getDocument(resource);
const vscodeRanges = ranges.map(range => typeConvert.Range.to(range));

Expand All @@ -613,8 +618,20 @@ class DocumentPasteEditProvider {
}

// Only send back values that have been added to the data transfer
const entries = Array.from(dataTransfer).filter(([, value]) => !(value instanceof InternalDataTransferItem));
return typeConvert.DataTransfer.from(entries);
const newEntries = Array.from(dataTransfer).filter(([, value]) => !(value instanceof InternalDataTransferItem));

// Store off original data transfer items so we can retrieve them on paste
const newCache = new Map<string, vscode.DataTransferItem>();

const items = await Promise.all(Array.from(newEntries, async ([mime, value]) => {
const id = generateUuid();
newCache.set(id, value);
return [mime, await typeConvert.DataTransferItem.from(mime, value, id)] as const;
}));

this._cachedPrepare = newCache;

return { items };
}

async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, context: extHostProtocol.IDocumentPasteContextDto, token: CancellationToken): Promise<extHostProtocol.IPasteEditDto[]> {
Expand All @@ -625,10 +642,22 @@ class DocumentPasteEditProvider {
const doc = this._documents.getDocument(resource);
const vscodeRanges = ranges.map(range => typeConvert.Range.to(range));

const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (id) => {
return (await this._proxy.$resolvePasteFileData(this._handle, requestId, id)).buffer;
const items = dataTransferDto.items.map(([mime, value]): [string, vscode.DataTransferItem] => {
const cached = this._cachedPrepare?.get(value.id);
if (cached) {
return [mime, cached];
}

return [
mime,
typeConvert.DataTransferItem.to(mime, value, async id => {
return (await this._proxy.$resolvePasteFileData(this._handle, requestId, id)).buffer;
})
];
});

const dataTransfer = new DataTransfer(items);

const edits = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, {
only: context.only ? new DocumentDropOrPasteEditKind(context.only) : undefined,
triggerKind: context.triggerKind,
Expand All @@ -637,7 +666,7 @@ class DocumentPasteEditProvider {
return [];
}

const cacheId = this._cache.add(edits);
const cacheId = this._editsCache.add(edits);

return edits.map((edit, i): extHostProtocol.IPasteEditDto => ({
_cacheId: [cacheId, i],
Expand All @@ -651,7 +680,7 @@ class DocumentPasteEditProvider {

async resolvePasteEdit(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<{ insertText?: string | vscode.SnippetString; additionalEdit?: extHostProtocol.IWorkspaceEditDto }> {
const [sessionId, itemId] = id;
const item = this._cache.get(sessionId, itemId);
const item = this._editsCache.get(sessionId, itemId);
if (!item || !this._provider.resolveDocumentPasteEdit) {
return {}; // this should not happen...
}
Expand All @@ -664,7 +693,7 @@ class DocumentPasteEditProvider {
}

releasePasteEdits(id: number): any {
this._cache.delete(id);
this._editsCache.delete(id);
}
}

Expand Down
36 changes: 16 additions & 20 deletions src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import { DisposableStore } from '../../../base/common/lifecycle.js';
import { ResourceMap, ResourceSet } from '../../../base/common/map.js';
import * as marked from '../../../base/common/marked/marked.js';
import { parse, revive } from '../../../base/common/marshalling.js';
import { MarshalledId } from '../../../base/common/marshallingIds.js';
import { Mimes } from '../../../base/common/mime.js';
import { cloneAndChange } from '../../../base/common/objects.js';
import { isWindows } from '../../../base/common/platform.js';
import { IPrefixTreeNode, WellDefinedPrefixTree } from '../../../base/common/prefixTree.js';
import { basename } from '../../../base/common/resources.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { isDefined, isEmptyObject, isNumber, isString, isUndefinedOrNull } from '../../../base/common/types.js';
import { URI, UriComponents, isUriComponents } from '../../../base/common/uri.js';
import { IURITransformer } from '../../../base/common/uriIpc.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { RenderLineNumbersType } from '../../../editor/common/config/editorOptions.js';
import { IPosition } from '../../../editor/common/core/position.js';
import * as editorRange from '../../../editor/common/core/range.js';
Expand All @@ -37,30 +40,28 @@ import { ProgressLocation as MainProgressLocation } from '../../../platform/prog
import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js';
import { IViewBadge } from '../../common/views.js';
import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatModel.js';
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js';
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
import { IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js';
import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js';
import * as notebooks from '../../contrib/notebook/common/notebookCommon.js';
import { CellEditType } from '../../contrib/notebook/common/notebookCommon.js';
import { ICellRange } from '../../contrib/notebook/common/notebookRange.js';
import * as search from '../../contrib/search/common/search.js';
import { TestId } from '../../contrib/testing/common/testId.js';
import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestRunProfileReference, ITestTag, TestMessageType, TestResultItem, TestRunProfileBitset, denamespaceTestTag, namespaceTestTag } from '../../contrib/testing/common/testTypes.js';
import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';
import { ACTIVE_GROUP, SIDE_GROUP } from '../../services/editor/common/editorService.js';
import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js';
import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
import * as extHostProtocol from './extHost.protocol.js';
import { CommandsConverter } from './extHostCommands.js';
import { getPrivateApiFor } from './extHostTestingPrivateApi.js';
import * as types from './extHostTypes.js';
import { IChatResponseTextPart, IChatResponsePromptTsxPart } from '../../contrib/chat/common/languageModels.js';
import { LanguageModelTextPart, LanguageModelPromptTsxPart } from './extHostTypes.js';
import { MarshalledId } from '../../../base/common/marshallingIds.js';
import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { isWindows } from '../../../base/common/platform.js';
import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js';
import { CellEditType } from '../../contrib/notebook/common/notebookCommon.js';
import { LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js';

export namespace Command {

Expand Down Expand Up @@ -2198,11 +2199,12 @@ export namespace DataTransferItem {
return new types.InternalDataTransferItem(item.asString);
}

export async function from(mime: string, item: vscode.DataTransferItem | IDataTransferItem): Promise<extHostProtocol.DataTransferItemDTO> {
export async function from(mime: string, item: vscode.DataTransferItem | IDataTransferItem, id: string = generateUuid()): Promise<extHostProtocol.DataTransferItemDTO> {
const stringValue = await item.asString();

if (mime === Mimes.uriList) {
return {
id,
asString: stringValue,
fileData: undefined,
uriListData: serializeUriList(stringValue),
Expand All @@ -2211,6 +2213,7 @@ export namespace DataTransferItem {

const fileValue = item.asFile();
return {
id,
asString: stringValue,
fileData: fileValue ? {
name: fileValue.name,
Expand Down Expand Up @@ -2251,19 +2254,12 @@ export namespace DataTransfer {
return new types.DataTransfer(init);
}

export async function from(dataTransfer: Iterable<readonly [string, vscode.DataTransferItem | IDataTransferItem]>): Promise<extHostProtocol.DataTransferDTO> {
const newDTO: extHostProtocol.DataTransferDTO = { items: [] };

const promises: Promise<any>[] = [];
for (const [mime, value] of dataTransfer) {
promises.push((async () => {
newDTO.items.push([mime, await DataTransferItem.from(mime, value)]);
})());
}

await Promise.all(promises);
export async function from(dataTransfer: Iterable<readonly [string, IDataTransferItem]>): Promise<extHostProtocol.DataTransferDTO> {
const items = await Promise.all(Array.from(dataTransfer, async ([mime, value]) => {
return [mime, await DataTransferItem.from(mime, value, value.id)] as const;
}));

return newDTO;
return { items };
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2902,9 +2902,9 @@ export class DataTransferFile implements vscode.DataTransferFile {

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

constructor(init?: Iterable<readonly [string, DataTransferItem]>) {
constructor(init?: Iterable<readonly [string, vscode.DataTransferItem]>) {
for (const [mime, item] of init ?? []) {
const existing = this.#items.get(this.#normalizeMime(mime));
if (existing) {
Expand All @@ -2915,17 +2915,17 @@ export class DataTransfer implements vscode.DataTransfer {
}
}

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

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

forEach(callbackfn: (value: DataTransferItem, key: string, dataTransfer: DataTransfer) => void, thisArg?: unknown): void {
forEach(callbackfn: (value: vscode.DataTransferItem, key: string, dataTransfer: DataTransfer) => void, thisArg?: unknown): void {
for (const [mime, items] of this.#items) {
for (const item of items) {
callbackfn.call(thisArg, item, mime, this);
Expand Down

0 comments on commit 4fb6e4a

Please sign in to comment.