From 4fb6e4a08f25280d637991c1c96bc2629366196b Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 24 Feb 2025 12:51:18 -0800 Subject: [PATCH] Preserve original data transfer value if still in same ext host context Fixes #241164 --- .../src/languageFeatures/copyPaste.ts | 24 +++++++- src/vs/base/common/dataTransfer.ts | 7 ++- .../api/browser/mainThreadLanguageFeatures.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 3 +- .../api/common/extHostLanguageFeatures.ts | 55 ++++++++++++++----- .../api/common/extHostTypeConverters.ts | 36 ++++++------ src/vs/workbench/api/common/extHostTypes.ts | 10 ++-- 7 files changed, 94 insertions(+), 43 deletions(-) diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts index a0d59256f1089..98b14148d95a9 100644 --- a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -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[], @@ -213,7 +227,15 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider; asFile(): IDataTransferFile | undefined; value: any; } -export function createStringDataTransferItem(stringOrPromise: string | Promise): IDataTransferItem { +export function createStringDataTransferItem(stringOrPromise: string | Promise, 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): IDataTransferItem { +export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise, id?: string): IDataTransferItem { const file = { id: generateUuid(), name: fileName, uri, data }; return { + id, asString: async () => '', asFile: () => file, value: undefined, diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 8c7d32de82730..8f48835044b59 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -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; }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6654da09e906e..575b5f4f5b5e6 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1889,13 +1889,14 @@ export interface IDataTransferFileDTO { } export interface DataTransferItemDTO { + id: string; readonly asString: string; readonly fileData: IDataTransferFileDTO | undefined; readonly uriListData?: ReadonlyArray; } export interface DataTransferDTO { - readonly items: Array<[/* type */string, DataTransferItemDTO]>; + items: Array; } export interface CheckboxUpdate { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 4a785ef16ded7..b8122b07faa8d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -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'; @@ -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'; @@ -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 @@ -586,7 +587,9 @@ class CodeActionAdapter { class DocumentPasteEditProvider { - private readonly _cache = new Cache('DocumentPasteEdit'); + private _cachedPrepare?: Map; + + private readonly _editsCache = new Cache('DocumentPasteEdit.edits'); constructor( private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, @@ -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)); @@ -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(); + + 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 { @@ -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, @@ -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], @@ -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... } @@ -664,7 +693,7 @@ class DocumentPasteEditProvider { } releasePasteEdits(id: number): any { - this._cache.delete(id); + this._editsCache.delete(id); } } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index edd8590eedda6..3c171452e9ec0 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -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'; @@ -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 { @@ -2198,11 +2199,12 @@ export namespace DataTransferItem { return new types.InternalDataTransferItem(item.asString); } - export async function from(mime: string, item: vscode.DataTransferItem | IDataTransferItem): Promise { + export async function from(mime: string, item: vscode.DataTransferItem | IDataTransferItem, id: string = generateUuid()): Promise { const stringValue = await item.asString(); if (mime === Mimes.uriList) { return { + id, asString: stringValue, fileData: undefined, uriListData: serializeUriList(stringValue), @@ -2211,6 +2213,7 @@ export namespace DataTransferItem { const fileValue = item.asFile(); return { + id, asString: stringValue, fileData: fileValue ? { name: fileValue.name, @@ -2251,19 +2254,12 @@ export namespace DataTransfer { return new types.DataTransfer(init); } - export async function from(dataTransfer: Iterable): Promise { - const newDTO: extHostProtocol.DataTransferDTO = { items: [] }; - - const promises: Promise[] = []; - 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): Promise { + 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 }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index edaf58d9a47d0..3c1d961a1853f 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2902,9 +2902,9 @@ export class DataTransferFile implements vscode.DataTransferFile { @es5ClassCompat export class DataTransfer implements vscode.DataTransfer { - #items = new Map(); + #items = new Map(); - constructor(init?: Iterable) { + constructor(init?: Iterable) { for (const [mime, item] of init ?? []) { const existing = this.#items.get(this.#normalizeMime(mime)); if (existing) { @@ -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);