Skip to content

Commit

Permalink
Use inline progress widget for long running copy pastes (#179730)
Browse files Browse the repository at this point in the history
Extracts the inline progress widget for use with the copy paste too
  • Loading branch information
mjbvz authored Apr 12, 2023
1 parent ee2a960 commit e551221
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 90 deletions.
126 changes: 73 additions & 53 deletions src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DataTransfers } from 'vs/base/browser/dnd';
import { addDisposableListener } from 'vs/base/browser/dom';
import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { createStringDataTransferItem, UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { UriList, VSDataTransfer, createStringDataTransferItem } from 'vs/base/common/dataTransfer';
import { Disposable } from 'vs/base/common/lifecycle';
import { Mimes } from 'vs/base/common/mime';
import { Schemas } from 'vs/base/common/network';
Expand All @@ -23,11 +23,12 @@ import { DocumentPasteEdit, DocumentPasteEditProvider, WorkspaceEdit } from 'vs/
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState';
import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress';
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { localize } from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';

const vscodeClipboardMime = 'application/vnd.code.copyMetadata';

Expand All @@ -51,13 +52,18 @@ export class CopyPasteController extends Disposable implements IEditorContributi
readonly dataTransferPromise: CancelablePromise<VSDataTransfer>;
};

private operationIdPool = 0;
private _currentOperation?: { readonly id: number; readonly promise: CancelablePromise<void> };

private readonly _pasteProgressManager: InlineProgressManager;

constructor(
editor: ICodeEditor,
@IInstantiationService instantiationService: IInstantiationService,
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
@IClipboardService private readonly _clipboardService: IClipboardService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IProgressService private readonly _progressService: IProgressService,
) {
super();

Expand All @@ -67,6 +73,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi
this._register(addDisposableListener(container, 'copy', e => this.handleCopy(e)));
this._register(addDisposableListener(container, 'cut', e => this.handleCopy(e)));
this._register(addDisposableListener(container, 'paste', e => this.handlePaste(e), true));

this._pasteProgressManager = this._register(new InlineProgressManager('pasteIntoEditor', editor, instantiationService));
}

private arePasteActionsEnabled(model: ITextModel): boolean {
Expand Down Expand Up @@ -145,6 +153,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi
return;
}

const operationId = this.operationIdPool++;
this._currentOperation?.promise.cancel();
this._pasteProgressManager.clear();

const selections = this._editor.getSelections();
if (!selections?.length || !this._editor.hasModel()) {
return;
Expand All @@ -169,69 +181,77 @@ export class CopyPasteController extends Disposable implements IEditorContributi
e.preventDefault();
e.stopImmediatePropagation();

const tokenSource = new EditorStateCancellationTokenSource(this._editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection);
try {
const dataTransfer = toVSDataTransfer(e.clipboardData);

if (metadata?.id && this._currentClipboardItem?.handle === metadata.id) {
const toMergeDataTransfer = await this._currentClipboardItem.dataTransferPromise;
if (tokenSource.token.isCancellationRequested) {
return;
}
const p = createCancelablePromise(async (token) => {
const editor = this._editor;
if (!editor.hasModel()) {
return;
}

toMergeDataTransfer.forEach((value, key) => {
dataTransfer.replace(key, value);
const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);
try {
this._pasteProgressManager.setAtPosition(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel"), {
cancel: () => tokenSource.cancel()
});
}

if (!dataTransfer.has(Mimes.uriList)) {
const resources = await this._clipboardService.readResources();
if (tokenSource.token.isCancellationRequested) {
return;
const dataTransfer = toVSDataTransfer(e.clipboardData!);

if (metadata?.id && this._currentClipboardItem?.handle === metadata.id) {
const toMergeDataTransfer = await this._currentClipboardItem.dataTransferPromise;
if (tokenSource.token.isCancellationRequested) {
return;
}

toMergeDataTransfer.forEach((value, key) => {
dataTransfer.replace(key, value);
});
}

if (resources.length) {
dataTransfer.append(Mimes.uriList, createStringDataTransferItem(UriList.create(resources)));
if (!dataTransfer.has(Mimes.uriList)) {
const resources = await this._clipboardService.readResources();
if (tokenSource.token.isCancellationRequested) {
return;
}

if (resources.length) {
dataTransfer.append(Mimes.uriList, createStringDataTransferItem(UriList.create(resources)));
}
}
}

dataTransfer.delete(vscodeClipboardMime);
dataTransfer.delete(vscodeClipboardMime);

const providerEdit = await this._progressService.withProgress({
location: ProgressLocation.Notification,
delay: 750,
title: localize('pasteProgressTitle', "Running paste handlers..."),
cancellable: true,
}, () => {
return this.getProviderPasteEdit(providers, dataTransfer, model, selections, tokenSource.token);
}, () => {
return tokenSource.cancel();
});
const providerEdit = await this.getProviderPasteEdit(providers, dataTransfer, model, selections, tokenSource.token);
if (tokenSource.token.isCancellationRequested) {
return;
}

if (tokenSource.token.isCancellationRequested) {
return;
}
if (providerEdit) {
const snippet = typeof providerEdit.insertText === 'string' ? SnippetParser.escape(providerEdit.insertText) : providerEdit.insertText.snippet;
const combinedWorkspaceEdit: WorkspaceEdit = {
edits: [
new ResourceTextEdit(model.uri, {
range: Selection.liftSelection(editor.getSelection()),
text: snippet,
insertAsSnippet: true,
}),
...(providerEdit.additionalEdit?.edits ?? [])
]
};
await this._bulkEditService.apply(combinedWorkspaceEdit, { editor });
return;
}

if (providerEdit) {
const snippet = typeof providerEdit.insertText === 'string' ? SnippetParser.escape(providerEdit.insertText) : providerEdit.insertText.snippet;
const combinedWorkspaceEdit: WorkspaceEdit = {
edits: [
new ResourceTextEdit(model.uri, {
range: Selection.liftSelection(this._editor.getSelection()),
text: snippet,
insertAsSnippet: true,
}),
...(providerEdit.additionalEdit?.edits ?? [])
]
};
await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor });
return;
await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token);
} finally {
tokenSource.dispose();
if (this._currentOperation?.id === operationId) {
this._pasteProgressManager.clear();
this._currentOperation = undefined;
}
}
});

await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token);
} finally {
tokenSource.dispose();
}
this._currentOperation = { id: operationId, promise: p };
}

private getProviderPasteEdit(providers: DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: Selection[], token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { DocumentOnDropEdit, WorkspaceEdit } from 'vs/editor/common/languages';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { DropProgressManager as DropProgressWidgetManager } from 'vs/editor/contrib/dropIntoEditor/browser/dropProgressWidget';
import { PostDropWidgetManager } from 'vs/editor/contrib/dropIntoEditor/browser/postDropWidget';
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState';
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { registerDefaultDropProviders } from './defaultOnDropProviders';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { InlineProgressManager } from 'vs/editor/contrib/inlineProgress/browser/inlineProgress';
import { localize } from 'vs/nls';


export class DropIntoEditorController extends Disposable implements IEditorContribution {
Expand All @@ -35,7 +36,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
private operationIdPool = 0;
private _currentOperation?: { readonly id: number; readonly promise: CancelablePromise<void> };

private readonly _dropProgressWidgetManager: DropProgressWidgetManager;
private readonly _dropProgressManager: InlineProgressManager;
private readonly _postDropWidgetManager: PostDropWidgetManager;

constructor(
Expand All @@ -47,7 +48,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
) {
super();

this._dropProgressWidgetManager = this._register(new DropProgressWidgetManager(editor, instantiationService));
this._dropProgressManager = this._register(new InlineProgressManager('dropIntoEditor', editor, instantiationService));
this._postDropWidgetManager = this._register(new PostDropWidgetManager(editor, instantiationService));

this._register(editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event)));
Expand All @@ -61,17 +62,17 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
}

this._currentOperation?.promise.cancel();
this._dropProgressWidgetManager.clear();
this._dropProgressManager.clear();

editor.focus();
editor.setPosition(position);

const id = this.operationIdPool++;
const operationId = this.operationIdPool++;

const p = createCancelablePromise(async (token) => {
const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token);

this._dropProgressWidgetManager.setAtPosition(position, {
this._dropProgressManager.setAtPosition(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), {
cancel: () => tokenSource.cancel()
});

Expand Down Expand Up @@ -110,14 +111,14 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
} finally {
tokenSource.dispose();

if (this._currentOperation?.id === id) {
this._dropProgressWidgetManager.clear();
if (this._currentOperation?.id === operationId) {
this._dropProgressManager.clear();
this._currentOperation = undefined;
}
}
});

this._currentOperation = { id, promise: p };
this._currentOperation = { id: operationId, promise: p };
}

private async extractDataTransferData(dragEvent: DragEvent): Promise<VSDataTransfer> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import * as dom from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { toAction } from 'vs/base/common/actions';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import 'vs/css!./dropProgressWidget';
import 'vs/css!./postDropWidget';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { Range } from 'vs/editor/common/core/range';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,57 +9,55 @@ import { Codicon } from 'vs/base/common/codicons';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { noBreakWhitespace } from 'vs/base/common/strings';
import { ThemeIcon } from 'vs/base/common/themables';
import 'vs/css!./dropProgressWidget';
import 'vs/css!./inlineProgressWidget';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IPosition } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';

const dropIntoEditorProgress = ModelDecorationOptions.register({
description: 'drop-into-editor-progress',
const inlineProgressDecoration = ModelDecorationOptions.register({
description: 'inline-progress-widget',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
showIfCollapsed: true,
after: {
content: noBreakWhitespace,
inlineClassName: 'drop-into-editor-progress-decoration',
inlineClassName: 'inline-editor-progress-decoration',
inlineClassNameAffectsLetterSpacing: true,
}
});

interface DropProgressDelegate {
cancel(): void;
}

class InlineDropProgressWidget extends Disposable implements IContentWidget {
private static readonly ID = 'editor.widget.dropProgressWidget';
class InlineProgressWidget extends Disposable implements IContentWidget {
private static readonly baseId = 'editor.widget.inlineProgressWidget';

allowEditorOverflow = false;
suppressMouseDown = true;

private domNode!: HTMLElement;

constructor(
private readonly typeId: string,
private readonly editor: ICodeEditor,
private readonly range: Range,
private readonly delegate: DropProgressDelegate,
title: string,
private readonly delegate: InlineProgressDelegate,
) {
super();

this.create();
this.create(title);

this.editor.addContentWidget(this);
this.editor.layoutContentWidget(this);
}

private create(): void {
this.domNode = dom.$('.inline-drop-progress-widget');
private create(title: string): void {
this.domNode = dom.$('.inline-progress-widget');
this.domNode.role = 'button';
this.domNode.title = localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel");
this.domNode.title = title;

const iconElement = dom.$('span.icon');
this.domNode.append(iconElement);
Expand All @@ -85,7 +83,7 @@ class InlineDropProgressWidget extends Disposable implements IContentWidget {
}

getId(): string {
return InlineDropProgressWidget.ID;
return InlineProgressWidget.baseId + '.' + this.typeId;
}

getDomNode(): HTMLElement {
Expand All @@ -105,16 +103,21 @@ class InlineDropProgressWidget extends Disposable implements IContentWidget {
}
}

export class DropProgressManager extends Disposable {
interface InlineProgressDelegate {
cancel(): void;
}

export class InlineProgressManager extends Disposable {

/** Delay before showing the progress widget */
private readonly _showDelay = 500; // ms
private readonly _showPromise = this._register(new MutableDisposable());

private readonly _currentDecorations: IEditorDecorationsCollection;
private readonly _currentWidget = new MutableDisposable<InlineDropProgressWidget>();
private readonly _currentWidget = new MutableDisposable<InlineProgressWidget>();

constructor(
readonly id: string,
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
Expand All @@ -123,18 +126,18 @@ export class DropProgressManager extends Disposable {
this._currentDecorations = _editor.createDecorationsCollection();
}

public setAtPosition(position: IPosition, delegate: DropProgressDelegate) {
public setAtPosition(position: IPosition, title: string, delegate: InlineProgressDelegate) {
this.clear();

this._showPromise.value = disposableTimeout(() => {
const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);
const range = Range.fromPositions(position);
const decorationIds = this._currentDecorations.set([{
range: range,
options: dropIntoEditorProgress,
options: inlineProgressDecoration,
}]);

if (decorationIds.length > 0) {
this._currentWidget.value = this._instantiationService.createInstance(InlineDropProgressWidget, this._editor, range, delegate);
this._currentWidget.value = this._instantiationService.createInstance(InlineProgressWidget, this.id, this._editor, range, title, delegate);
}
}, this._showDelay);
}
Expand Down
Loading

0 comments on commit e551221

Please sign in to comment.