Make sure drop edits are disposed of properly (#216084)

This commit is contained in:
Matt Bierner 2024-06-17 14:07:00 -07:00 committed by GitHub
parent 3ef8921a20
commit 290eb3df07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 53 additions and 24 deletions

View file

@ -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<DocumentDropEdit[]>;
provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult<DocumentDropEditsSession>;
resolveDocumentDropEdit?(edit: DocumentDropEdit, token: CancellationToken): Promise<DocumentDropEdit>;
}

View file

@ -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<DocumentDropEdit[] | undefined> {
async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentDropEditsSession | undefined> {
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<DocumentPasteEdit | undefined>;

View file

@ -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<DocumentDropEdit & { readonly providerId?: string }>) {

View file

@ -1124,7 +1124,7 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEdit
}
}
async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<languages.DocumentDropEdit[] | undefined> {
async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<languages.DocumentDropEditsSession | undefined> {
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();
}

View file

@ -2192,6 +2192,7 @@ export interface ExtHostLanguageFeaturesShape {
$provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise<ITypeHierarchyItemDto[] | undefined>;
$releaseTypeHierarchy(handle: number, sessionId: string): void;
$provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<IDocumentDropEditDto[] | undefined>;
$releaseDocumentOnDropEdits(handle: number, cacheId: number): void;
$provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise<IWorkspaceEditDto | null>;
$provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise<IdentifiableInlineEdit | undefined>;
$freeInlineEdit(handle: number, pid: number): void;

View file

@ -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);
}