Revert "Merge from vscode 81d7885dc2e9dc617e1522697a2966bc4025a45d (#5949)" (#5983)

This reverts commit d15a3fcc98.
This commit is contained in:
Karl Burtram
2019-06-11 12:35:58 -07:00
committed by GitHub
parent 95a50b7892
commit 5a7562a37b
926 changed files with 11394 additions and 19540 deletions

View File

@@ -15,14 +15,8 @@ import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTr
import { IModelService } from 'vs/editor/common/services/modelService';
import { CodeActionFilter, CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind } from './codeActionTrigger';
import { TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
export interface CodeActionSet extends IDisposable {
readonly actions: readonly CodeAction[];
readonly hasAutoFix: boolean;
}
class ManagedCodeActionSet extends Disposable implements CodeActionSet {
export class CodeActionSet {
private static codeActionsComparator(a: CodeAction, b: CodeAction): number {
if (isNonEmptyArray(a.diagnostics)) {
@@ -40,10 +34,8 @@ class ManagedCodeActionSet extends Disposable implements CodeActionSet {
public readonly actions: readonly CodeAction[];
public constructor(actions: readonly CodeAction[], disposables: DisposableStore) {
super();
this._register(disposables);
this.actions = mergeSort([...actions], ManagedCodeActionSet.codeActionsComparator);
public constructor(actions: readonly CodeAction[]) {
this.actions = mergeSort([...actions], CodeActionSet.codeActionsComparator);
}
public get hasAutoFix() {
@@ -67,14 +59,12 @@ export function getCodeActions(
const cts = new TextModelCancellationTokenSource(model, token);
const providers = getCodeActionProviders(model, filter);
const disposables = new DisposableStore();
const promises = providers.map(provider => {
return Promise.resolve(provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token)).then(providedCodeActions => {
if (cts.token.isCancellationRequested || !providedCodeActions) {
if (cts.token.isCancellationRequested || !Array.isArray(providedCodeActions)) {
return [];
}
disposables.add(providedCodeActions);
return providedCodeActions.actions.filter(action => action && filtersAction(filter, action));
return providedCodeActions.filter(action => action && filtersAction(filter, action));
}, (err): CodeAction[] => {
if (isPromiseCanceledError(err)) {
throw err;
@@ -94,7 +84,7 @@ export function getCodeActions(
return Promise.all(promises)
.then(flatten)
.then(actions => new ManagedCodeActionSet(actions, disposables))
.then(actions => new CodeActionSet(actions))
.finally(() => {
listener.dispose();
cts.dispose();
@@ -116,7 +106,7 @@ function getCodeActionProviders(
});
}
registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise<ReadonlyArray<CodeAction>> {
registerLanguageCommand('_executeCodeActionProvider', function (accessor, args): Promise<ReadonlyArray<CodeAction>> {
const { resource, range, kind } = args;
if (!(resource instanceof URI) || !Range.isIRange(range)) {
throw illegalArgument();
@@ -127,11 +117,9 @@ registerLanguageCommand('_executeCodeActionProvider', async function (accessor,
throw illegalArgument();
}
const codeActionSet = await getCodeActions(
return getCodeActions(
model,
model.validateRange(range),
{ type: 'manual', filter: { includeSourceActions: true, kind: kind && kind.value ? new CodeActionKind(kind.value) : undefined } },
CancellationToken.None);
codeActionSet.dispose();
return codeActionSet.actions;
CancellationToken.None).then(actions => actions.actions);
});

View File

@@ -3,8 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancelablePromise } from 'vs/base/common/async';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
@@ -19,10 +20,10 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IMarkerService } from 'vs/platform/markers/common/markers';
import { ILocalProgressService } from 'vs/platform/progress/common/progress';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { CodeActionModel, SUPPORTED_CODE_ACTIONS, CodeActionsState } from './codeActionModel';
import { CodeActionAutoApply, CodeActionFilter, CodeActionKind, CodeActionTrigger } from './codeActionTrigger';
import { CodeActionWidget } from './codeActionWidget';
import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeActionTrigger';
import { CodeActionContextMenu } from './codeActionWidget';
import { LightBulbWidget } from './lightBulbWidget';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { onUnexpectedError } from 'vs/base/common/errors';
@@ -44,15 +45,16 @@ export class QuickFixController extends Disposable implements IEditorContributio
private readonly _editor: ICodeEditor;
private readonly _model: CodeActionModel;
private readonly _codeActionWidget: CodeActionWidget;
private readonly _codeActionContextMenu: CodeActionContextMenu;
private readonly _lightBulbWidget: LightBulbWidget;
private _currentCodeActions: CodeActionSet | undefined;
private _activeRequest: CancelablePromise<CodeActionSet> | undefined;
constructor(
editor: ICodeEditor,
@IMarkerService markerService: IMarkerService,
@IContextKeyService contextKeyService: IContextKeyService,
@ILocalProgressService progressService: ILocalProgressService,
@IProgressService progressService: IProgressService,
@IContextMenuService contextMenuService: IContextMenuService,
@ICommandService private readonly _commandService: ICommandService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@@ -61,70 +63,58 @@ export class QuickFixController extends Disposable implements IEditorContributio
super();
this._editor = editor;
this._model = this._register(new CodeActionModel(this._editor, markerService, contextKeyService, progressService));
this._codeActionWidget = new CodeActionWidget(editor, contextMenuService, {
onSelectCodeAction: async (action) => {
try {
await this._applyCodeAction(action);
} finally {
// Retrigger
this._trigger({ type: 'auto', filter: {} });
}
}
});
this._model = new CodeActionModel(this._editor, markerService, contextKeyService, progressService);
this._codeActionContextMenu = new CodeActionContextMenu(editor, contextMenuService, action => this._onApplyCodeAction(action));
this._lightBulbWidget = this._register(new LightBulbWidget(editor));
this._updateLightBulbTitle();
this._register(this._codeActionContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto', filter: {} })));
this._register(this._lightBulbWidget.onClick(this._handleLightBulbSelect, this));
this._register(this._model.onDidChangeState((newState) => this._onDidChangeCodeActionsState(newState)));
this._register(this._model.onDidChangeState(e => this._onDidChangeCodeActionsState(e)));
this._register(this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitle, this));
}
dipose() {
public dispose(): void {
super.dispose();
dispose(this._currentCodeActions);
this._model.dispose();
}
private _onDidChangeCodeActionsState(newState: CodeActionsState.State): void {
if (newState.type === CodeActionsState.Type.Triggered) {
newState.actions.then(actions => {
dispose(this._currentCodeActions);
this._currentCodeActions = actions;
if (this._activeRequest) {
this._activeRequest.cancel();
this._activeRequest = undefined;
}
if (!actions.actions.length && newState.trigger.context) {
MessageController.get(this._editor).showMessage(newState.trigger.context.notAvailableMessage, newState.trigger.context.position);
}
});
if (newState.type === CodeActionsState.Type.Triggered) {
this._activeRequest = newState.actions;
if (newState.trigger.filter && newState.trigger.filter.kind) {
// Triggered for specific scope
newState.actions.then(codeActions => {
if (codeActions.actions.length > 0) {
newState.actions.then(fixes => {
if (fixes.actions.length > 0) {
// Apply if we only have one action or requested autoApply
if (newState.trigger.autoApply === CodeActionAutoApply.First || (newState.trigger.autoApply === CodeActionAutoApply.IfSingle && codeActions.actions.length === 1)) {
this._applyCodeAction(codeActions.actions[0]);
if (newState.trigger.autoApply === CodeActionAutoApply.First || (newState.trigger.autoApply === CodeActionAutoApply.IfSingle && fixes.actions.length === 1)) {
this._onApplyCodeAction(fixes.actions[0]);
return;
}
}
this._codeActionWidget.show(newState.actions, newState.position);
this._codeActionContextMenu.show(newState.actions, newState.position);
}).catch(onUnexpectedError);
} else if (newState.trigger.type === 'manual') {
this._codeActionWidget.show(newState.actions, newState.position);
this._codeActionContextMenu.show(newState.actions, newState.position);
} else {
// auto magically triggered
// * update an existing list of code actions
// * manage light bulb
if (this._codeActionWidget.isVisible) {
this._codeActionWidget.show(newState.actions, newState.position);
if (this._codeActionContextMenu.isVisible) {
this._codeActionContextMenu.show(newState.actions, newState.position);
} else {
this._lightBulbWidget.tryShow(newState);
}
}
} else {
dispose(this._currentCodeActions);
this._currentCodeActions = undefined;
this._lightBulbWidget.hide();
}
}
@@ -133,26 +123,12 @@ export class QuickFixController extends Disposable implements IEditorContributio
return QuickFixController.ID;
}
public manualTriggerAtCurrentPosition(
notAvailableMessage: string,
filter?: CodeActionFilter,
autoApply?: CodeActionAutoApply
): void {
if (!this._editor.hasModel()) {
return;
}
MessageController.get(this._editor).closeMessage();
const triggerPosition = this._editor.getPosition();
this._trigger({ type: 'manual', filter, autoApply, context: { notAvailableMessage, position: triggerPosition } });
}
private _trigger(trigger: CodeActionTrigger) {
return this._model.trigger(trigger);
}
private _handleLightBulbSelect(e: { x: number, y: number, state: CodeActionsState.Triggered }): void {
this._codeActionWidget.show(e.state.actions, e);
this._codeActionContextMenu.show(e.state.actions, e);
}
public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): Promise<CodeActionSet | undefined> {
return this._model.trigger({ type: 'manual', filter, autoApply });
}
private _updateLightBulbTitle(): void {
@@ -166,7 +142,7 @@ export class QuickFixController extends Disposable implements IEditorContributio
this._lightBulbWidget.title = title;
}
private _applyCodeAction(action: CodeAction): Promise<void> {
private _onApplyCodeAction(action: CodeAction): Promise<void> {
return applyCodeAction(action, this._bulkEditService, this._commandService, this._editor);
}
}
@@ -185,18 +161,28 @@ export async function applyCodeAction(
}
}
function triggerCodeActionsForEditorSelection(
function showCodeActionsForEditorSelection(
editor: ICodeEditor,
notAvailableMessage: string,
filter: CodeActionFilter | undefined,
autoApply: CodeActionAutoApply | undefined
): void {
if (editor.hasModel()) {
const controller = QuickFixController.get(editor);
if (controller) {
controller.manualTriggerAtCurrentPosition(notAvailableMessage, filter, autoApply);
}
filter?: CodeActionFilter,
autoApply?: CodeActionAutoApply
) {
if (!editor.hasModel()) {
return;
}
const controller = QuickFixController.get(editor);
if (!controller) {
return;
}
MessageController.get(editor).closeMessage();
const pos = editor.getPosition();
controller.triggerFromEditorSelection(filter, autoApply).then(codeActions => {
if (!codeActions || !codeActions.actions.length) {
MessageController.get(editor).showMessage(notAvailableMessage, pos);
}
});
}
export class QuickFixAction extends EditorAction {
@@ -218,7 +204,7 @@ export class QuickFixAction extends EditorAction {
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
return triggerCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), undefined, undefined);
return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"));
}
}
@@ -298,7 +284,7 @@ export class CodeActionCommand extends EditorCommand {
kind: CodeActionKind.Empty,
apply: CodeActionAutoApply.IfSingle,
});
return triggerCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"),
return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"),
{
kind: args.kind,
includeSourceActions: true,
@@ -361,7 +347,7 @@ export class RefactorAction extends EditorAction {
kind: CodeActionKind.Refactor,
apply: CodeActionAutoApply.Never
});
return triggerCodeActionsForEditorSelection(editor,
return showCodeActionsForEditorSelection(editor,
nls.localize('editor.action.refactor.noneMessage', "No refactorings available"),
{
kind: CodeActionKind.Refactor.contains(args.kind) ? args.kind : CodeActionKind.Empty,
@@ -416,7 +402,7 @@ export class SourceAction extends EditorAction {
kind: CodeActionKind.Source,
apply: CodeActionAutoApply.Never
});
return triggerCodeActionsForEditorSelection(editor,
return showCodeActionsForEditorSelection(editor,
nls.localize('editor.action.source.noneMessage', "No source actions available"),
{
kind: CodeActionKind.Source.contains(args.kind) ? args.kind : CodeActionKind.Empty,
@@ -448,7 +434,7 @@ export class OrganizeImportsAction extends EditorAction {
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
return triggerCodeActionsForEditorSelection(editor,
return showCodeActionsForEditorSelection(editor,
nls.localize('editor.action.organize.noneMessage', "No organize imports action available"),
{ kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true },
CodeActionAutoApply.IfSingle);
@@ -471,7 +457,7 @@ export class FixAllAction extends EditorAction {
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
return triggerCodeActionsForEditorSelection(editor,
return showCodeActionsForEditorSelection(editor,
nls.localize('fixAll.noneMessage', "No fix all action available"),
{ kind: CodeActionKind.SourceFixAll, includeSourceActions: true },
CodeActionAutoApply.IfSingle);
@@ -502,7 +488,7 @@ export class AutoFixAction extends EditorAction {
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
return triggerCodeActionsForEditorSelection(editor,
return showCodeActionsForEditorSelection(editor,
nls.localize('editor.action.autoFix.noneMessage', "No auto fixes available"),
{
kind: CodeActionKind.QuickFix,

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { CancelablePromise, createCancelablePromise, TimeoutTimer } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { dispose, Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
@@ -14,34 +14,36 @@ import { Selection } from 'vs/editor/common/core/selection';
import { CodeActionProviderRegistry } from 'vs/editor/common/modes';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IMarkerService } from 'vs/platform/markers/common/markers';
import { ILocalProgressService } from 'vs/platform/progress/common/progress';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { getCodeActions, CodeActionSet } from './codeAction';
import { CodeActionTrigger } from './codeActionTrigger';
export const SUPPORTED_CODE_ACTIONS = new RawContextKey<string>('supportedCodeAction', '');
export type TriggeredCodeAction = undefined | {
readonly selection: Selection;
readonly trigger: CodeActionTrigger;
readonly position: Position;
};
export class CodeActionOracle {
class CodeActionOracle extends Disposable {
private readonly _autoTriggerTimer = this._register(new TimeoutTimer());
private _disposables: IDisposable[] = [];
private readonly _autoTriggerTimer = new TimeoutTimer();
constructor(
private readonly _editor: ICodeEditor,
private readonly _markerService: IMarkerService,
private readonly _signalChange: (triggered: TriggeredCodeAction) => void,
private readonly _signalChange: (newState: CodeActionsState.State) => void,
private readonly _delay: number = 250,
private readonly _progressService?: IProgressService,
) {
super();
this._register(this._markerService.onMarkerChanged(e => this._onMarkerChanges(e)));
this._register(this._editor.onDidChangeCursorPosition(() => this._onCursorChange()));
this._disposables.push(
this._markerService.onMarkerChanged(e => this._onMarkerChanges(e)),
this._editor.onDidChangeCursorPosition(() => this._onCursorChange()),
);
}
public trigger(trigger: CodeActionTrigger): TriggeredCodeAction {
dispose(): void {
this._disposables = dispose(this._disposables);
this._autoTriggerTimer.cancel();
}
trigger(trigger: CodeActionTrigger) {
const selection = this._getRangeOfSelectionUnlessWhitespaceEnclosed(trigger);
return this._createEventAndSignalChange(trigger, selection);
}
@@ -110,24 +112,35 @@ class CodeActionOracle extends Disposable {
return selection ? selection : undefined;
}
private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): TriggeredCodeAction {
const model = this._editor.getModel();
if (!selection || !model) {
private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): Promise<CodeActionSet | undefined> {
if (!selection) {
// cancel
this._signalChange(undefined);
return undefined;
this._signalChange(CodeActionsState.Empty);
return Promise.resolve(undefined);
} else {
const model = this._editor.getModel();
if (!model) {
// cancel
this._signalChange(CodeActionsState.Empty);
return Promise.resolve(undefined);
}
const markerRange = this._getRangeOfMarker(selection);
const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition();
const actions = createCancelablePromise(token => getCodeActions(model, selection, trigger, token));
if (this._progressService && trigger.type === 'manual') {
this._progressService.showWhile(actions, 250);
}
this._signalChange(new CodeActionsState.Triggered(
trigger,
selection,
position,
actions
));
return actions;
}
const markerRange = this._getRangeOfMarker(selection);
const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition();
const e: TriggeredCodeAction = {
trigger,
selection,
position
};
this._signalChange(e);
return e;
}
}
@@ -154,35 +167,36 @@ export namespace CodeActionsState {
export type State = typeof Empty | Triggered;
}
export class CodeActionModel extends Disposable {
export class CodeActionModel {
private _codeActionOracle?: CodeActionOracle;
private _state: CodeActionsState.State = CodeActionsState.Empty;
private _onDidChangeState = new Emitter<CodeActionsState.State>();
private _disposables: IDisposable[] = [];
private readonly _supportedCodeActions: IContextKey<string>;
private readonly _onDidChangeState = this._register(new Emitter<CodeActionsState.State>());
public readonly onDidChangeState = this._onDidChangeState.event;
constructor(
private readonly _editor: ICodeEditor,
private readonly _markerService: IMarkerService,
contextKeyService: IContextKeyService,
private readonly _progressService?: ILocalProgressService
private readonly _progressService: IProgressService
) {
super();
this._supportedCodeActions = SUPPORTED_CODE_ACTIONS.bindTo(contextKeyService);
this._register(this._editor.onDidChangeModel(() => this._update()));
this._register(this._editor.onDidChangeModelLanguage(() => this._update()));
this._register(CodeActionProviderRegistry.onDidChange(() => this._update()));
this._disposables.push(this._editor.onDidChangeModel(() => this._update()));
this._disposables.push(this._editor.onDidChangeModelLanguage(() => this._update()));
this._disposables.push(CodeActionProviderRegistry.onDidChange(() => this._update()));
this._update();
}
dispose(): void {
super.dispose();
this._disposables = dispose(this._disposables);
dispose(this._codeActionOracle);
this.setState(CodeActionsState.Empty, true);
}
get onDidChangeState(): Event<CodeActionsState.State> {
return this._onDidChangeState.event;
}
private _update(): void {
@@ -191,6 +205,9 @@ export class CodeActionModel extends Disposable {
this._codeActionOracle = undefined;
}
if (this._state.type === CodeActionsState.Type.Triggered) {
this._state.actions.cancel();
}
this.setState(CodeActionsState.Empty);
const model = this._editor.getModel();
@@ -207,46 +224,25 @@ export class CodeActionModel extends Disposable {
this._supportedCodeActions.set(supportedActions.join(' '));
this._codeActionOracle = new CodeActionOracle(this._editor, this._markerService, trigger => {
if (!trigger) {
this.setState(CodeActionsState.Empty);
return;
}
const actions = createCancelablePromise(token => getCodeActions(model, trigger.selection, trigger.trigger, token));
if (this._progressService && trigger.trigger.type === 'manual') {
this._progressService.showWhile(actions, 250);
}
this.setState(new CodeActionsState.Triggered(trigger.trigger, trigger.selection, trigger.position, actions));
}, undefined);
this._codeActionOracle = new CodeActionOracle(this._editor, this._markerService, newState => this.setState(newState), undefined, this._progressService);
this._codeActionOracle.trigger({ type: 'auto' });
} else {
this._supportedCodeActions.reset();
}
}
public trigger(trigger: CodeActionTrigger) {
public trigger(trigger: CodeActionTrigger): Promise<CodeActionSet | undefined> {
if (this._codeActionOracle) {
this._codeActionOracle.trigger(trigger);
return this._codeActionOracle.trigger(trigger);
}
return Promise.resolve(undefined);
}
private setState(newState: CodeActionsState.State, skipNotify?: boolean) {
private setState(newState: CodeActionsState.State) {
if (newState === this._state) {
return;
}
// Cancel old request
if (this._state.type === CodeActionsState.Type.Triggered) {
this._state.actions.cancel();
}
this._state = newState;
if (!skipNotify) {
this._onDidChangeState.fire(newState);
}
this._onDidChangeState.fire(newState);
}
}

View File

@@ -5,7 +5,6 @@
import { startsWith } from 'vs/base/common/strings';
import { CodeAction } from 'vs/editor/common/modes';
import { Position } from 'vs/editor/common/core/position';
export class CodeActionKind {
private static readonly sep = '.';
@@ -91,8 +90,4 @@ export interface CodeActionTrigger {
readonly type: 'auto' | 'manual';
readonly filter?: CodeActionFilter;
readonly autoApply?: CodeActionAutoApply;
readonly context?: {
readonly notAvailableMessage: string;
readonly position: Position;
};
}

View File

@@ -6,6 +6,7 @@
import { getDomNodePagePosition } from 'vs/base/browser/dom';
import { Action } from 'vs/base/common/actions';
import { canceled } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { ScrollType } from 'vs/editor/common/editorCommon';
@@ -13,32 +14,25 @@ import { CodeAction } from 'vs/editor/common/modes';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction';
interface CodeActionWidgetDelegate {
onSelectCodeAction: (action: CodeAction) => Promise<any>;
}
export class CodeActionWidget {
export class CodeActionContextMenu {
private _visible: boolean;
private readonly _onDidExecuteCodeAction = new Emitter<void>();
public readonly onDidExecuteCodeAction: Event<void> = this._onDidExecuteCodeAction.event;
constructor(
private readonly _editor: ICodeEditor,
private readonly _contextMenuService: IContextMenuService,
private readonly _delegate: CodeActionWidgetDelegate
private readonly _onApplyCodeAction: (action: CodeAction) => Promise<any>
) { }
public async show(actionsToShow: Promise<CodeActionSet>, at?: { x: number; y: number } | Position): Promise<void> {
async show(actionsToShow: Promise<CodeActionSet>, at?: { x: number; y: number } | Position): Promise<void> {
const codeActions = await actionsToShow;
if (!codeActions.actions.length) {
this._visible = false;
return;
}
if (!this._editor.getDomNode()) {
// cancel when editor went off-dom
this._visible = false;
return Promise.reject(canceled());
}
this._visible = true;
const actions = codeActions.actions.map(action => this.codeActionToAction(action));
this._contextMenuService.showContextMenu({
@@ -60,7 +54,9 @@ export class CodeActionWidget {
private codeActionToAction(action: CodeAction): Action {
const id = action.command ? action.command.id : action.title;
const title = action.title;
return new Action(id, title, undefined, true, () => this._delegate.onSelectCodeAction(action));
return new Action(id, title, undefined, true, () =>
this._onApplyCodeAction(action)
.finally(() => this._onDidExecuteCodeAction.fire(undefined)));
}
get isVisible(): boolean {

View File

@@ -45,14 +45,13 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
this._futureFixes.cancel();
}
}));
this._register(dom.addStandardDisposableListener(this._domNode, 'mousedown', e => {
this._register(dom.addStandardDisposableListener(this._domNode, 'click', e => {
if (this._state.type !== CodeActionsState.Type.Triggered) {
return;
}
// Make sure that focus / cursor location is not lost when clicking widget icon
this._editor.focus();
dom.EventHelper.stop(e, true);
// a bit of extra work to make sure the menu
// doesn't cover the line-text
const { top, height } = dom.getDomNodePagePosition(this._domNode);
@@ -107,8 +106,9 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
return this._position;
}
tryShow(newState: CodeActionsState.Triggered) {
if (this._position && (!newState.position || this._position.position && this._position.position.lineNumber !== newState.position.lineNumber)) {
tryShow(newState: CodeActionsState.State) {
if (newState.type !== CodeActionsState.Type.Triggered || this._position && (!newState.position || this._position.position && this._position.position.lineNumber !== newState.position.lineNumber)) {
// hide when getting a 'hide'-request or when currently
// showing on another line
this.hide();
@@ -121,6 +121,10 @@ export class LightBulbWidget extends Disposable implements IContentWidget {
const { token } = this._futureFixes;
this._state = newState;
if (this._state.type === CodeActionsState.Empty.type) {
return;
}
const selection = this._state.rangeOrSelection;
this._state.actions.then(fixes => {
if (!token.isCancellationRequested && fixes.actions.length > 0 && selection) {

View File

@@ -3,34 +3,22 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { TextModel } from 'vs/editor/common/model/textModel';
import * as modes from 'vs/editor/common/modes';
import { CodeAction, CodeActionContext, CodeActionProvider, CodeActionProviderRegistry, Command, LanguageIdentifier, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes';
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers';
import { CancellationToken } from 'vs/base/common/cancellation';
function staticCodeActionProvider(...actions: modes.CodeAction[]): modes.CodeActionProvider {
return new class implements modes.CodeActionProvider {
provideCodeActions(): modes.CodeActionList {
return {
actions: actions,
dispose: () => { }
};
}
};
}
suite('CodeAction', () => {
let langId = new modes.LanguageIdentifier('fooLang', 17);
let langId = new LanguageIdentifier('fooLang', 17);
let uri = URI.parse('untitled:path');
let model: TextModel;
const disposables = new DisposableStore();
let disposables: IDisposable[] = [];
let testData = {
diagnostics: {
abc: {
@@ -58,7 +46,7 @@ suite('CodeAction', () => {
},
command: {
abc: {
command: new class implements modes.Command {
command: new class implements Command {
id: '1';
title: 'abc';
},
@@ -68,8 +56,8 @@ suite('CodeAction', () => {
spelling: {
bcd: {
diagnostics: <IMarkerData[]>[],
edit: new class implements modes.WorkspaceEdit {
edits: modes.ResourceTextEdit[];
edit: new class implements WorkspaceEdit {
edits: ResourceTextEdit[];
},
title: 'abc'
}
@@ -91,27 +79,30 @@ suite('CodeAction', () => {
};
setup(function () {
disposables.clear();
model = TextModel.createFromString('test1\ntest2\ntest3', undefined, langId, uri);
disposables.add(model);
disposables = [model];
});
teardown(function () {
disposables.clear();
dispose(disposables);
});
test('CodeActions are sorted by type, #38623', async function () {
const provider = staticCodeActionProvider(
testData.command.abc,
testData.diagnostics.bcd,
testData.spelling.bcd,
testData.tsLint.bcd,
testData.tsLint.abc,
testData.diagnostics.abc
);
const provider = new class implements CodeActionProvider {
provideCodeActions() {
return [
testData.command.abc,
testData.diagnostics.bcd,
testData.spelling.bcd,
testData.tsLint.bcd,
testData.tsLint.abc,
testData.diagnostics.abc
];
}
};
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
const expected = [
// CodeActions with a diagnostics array are shown first ordered by diagnostics.message
@@ -131,13 +122,17 @@ suite('CodeAction', () => {
});
test('getCodeActions should filter by scope', async function () {
const provider = staticCodeActionProvider(
{ title: 'a', kind: 'a' },
{ title: 'b', kind: 'b' },
{ title: 'a.b', kind: 'a.b' }
);
const provider = new class implements CodeActionProvider {
provideCodeActions(): CodeAction[] {
return [
{ title: 'a', kind: 'a' },
{ title: 'b', kind: 'b' },
{ title: 'a.b', kind: 'a.b' }
];
}
};
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
{
const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } }, CancellationToken.None);
@@ -159,18 +154,15 @@ suite('CodeAction', () => {
});
test('getCodeActions should forward requested scope to providers', async function () {
const provider = new class implements modes.CodeActionProvider {
provideCodeActions(_model: any, _range: Range, context: modes.CodeActionContext, _token: any): modes.CodeActionList {
return {
actions: [
{ title: context.only || '', kind: context.only }
],
dispose: () => { }
};
const provider = new class implements CodeActionProvider {
provideCodeActions(_model: any, _range: Range, context: CodeActionContext, _token: any): CodeAction[] {
return [
{ title: context.only || '', kind: context.only }
];
}
};
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } }, CancellationToken.None);
assert.equal(actions.length, 1);
@@ -178,12 +170,16 @@ suite('CodeAction', () => {
});
test('getCodeActions should not return source code action by default', async function () {
const provider = staticCodeActionProvider(
{ title: 'a', kind: CodeActionKind.Source.value },
{ title: 'b', kind: 'b' }
);
const provider = new class implements CodeActionProvider {
provideCodeActions(): CodeAction[] {
return [
{ title: 'a', kind: CodeActionKind.Source.value },
{ title: 'b', kind: 'b' }
];
}
};
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
{
const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto' }, CancellationToken.None);
@@ -200,16 +196,16 @@ suite('CodeAction', () => {
test('getCodeActions should not invoke code action providers filtered out by providedCodeActionKinds', async function () {
let wasInvoked = false;
const provider = new class implements modes.CodeActionProvider {
provideCodeActions(): modes.CodeActionList {
const provider = new class implements CodeActionProvider {
provideCodeActions() {
wasInvoked = true;
return { actions: [], dispose: () => { } };
return [];
}
providedCodeActionKinds = [CodeActionKind.Refactor.value];
};
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), {
type: 'auto',

View File

@@ -4,38 +4,32 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Selection } from 'vs/editor/common/core/selection';
import { TextModel } from 'vs/editor/common/model/textModel';
import * as modes from 'vs/editor/common/modes';
import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/codeActionModel';
import { CodeActionProviderRegistry, LanguageIdentifier } from 'vs/editor/common/modes';
import { CodeActionOracle, CodeActionsState } from 'vs/editor/contrib/codeAction/codeActionModel';
import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { MarkerService } from 'vs/platform/markers/common/markerService';
const testProvider = {
provideCodeActions(): modes.CodeActionList {
return {
actions: [
{ title: 'test', command: { id: 'test-command', title: 'test', arguments: [] } }
],
dispose() { /* noop*/ }
};
provideCodeActions() {
return [{ id: 'test-command', title: 'test', arguments: [] }];
}
};
suite('CodeActionModel', () => {
suite('CodeAction', () => {
const languageIdentifier = new modes.LanguageIdentifier('foo-lang', 3);
const languageIdentifier = new LanguageIdentifier('foo-lang', 3);
let uri = URI.parse('untitled:path');
let model: TextModel;
let markerService: MarkerService;
let editor: ICodeEditor;
const disposables = new DisposableStore();
let disposables: IDisposable[];
setup(() => {
disposables.clear();
disposables = [];
markerService = new MarkerService();
model = TextModel.createFromString('foobar foo bar\nfarboo far boo', undefined, languageIdentifier, uri);
editor = createTestCodeEditor({ model: model });
@@ -43,28 +37,26 @@ suite('CodeActionModel', () => {
});
teardown(() => {
disposables.clear();
dispose(disposables);
editor.dispose();
model.dispose();
markerService.dispose();
});
test('Orcale -> marker added', done => {
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.add(reg);
const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.push(reg);
const contextKeys = new MockContextKeyService();
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
disposables.add(model.onDidChangeState((e: CodeActionsState.Triggered) => {
const oracle = new CodeActionOracle(editor, markerService, (e: CodeActionsState.Triggered) => {
assert.equal(e.trigger.type, 'auto');
assert.ok(e.actions);
e.actions.then(fixes => {
model.dispose();
oracle.dispose();
assert.equal(fixes.actions.length, 1);
done();
}, done);
}));
});
// start here
markerService.changeOne('fake', uri, [{
@@ -78,8 +70,8 @@ suite('CodeActionModel', () => {
});
test('Orcale -> position changed', () => {
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.add(reg);
const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.push(reg);
markerService.changeOne('fake', uri, [{
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
@@ -92,29 +84,28 @@ suite('CodeActionModel', () => {
editor.setPosition({ lineNumber: 2, column: 1 });
return new Promise((resolve, reject) => {
const contextKeys = new MockContextKeyService();
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
disposables.add(model.onDidChangeState((e: CodeActionsState.Triggered) => {
const oracle = new CodeActionOracle(editor, markerService, (e: CodeActionsState.Triggered) => {
assert.equal(e.trigger.type, 'auto');
assert.ok(e.actions);
e.actions.then(fixes => {
model.dispose();
oracle.dispose();
assert.equal(fixes.actions.length, 1);
resolve(undefined);
}, reject);
}));
});
// start here
editor.setPosition({ lineNumber: 1, column: 1 });
});
});
test('Lightbulb is in the wrong place, #29933', async function () {
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, {
provideCodeActions(_doc, _range): modes.CodeActionList {
return { actions: [], dispose() { /* noop*/ } };
const reg = CodeActionProviderRegistry.register(languageIdentifier.language, {
provideCodeActions(_doc, _range) {
return [];
}
});
disposables.add(reg);
disposables.push(reg);
editor.getModel()!.setValue('// @ts-check\n2\ncon\n');
@@ -128,9 +119,8 @@ suite('CodeActionModel', () => {
// case 1 - drag selection over multiple lines -> range of enclosed marker, position or marker
await new Promise(resolve => {
const contextKeys = new MockContextKeyService();
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
disposables.add(model.onDidChangeState((e: CodeActionsState.Triggered) => {
let oracle = new CodeActionOracle(editor, markerService, (e: CodeActionsState.Triggered) => {
assert.equal(e.trigger.type, 'auto');
const selection = <Selection>e.rangeOrSelection;
assert.deepEqual(selection.selectionStartLineNumber, 1);
@@ -138,32 +128,31 @@ suite('CodeActionModel', () => {
assert.deepEqual(selection.endLineNumber, 4);
assert.deepEqual(selection.endColumn, 1);
assert.deepEqual(e.position, { lineNumber: 3, column: 1 });
model.dispose();
oracle.dispose();
resolve(undefined);
}, 5));
}, 5);
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
});
});
test('Orcale -> should only auto trigger once for cursor and marker update right after each other', done => {
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.add(reg);
const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.push(reg);
let triggerCount = 0;
const contextKeys = new MockContextKeyService();
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
disposables.add(model.onDidChangeState((e: CodeActionsState.Triggered) => {
const oracle = new CodeActionOracle(editor, markerService, (e: CodeActionsState.Triggered) => {
assert.equal(e.trigger.type, 'auto');
++triggerCount;
// give time for second trigger before completing test
setTimeout(() => {
model.dispose();
oracle.dispose();
assert.strictEqual(triggerCount, 1);
done();
}, 50);
}, 5 /*delay*/));
}, 5 /*delay*/);
markerService.changeOne('fake', uri, [{
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,