Handle promise cancelled for codeActionModel

Fixes #114031

This handles the case where an external caller is waiting on the actions, but the model is disposed. In this case, we should return an empty code action set instead of throwing an exception
This commit is contained in:
Matt Bierner 2021-02-22 15:03:01 -08:00
parent 2b44e04c10
commit b6377b8e71

View file

@ -4,10 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { CancelablePromise, createCancelablePromise, TimeoutTimer } from 'vs/base/common/async';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { Emitter } from 'vs/base/common/event';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
@ -15,10 +18,8 @@ import { CodeActionProviderRegistry, CodeActionTriggerType } from 'vs/editor/com
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IMarkerService } from 'vs/platform/markers/common/markers';
import { IEditorProgressService, Progress } from 'vs/platform/progress/common/progress';
import { getCodeActions, CodeActionSet } from './codeAction';
import { CodeActionSet, getCodeActions } from './codeAction';
import { CodeActionTrigger } from './types';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { isEqual } from 'vs/base/common/resources';
export const SUPPORTED_CODE_ACTIONS = new RawContextKey<string>('supportedCodeAction', '');
@ -147,17 +148,38 @@ export namespace CodeActionsState {
export class Triggered {
readonly type = Type.Triggered;
public readonly actions: Promise<CodeActionSet>;
constructor(
public readonly trigger: CodeActionTrigger,
public readonly rangeOrSelection: Range | Selection,
public readonly position: Position,
public readonly actions: CancelablePromise<CodeActionSet>,
) { }
private readonly _cancellablePromise: CancelablePromise<CodeActionSet>,
) {
this.actions = _cancellablePromise.catch((e): CodeActionSet => {
if (isPromiseCanceledError(e)) {
return emptyCodeActionSet;
}
throw e;
});
}
public cancel() {
this._cancellablePromise.cancel();
}
}
export type State = typeof Empty | Triggered;
}
const emptyCodeActionSet: CodeActionSet = {
allActions: [],
validActions: [],
dispose: () => { },
documentation: [],
hasAutoFix: false
};
export class CodeActionModel extends Disposable {
private readonly _codeActionOracle = this._register(new MutableDisposable<CodeActionOracle>());
@ -167,6 +189,8 @@ export class CodeActionModel extends Disposable {
private readonly _onDidChangeState = this._register(new Emitter<CodeActionsState.State>());
public readonly onDidChangeState = this._onDidChangeState.event;
#isDisposed = false;
constructor(
private readonly _editor: ICodeEditor,
private readonly _markerService: IMarkerService,
@ -184,11 +208,20 @@ export class CodeActionModel extends Disposable {
}
dispose(): void {
if (this.#isDisposed) {
return;
}
this.#isDisposed = true;
super.dispose();
this.setState(CodeActionsState.Empty, true);
}
private _update(): void {
if (this.#isDisposed) {
return;
}
this._codeActionOracle.value = undefined;
this.setState(CodeActionsState.Empty);
@ -240,12 +273,12 @@ export class CodeActionModel extends Disposable {
// Cancel old request
if (this._state.type === CodeActionsState.Type.Triggered) {
this._state.actions.cancel();
this._state.cancel();
}
this._state = newState;
if (!skipNotify) {
if (!skipNotify && !this.#isDisposed) {
this._onDidChangeState.fire(newState);
}
}