From 290eb3df07869411b1883eb6921f7d70f7885742 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 17 Jun 2024 14:07:00 -0700 Subject: [PATCH] Make sure drop edits are disposed of properly (#216084) --- src/vs/editor/common/languages.ts | 10 ++++++- .../browser/defaultProviders.ts | 14 +++++++--- .../browser/dropIntoEditorController.ts | 27 ++++++++++++------- .../api/browser/mainThreadLanguageFeatures.ts | 23 +++++++++------- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostLanguageFeatures.ts | 2 +- 6 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 018258e559d..96e743a2dca 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -2214,6 +2214,14 @@ export interface DocumentDropEdit { additionalEdit?: WorkspaceEdit; } +/** + * @internal + */ +export interface DocumentDropEditsSession { + edits: readonly DocumentDropEdit[]; + dispose(): void; +} + /** * @internal */ @@ -2221,7 +2229,7 @@ export interface DocumentDropEditProvider { readonly id?: string; readonly dropMimeTypes?: readonly string[]; - provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; + provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; resolveDocumentDropEdit?(edit: DocumentDropEdit, token: CancellationToken): Promise; } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 006713618a6..e44d9341c9b 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -14,7 +14,7 @@ import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; -import { DocumentDropEdit, DocumentDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; +import { DocumentDropEditProvider, DocumentDropEditsSession, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; @@ -34,14 +34,20 @@ abstract class SimplePasteAndDropProvider implements DocumentDropEditProvider, D } return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], dispose() { }, - edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] }; } - async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] : undefined; + if (!edit) { + return; + } + return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], + dispose() { }, + }; } protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 167baf7bdde..082b7447881 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -7,7 +7,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { VSDataTransfer, matchesMimeType } from 'vs/base/common/dataTransfer'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -84,8 +84,9 @@ export class DropIntoEditorController extends Disposable implements IEditorContr editor.setPosition(position); const p = createCancelablePromise(async (token) => { - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token); + const disposables = new DisposableStore(); + const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token)); try { const ourDataTransfer = await this.extractDataTransferData(dragEvent); if (ourDataTransfer.size === 0 || tokenSource.token.isCancellationRequested) { @@ -107,19 +108,19 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime)); }); - const edits = await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource); + const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource)); if (tokenSource.token.isCancellationRequested) { return; } - if (edits.length) { - const activeEditIndex = this.getInitialActiveEditIndex(model, edits); + if (editSession.edits.length) { + const activeEditIndex = this.getInitialActiveEditIndex(model, editSession.edits); const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop'; // Pass in the parent token here as it tracks cancelling the entire drop operation - await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, async edit => edit, token); + await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: editSession.edits }, canShowWidget, async edit => edit, token); } } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentOperation === p) { this._currentOperation = undefined; } @@ -131,10 +132,15 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { + const disposables = new DisposableStore(); + const results = await raceCancellation(Promise.all(providers.map(async provider => { try { const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, tokenSource.token); - return edits?.map(edit => ({ ...edit, providerId: provider.id })); + if (edits) { + disposables.add(edits); + } + return edits?.edits.map(edit => ({ ...edit, providerId: provider.id })); } catch (err) { console.error(err); } @@ -142,7 +148,10 @@ export class DropIntoEditorController extends Disposable implements IEditorContr })), tokenSource.token); const edits = coalesce(results ?? []).flat(); - return sortEditsByYieldTo(edits); + return { + edits: sortEditsByYieldTo(edits), + dispose: () => disposables.dispose() + }; } private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 1781e73af7a..41ae1ab857c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -1124,7 +1124,7 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEdit } } - async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const request = this.dataTransfers.add(dataTransfer); try { const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer); @@ -1137,14 +1137,19 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEdit return; } - return edits.map(edit => { - return { - ...edit, - yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), - kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, - additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), - }; - }); + return { + edits: edits.map(edit => { + return { + ...edit, + yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), + kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, + additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), + }; + }), + dispose: () => { + this._proxy.$releaseDocumentOnDropEdits(this._handle, request.id); + }, + }; } finally { request.dispose(); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index cc5a244204f..67b467c93d1 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2192,6 +2192,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseTypeHierarchy(handle: number, sessionId: string): void; $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void; $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index be98bd49d0d..a4c8f96ca20 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2806,7 +2806,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentDropEditAdapter, adapter => adapter.resolveDropEdit(id, token), {}, undefined); } - $releaseDropEdits(handle: number, cacheId: number): void { + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void { this._withAdapter(handle, DocumentDropEditAdapter, adapter => Promise.resolve(adapter.releaseDropEdits(cacheId)), undefined, undefined); }