Merge VS Code 1.31.1 (#4283)

This commit is contained in:
Matt Irvine
2019-03-15 13:09:45 -07:00
committed by GitHub
parent 7d31575149
commit 86bac90001
1716 changed files with 53308 additions and 48375 deletions

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { flatten, mergeSort, isNonEmptyArray } from 'vs/base/common/arrays';
import { flatten, isNonEmptyArray, mergeSort } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
@@ -13,73 +13,59 @@ import { Selection } from 'vs/editor/common/core/selection';
import { ITextModel } from 'vs/editor/common/model';
import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes';
import { IModelService } from 'vs/editor/common/services/modelService';
import { CodeActionFilter, CodeActionKind, CodeActionTrigger } from './codeActionTrigger';
import { CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind, CodeActionFilter } from './codeActionTrigger';
export function getCodeActions(
model: ITextModel,
rangeOrSelection: Range | Selection,
trigger: CodeActionTrigger,
token: CancellationToken
): Promise<CodeAction[]> {
const filter = trigger.filter || {};
export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Selection, trigger?: CodeActionTrigger, token: CancellationToken = CancellationToken.None): Promise<CodeAction[]> {
const codeActionContext: CodeActionContext = {
only: trigger && trigger.filter && trigger.filter.kind ? trigger.filter.kind.value : undefined,
trigger: trigger && trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic
only: filter.kind ? filter.kind.value : undefined,
trigger: trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic
};
const promises = CodeActionProviderRegistry.all(model)
.filter(provider => {
if (!provider.providedCodeActionKinds) {
return true;
if (filter.kind && CodeActionKind.Source.contains(filter.kind) && rangeOrSelection.isEmpty()) {
rangeOrSelection = model.getFullModelRange();
}
const promises = getCodeActionProviders(model, filter).map(provider => {
return Promise.resolve(provider.provideCodeActions(model, rangeOrSelection, codeActionContext, token)).then(providedCodeActions => {
if (!Array.isArray(providedCodeActions)) {
return [];
}
return providedCodeActions.filter(action => action && filtersAction(filter, action));
}, (err): CodeAction[] => {
if (isPromiseCanceledError(err)) {
throw err;
}
// Avoid calling providers that we know will not return code actions of interest
return provider.providedCodeActionKinds.some(providedKind => {
// Filter out actions by kind
// The provided kind can be either a subset of a superset of the filtered kind
if (trigger && trigger.filter && trigger.filter.kind && !(trigger.filter.kind.contains(providedKind) || new CodeActionKind(providedKind).contains(trigger.filter.kind.value))) {
return false;
}
// Don't return source actions unless they are explicitly requested
if (trigger && CodeActionKind.Source.contains(providedKind) && (!trigger.filter || !trigger.filter.includeSourceActions)) {
return false;
}
return true;
});
})
.map(support => {
return Promise.resolve(support.provideCodeActions(model, rangeOrSelection, codeActionContext, token)).then(providedCodeActions => {
if (!Array.isArray(providedCodeActions)) {
return [];
}
return providedCodeActions.filter(action => isValidAction(trigger && trigger.filter, action));
}, (err): CodeAction[] => {
if (isPromiseCanceledError(err)) {
throw err;
}
onUnexpectedExternalError(err);
return [];
});
onUnexpectedExternalError(err);
return [];
});
});
return Promise.all(promises)
.then(flatten)
.then(allCodeActions => mergeSort(allCodeActions, codeActionsComparator));
}
function isValidAction(filter: CodeActionFilter | undefined, action: CodeAction): boolean {
return action && isValidActionKind(filter, action.kind);
}
function isValidActionKind(filter: CodeActionFilter | undefined, kind: string | undefined): boolean {
// Filter out actions by kind
if (filter && filter.kind && (!kind || !filter.kind.contains(kind))) {
return false;
}
// Don't return source actions unless they are explicitly requested
if (kind && CodeActionKind.Source.contains(kind) && (!filter || !filter.includeSourceActions)) {
return false;
}
return true;
function getCodeActionProviders(
model: ITextModel,
filter: CodeActionFilter
) {
return CodeActionProviderRegistry.all(model)
// Don't include providers that we know will not return code actions of interest
.filter(provider => {
if (!provider.providedCodeActionKinds) {
// We don't know what type of actions this provider will return.
return true;
}
return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new CodeActionKind(kind)));
});
}
function codeActionsComparator(a: CodeAction, b: CodeAction): number {
@@ -107,5 +93,9 @@ registerLanguageCommand('_executeCodeActionProvider', function (accessor, args)
throw illegalArgument();
}
return getCodeActions(model, model.validateRange(range), { type: 'manual', filter: { includeSourceActions: true } });
return getCodeActions(
model,
model.validateRange(range),
{ type: 'manual', filter: { includeSourceActions: true } },
CancellationToken.None);
});

View File

@@ -21,7 +21,7 @@ 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 { IProgressService } from 'vs/platform/progress/common/progress';
import { CodeActionModel, CodeActionsComputeEvent, SUPPORTED_CODE_ACTIONS } from './codeActionModel';
import { CodeActionModel, SUPPORTED_CODE_ACTIONS, CodeActionsState } from './codeActionModel';
import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeActionTrigger';
import { CodeActionContextMenu } from './codeActionWidget';
import { LightBulbWidget } from './lightBulbWidget';
@@ -69,7 +69,7 @@ export class QuickFixController implements IEditorContribution {
this._disposables.push(
this._codeActionContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto', filter: {} })),
this._lightBulbWidget.onClick(this._handleLightBulbSelect, this),
this._model.onDidChangeFixes(e => this._onCodeActionsEvent(e)),
this._model.onDidChangeState(e => this._onDidChangeCodeActionsState(e)),
this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitle, this)
);
}
@@ -79,39 +79,39 @@ export class QuickFixController implements IEditorContribution {
dispose(this._disposables);
}
private _onCodeActionsEvent(e: CodeActionsComputeEvent): void {
private _onDidChangeCodeActionsState(newState: CodeActionsState.State): void {
if (this._activeRequest) {
this._activeRequest.cancel();
this._activeRequest = undefined;
}
if (e && e.actions) {
this._activeRequest = e.actions;
}
if (newState.type === CodeActionsState.Type.Triggered) {
this._activeRequest = newState.actions;
if (e && e.actions && e.trigger.filter && e.trigger.filter.kind) {
// Triggered for specific scope
// Apply if we only have one action or requested autoApply, otherwise show menu
e.actions.then(fixes => {
if (fixes.length > 0 && e.trigger.autoApply === CodeActionAutoApply.First || (e.trigger.autoApply === CodeActionAutoApply.IfSingle && fixes.length === 1)) {
this._onApplyCodeAction(fixes[0]);
} else {
this._codeActionContextMenu.show(e.actions, e.position);
}
}).catch(onUnexpectedError);
return;
}
if (newState.trigger.filter && newState.trigger.filter.kind) {
// Triggered for specific scope
newState.actions.then(fixes => {
if (fixes.length > 0) {
// Apply if we only have one action or requested autoApply
if (newState.trigger.autoApply === CodeActionAutoApply.First || (newState.trigger.autoApply === CodeActionAutoApply.IfSingle && fixes.length === 1)) {
this._onApplyCodeAction(fixes[0]);
return;
}
}
this._codeActionContextMenu.show(newState.actions, newState.position);
if (e && e.trigger.type === 'manual') {
this._codeActionContextMenu.show(e.actions, e.position);
} else if (e && e.actions) {
// auto magically triggered
// * update an existing list of code actions
// * manage light bulb
if (this._codeActionContextMenu.isVisible) {
this._codeActionContextMenu.show(e.actions, e.position);
}).catch(onUnexpectedError);
} else if (newState.trigger.type === 'manual') {
this._codeActionContextMenu.show(newState.actions, newState.position);
} else {
this._lightBulbWidget.model = e;
// auto magically triggered
// * update an existing list of code actions
// * manage light bulb
if (this._codeActionContextMenu.isVisible) {
this._codeActionContextMenu.show(newState.actions, newState.position);
} else {
this._lightBulbWidget.tryShow(newState);
}
}
} else {
this._lightBulbWidget.hide();
@@ -122,13 +122,11 @@ export class QuickFixController implements IEditorContribution {
return QuickFixController.ID;
}
private _handleLightBulbSelect(coords: { x: number, y: number }): void {
if (this._lightBulbWidget.model && this._lightBulbWidget.model.actions) {
this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords);
}
private _handleLightBulbSelect(e: { x: number, y: number, state: CodeActionsState.Triggered }): void {
this._codeActionContextMenu.show(e.state.actions, e);
}
public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): Thenable<CodeAction[] | undefined> {
public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): Promise<CodeAction[] | undefined> {
return this._model.trigger({ type: 'manual', filter, autoApply });
}
@@ -158,7 +156,7 @@ export async function applyCodeAction(
await bulkEditService.apply(action.edit, { editor });
}
if (action.command) {
await commandService.executeCommand(action.command.id, ...action.command.arguments);
await commandService.executeCommand(action.command.id, ...(action.command.arguments || []));
}
}
@@ -168,6 +166,10 @@ function showCodeActionsForEditorSelection(
filter?: CodeActionFilter,
autoApply?: CodeActionAutoApply
) {
if (!editor.hasModel()) {
return;
}
const controller = QuickFixController.get(editor);
if (!controller) {
return;
@@ -206,38 +208,41 @@ export class QuickFixAction extends EditorAction {
class CodeActionCommandArgs {
public static fromUser(arg: any): CodeActionCommandArgs {
public static fromUser(arg: any, defaults: { kind: CodeActionKind, apply: CodeActionAutoApply }): CodeActionCommandArgs {
if (!arg || typeof arg !== 'object') {
return new CodeActionCommandArgs(CodeActionKind.Empty, CodeActionAutoApply.IfSingle);
return new CodeActionCommandArgs(defaults.kind, defaults.apply, false);
}
return new CodeActionCommandArgs(
CodeActionCommandArgs.getKindFromUser(arg),
CodeActionCommandArgs.getApplyFromUser(arg));
CodeActionCommandArgs.getKindFromUser(arg, defaults.kind),
CodeActionCommandArgs.getApplyFromUser(arg, defaults.apply),
CodeActionCommandArgs.getPreferredUser(arg));
}
private static getApplyFromUser(arg: any) {
private static getApplyFromUser(arg: any, defaultAutoApply: CodeActionAutoApply) {
switch (typeof arg.apply === 'string' ? arg.apply.toLowerCase() : '') {
case 'first':
return CodeActionAutoApply.First;
case 'never':
return CodeActionAutoApply.Never;
case 'ifsingle':
default:
return CodeActionAutoApply.IfSingle;
case 'first': return CodeActionAutoApply.First;
case 'never': return CodeActionAutoApply.Never;
case 'ifsingle': return CodeActionAutoApply.IfSingle;
default: return defaultAutoApply;
}
}
private static getKindFromUser(arg: any) {
private static getKindFromUser(arg: any, defaultKind: CodeActionKind) {
return typeof arg.kind === 'string'
? new CodeActionKind(arg.kind)
: CodeActionKind.Empty;
: defaultKind;
}
private static getPreferredUser(arg: any): boolean {
return typeof arg.preferred === 'boolean'
? arg.preferred
: false;
}
private constructor(
public readonly kind: CodeActionKind,
public readonly apply: CodeActionAutoApply
public readonly apply: CodeActionAutoApply,
public readonly preferred: boolean,
) { }
}
@@ -253,8 +258,17 @@ export class CodeActionCommand extends EditorCommand {
}
public runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) {
const args = CodeActionCommandArgs.fromUser(userArg);
return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), { kind: args.kind, includeSourceActions: true }, args.apply);
const args = CodeActionCommandArgs.fromUser(userArg, {
kind: CodeActionKind.Empty,
apply: CodeActionAutoApply.IfSingle,
});
return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"),
{
kind: args.kind,
includeSourceActions: true,
onlyIncludePreferredActions: args.preferred,
},
args.apply);
}
}
@@ -287,11 +301,18 @@ export class RefactorAction extends EditorAction {
});
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
public run(_accessor: ServicesAccessor, editor: ICodeEditor, userArg: any): void {
const args = CodeActionCommandArgs.fromUser(userArg, {
kind: CodeActionKind.Refactor,
apply: CodeActionAutoApply.Never
});
return showCodeActionsForEditorSelection(editor,
nls.localize('editor.action.refactor.noneMessage', "No refactorings available"),
{ kind: CodeActionKind.Refactor },
CodeActionAutoApply.Never);
{
kind: CodeActionKind.Refactor.contains(args.kind) ? args.kind : CodeActionKind.Empty,
onlyIncludePreferredActions: args.preferred,
},
args.apply);
}
}
@@ -316,11 +337,19 @@ export class SourceAction extends EditorAction {
});
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
public run(_accessor: ServicesAccessor, editor: ICodeEditor, userArg: any): void {
const args = CodeActionCommandArgs.fromUser(userArg, {
kind: CodeActionKind.Source,
apply: CodeActionAutoApply.Never
});
return showCodeActionsForEditorSelection(editor,
nls.localize('editor.action.source.noneMessage', "No source actions available"),
{ kind: CodeActionKind.Source, includeSourceActions: true },
CodeActionAutoApply.Never);
{
kind: CodeActionKind.Source.contains(args.kind) ? args.kind : CodeActionKind.Empty,
includeSourceActions: true,
onlyIncludePreferredActions: args.preferred,
},
args.apply);
}
}
@@ -350,4 +379,38 @@ export class OrganizeImportsAction extends EditorAction {
{ kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true },
CodeActionAutoApply.IfSingle);
}
}
}
export class AutoFixAction extends EditorAction {
static readonly Id = 'editor.action.autoFix';
constructor() {
super({
id: AutoFixAction.Id,
label: nls.localize('autoFix.label', "Auto Fix..."),
alias: 'Auto Fix',
precondition: ContextKeyExpr.and(
EditorContextKeys.writable,
contextKeyForSupportedActions(CodeActionKind.QuickFix)),
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.US_DOT,
mac: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_DOT
},
weight: KeybindingWeight.EditorContrib
}
});
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
return showCodeActionsForEditorSelection(editor,
nls.localize('editor.action.autoFix.noneMessage', "No auto fixes available"),
{
kind: CodeActionKind.QuickFix,
onlyIncludePreferredActions: true
},
CodeActionAutoApply.IfSingle);
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { CodeActionCommand, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, SourceAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
import { CodeActionCommand, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, SourceAction, AutoFixAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
registerEditorContribution(QuickFixController);
@@ -12,4 +12,5 @@ registerEditorAction(QuickFixAction);
registerEditorAction(RefactorAction);
registerEditorAction(SourceAction);
registerEditorAction(OrganizeImportsAction);
registerEditorAction(AutoFixAction);
registerEditorCommand(new CodeActionCommand());

View File

@@ -28,7 +28,7 @@ export class CodeActionOracle {
constructor(
private _editor: ICodeEditor,
private readonly _markerService: IMarkerService,
private _signalChange: (e: CodeActionsComputeEvent) => any,
private _signalChange: (newState: CodeActionsState.State) => void,
private readonly _delay: number = 250,
private readonly _progressService?: IProgressService,
) {
@@ -112,26 +112,16 @@ export class CodeActionOracle {
return selection ? selection : undefined;
}
private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): Thenable<CodeAction[] | undefined> {
private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): Promise<CodeAction[] | undefined> {
if (!selection) {
// cancel
this._signalChange({
trigger,
rangeOrSelection: undefined,
position: undefined,
actions: undefined,
});
this._signalChange(CodeActionsState.Empty);
return Promise.resolve(undefined);
} else {
const model = this._editor.getModel();
if (!model) {
// cancel
this._signalChange({
trigger,
rangeOrSelection: undefined,
position: undefined,
actions: undefined,
});
this._signalChange(CodeActionsState.Empty);
return Promise.resolve(undefined);
}
@@ -143,22 +133,38 @@ export class CodeActionOracle {
this._progressService.showWhile(actions, 250);
}
this._signalChange({
this._signalChange(new CodeActionsState.Triggered(
trigger,
rangeOrSelection: selection,
selection,
position,
actions
});
));
return actions;
}
}
}
export interface CodeActionsComputeEvent {
trigger: CodeActionTrigger;
rangeOrSelection: Range | Selection | undefined;
position: Position | undefined;
actions: CancelablePromise<CodeAction[]> | undefined;
export namespace CodeActionsState {
export const enum Type {
Empty,
Triggered,
}
export const Empty = new class { readonly type = Type.Empty; };
export class Triggered {
readonly type = Type.Triggered;
constructor(
public readonly trigger: CodeActionTrigger,
public readonly rangeOrSelection: Range | Selection,
public readonly position: Position,
public readonly actions: CancelablePromise<CodeAction[]>,
) { }
}
export type State = typeof Empty | Triggered;
}
export class CodeActionModel {
@@ -166,7 +172,8 @@ export class CodeActionModel {
private _editor: ICodeEditor;
private _markerService: IMarkerService;
private _codeActionOracle?: CodeActionOracle;
private _onDidChangeFixes = new Emitter<CodeActionsComputeEvent>();
private _state: CodeActionsState.State = CodeActionsState.Empty;
private _onDidChangeState = new Emitter<CodeActionsState.State>();
private _disposables: IDisposable[] = [];
private readonly _supportedCodeActions: IContextKey<string>;
@@ -178,7 +185,7 @@ export class CodeActionModel {
this._disposables.push(this._editor.onDidChangeModel(() => this._update()));
this._disposables.push(this._editor.onDidChangeModelLanguage(() => this._update()));
this._disposables.push(CodeActionProviderRegistry.onDidChange(this._update, this));
this._disposables.push(CodeActionProviderRegistry.onDidChange(() => this._update()));
this._update();
}
@@ -188,23 +195,26 @@ export class CodeActionModel {
dispose(this._codeActionOracle);
}
get onDidChangeFixes(): Event<CodeActionsComputeEvent> {
return this._onDidChangeFixes.event;
get onDidChangeState(): Event<CodeActionsState.State> {
return this._onDidChangeState.event;
}
private _update(): void {
if (this._codeActionOracle) {
this._codeActionOracle.dispose();
this._codeActionOracle = undefined;
this._onDidChangeFixes.fire(undefined);
}
if (this._state.type === CodeActionsState.Type.Triggered) {
// this._state.actions.cancel();
}
this.setState(CodeActionsState.Empty);
const model = this._editor.getModel();
if (model
&& CodeActionProviderRegistry.has(model)
&& !this._editor.getConfiguration().readOnly) {
&& !this._editor.getConfiguration().readOnly
) {
const supportedActions: string[] = [];
for (const provider of CodeActionProviderRegistry.all(model)) {
if (Array.isArray(provider.providedCodeActionKinds)) {
@@ -214,17 +224,25 @@ export class CodeActionModel {
this._supportedCodeActions.set(supportedActions.join(' '));
this._codeActionOracle = new CodeActionOracle(this._editor, this._markerService, p => this._onDidChangeFixes.fire(p), undefined, this._progressService);
this._codeActionOracle = new CodeActionOracle(this._editor, this._markerService, newState => this.setState(newState), undefined, this._progressService);
this._codeActionOracle.trigger({ type: 'auto' });
} else {
this._supportedCodeActions.reset();
}
}
trigger(trigger: CodeActionTrigger): Thenable<CodeAction[] | undefined> {
public trigger(trigger: CodeActionTrigger): Promise<CodeAction[] | undefined> {
if (this._codeActionOracle) {
return this._codeActionOracle.trigger(trigger);
}
return Promise.resolve(undefined);
}
private setState(newState: CodeActionsState.State) {
if (newState === this._state) {
return;
}
this._state = newState;
this._onDidChangeState.fire(newState);
}
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { startsWith } from 'vs/base/common/strings';
import { CodeAction } from 'vs/editor/common/modes';
export class CodeActionKind {
private static readonly sep = '.';
@@ -13,25 +14,72 @@ export class CodeActionKind {
public static readonly Refactor = new CodeActionKind('refactor');
public static readonly Source = new CodeActionKind('source');
public static readonly SourceOrganizeImports = new CodeActionKind('source.organizeImports');
public static readonly SourceFixAll = new CodeActionKind('source.fixAll');
constructor(
public readonly value: string
) { }
public contains(other: string): boolean {
return this.value === other || startsWith(other, this.value + CodeActionKind.sep);
public contains(other: CodeActionKind): boolean {
return this.value === other.value || startsWith(other.value, this.value + CodeActionKind.sep);
}
public intersects(other: CodeActionKind): boolean {
return this.contains(other) || other.contains(this);
}
}
export const enum CodeActionAutoApply {
IfSingle = 1,
First = 2,
Never = 3
IfSingle,
First,
Never,
}
export interface CodeActionFilter {
readonly kind?: CodeActionKind;
readonly includeSourceActions?: boolean;
readonly onlyIncludePreferredActions?: boolean;
}
export function mayIncludeActionsOfKind(filter: CodeActionFilter, providedKind: CodeActionKind): boolean {
// A provided kind may be a subset or superset of our filtered kind.
if (filter.kind && !filter.kind.intersects(providedKind)) {
return false;
}
// Don't return source actions unless they are explicitly requested
if (CodeActionKind.Source.contains(providedKind) && !filter.includeSourceActions) {
return false;
}
return true;
}
export function filtersAction(filter: CodeActionFilter, action: CodeAction): boolean {
const actionKind = action.kind ? new CodeActionKind(action.kind) : undefined;
// Filter out actions by kind
if (filter.kind) {
if (!actionKind || !filter.kind.contains(actionKind)) {
return false;
}
}
// Don't return source actions unless they are explicitly requested
if (!filter.includeSourceActions) {
if (actionKind && CodeActionKind.Source.contains(actionKind)) {
return false;
}
}
if (filter.onlyIncludePreferredActions) {
if (!action.isPreferred) {
return false;
}
}
return true;
}
export interface CodeActionTrigger {

View File

@@ -5,7 +5,6 @@
import { getDomNodePagePosition } from 'vs/base/browser/dom';
import { Action } from 'vs/base/common/actions';
import { always } from 'vs/base/common/async';
import { canceled } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
@@ -27,48 +26,45 @@ export class CodeActionContextMenu {
private readonly _onApplyCodeAction: (action: CodeAction) => Promise<any>
) { }
show(fixes: Thenable<CodeAction[]>, at: { x: number; y: number } | Position) {
const actionsPromise = fixes ? fixes.then(value => {
return value.map(action => {
return new Action(action.command ? action.command.id : action.title, action.title, undefined, true, () => {
return always(
this._onApplyCodeAction(action),
() => this._onDidExecuteCodeAction.fire(undefined));
});
});
}).then(actions => {
if (!this._editor.getDomNode()) {
// cancel when editor went off-dom
return Promise.reject(canceled());
}
return actions;
}) : Promise.resolve([] as Action[]);
actionsPromise.then(actions => {
this._contextMenuService.showContextMenu({
getAnchor: () => {
if (Position.isIPosition(at)) {
at = this._toCoords(at);
}
return at;
},
getActions: () => actions,
onHide: () => {
this._visible = false;
this._editor.focus();
},
autoSelectFirstItem: true
});
async show(actionsToShow: Promise<CodeAction[]>, at?: { x: number; y: number } | Position): Promise<void> {
const codeActions = await actionsToShow;
if (!this._editor.getDomNode()) {
// cancel when editor went off-dom
return Promise.reject(canceled());
}
const actions = codeActions.map(action => this.codeActionToAction(action));
this._contextMenuService.showContextMenu({
getAnchor: () => {
if (Position.isIPosition(at)) {
at = this._toCoords(at);
}
return at || { x: 0, y: 0 };
},
getActions: () => actions,
onHide: () => {
this._visible = false;
this._editor.focus();
},
autoSelectFirstItem: true
});
}
private codeActionToAction(action: CodeAction): Action {
const id = action.command ? action.command.id : action.title;
const title = action.isPreferred ? `${action.title}` : action.title;
return new Action(id, title, undefined, true, () =>
this._onApplyCodeAction(action)
.finally(() => this._onDidExecuteCodeAction.fire(undefined)));
}
get isVisible(): boolean {
return this._visible;
}
private _toCoords(position: Position): { x: number, y: number } {
if (!this._editor.hasModel()) {
return { x: 0, y: 0 };
}
this._editor.revealPosition(position, ScrollType.Immediate);
this._editor.render();

View File

@@ -6,45 +6,49 @@
import * as dom from 'vs/base/browser/dom';
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import 'vs/css!./lightBulbWidget';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { TextModel } from 'vs/editor/common/model/textModel';
import { CodeActionsComputeEvent } from './codeActionModel';
import { CodeActionsState } from './codeActionModel';
export class LightBulbWidget implements IDisposable, IContentWidget {
export class LightBulbWidget extends Disposable implements IContentWidget {
private static readonly _posPref = [ContentWidgetPositionPreference.EXACT];
private readonly _domNode: HTMLDivElement;
private readonly _editor: ICodeEditor;
private readonly _disposables: IDisposable[] = [];
private readonly _onClick = new Emitter<{ x: number, y: number }>();
readonly onClick: Event<{ x: number, y: number }> = this._onClick.event;
private readonly _onClick = this._register(new Emitter<{ x: number; y: number; state: CodeActionsState.Triggered }>());
public readonly onClick = this._onClick.event;
private _position: IContentWidgetPosition | null;
private _model: CodeActionsComputeEvent | null;
private _state: CodeActionsState.State = CodeActionsState.Empty;
private _futureFixes = new CancellationTokenSource();
constructor(editor: ICodeEditor) {
super();
this._domNode = document.createElement('div');
this._domNode.className = 'lightbulb-glyph';
this._editor = editor;
this._editor.addContentWidget(this);
this._disposables.push(this._editor.onDidChangeModel(_ => this._futureFixes.cancel()));
this._disposables.push(this._editor.onDidChangeModelLanguage(_ => this._futureFixes.cancel()));
this._disposables.push(this._editor.onDidChangeModelContent(_ => {
this._register(this._editor.onDidChangeModel(_ => this._futureFixes.cancel()));
this._register(this._editor.onDidChangeModelLanguage(_ => this._futureFixes.cancel()));
this._register(this._editor.onDidChangeModelContent(_ => {
// cancel when the line in question has been removed
const editorModel = this._editor.getModel();
if (!this.model || !this.model.position || !editorModel || this.model.position.lineNumber >= editorModel.getLineCount()) {
if (this._state.type !== CodeActionsState.Type.Triggered || !editorModel || this._state.position.lineNumber >= editorModel.getLineCount()) {
this._futureFixes.cancel();
}
}));
this._disposables.push(dom.addStandardDisposableListener(this._domNode, 'click', 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();
// a bit of extra work to make sure the menu
@@ -53,16 +57,17 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
const { lineHeight } = this._editor.getConfiguration();
let pad = Math.floor(lineHeight / 3);
if (this._position && this._model && this._model.position && this._position.position !== null && this._position.position.lineNumber < this._model.position.lineNumber) {
if (this._position && this._position.position !== null && this._position.position.lineNumber < this._state.position.lineNumber) {
pad += lineHeight;
}
this._onClick.fire({
x: e.posx,
y: top + height + pad
y: top + height + pad,
state: this._state
});
}));
this._disposables.push(dom.addDisposableListener(this._domNode, 'mouseenter', (e: MouseEvent) => {
this._register(dom.addDisposableListener(this._domNode, 'mouseenter', (e: MouseEvent) => {
if ((e.buttons & 1) !== 1) {
return;
}
@@ -75,7 +80,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
monitor.dispose();
});
}));
this._disposables.push(this._editor.onDidChangeConfiguration(e => {
this._register(this._editor.onDidChangeConfiguration(e => {
// hide when told to do so
if (e.contribInfo && !this._editor.getConfiguration().contribInfo.lightbulbEnabled) {
this.hide();
@@ -84,7 +89,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
}
dispose(): void {
dispose(this._disposables);
super.dispose();
this._editor.removeContentWidget(this);
}
@@ -100,9 +105,9 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
return this._position;
}
set model(value: CodeActionsComputeEvent | null) {
tryShow(newState: CodeActionsState.State) {
if (!value || this._position && (!value.position || this._position.position && this._position.position.lineNumber !== value.position.lineNumber)) {
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();
@@ -113,14 +118,14 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
this._futureFixes = new CancellationTokenSource();
const { token } = this._futureFixes;
this._model = value;
this._state = newState;
if (!this._model || !this._model.actions) {
if (this._state.type === CodeActionsState.Empty.type) {
return;
}
const selection = this._model.rangeOrSelection;
this._model.actions.then(fixes => {
const selection = this._state.rangeOrSelection;
this._state.actions.then(fixes => {
if (!token.isCancellationRequested && fixes && fixes.length > 0 && selection) {
this._show();
} else {
@@ -131,10 +136,6 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
});
}
get model(): CodeActionsComputeEvent | null {
return this._model;
}
set title(value: string) {
this._domNode.title = value;
}
@@ -148,10 +149,10 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
if (!config.contribInfo.lightbulbEnabled) {
return;
}
if (!this._model || !this._model.position) {
if (this._state.type !== CodeActionsState.Type.Triggered) {
return;
}
const { lineNumber, column } = this._model.position;
const { lineNumber, column } = this._state.position;
const model = this._editor.getModel();
if (!model) {
return;
@@ -188,7 +189,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
hide(): void {
this._position = null;
this._model = null;
this._state = CodeActionsState.Empty;
this._futureFixes.cancel();
this._editor.layoutContentWidget(this);
}

View File

@@ -11,6 +11,8 @@ import { CodeAction, CodeActionContext, CodeActionProvider, CodeActionProviderRe
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';
import { ITextModel } from 'vs/editor/common/model';
suite('CodeAction', () => {
@@ -115,7 +117,7 @@ suite('CodeAction', () => {
testData.tsLint.abc
];
const actions = await getCodeActions(model, new Range(1, 1, 2, 1));
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'manual' }, CancellationToken.None);
assert.equal(actions.length, 6);
assert.deepEqual(actions, expected);
});
@@ -134,20 +136,20 @@ suite('CodeAction', () => {
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
{
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } });
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } }, CancellationToken.None);
assert.equal(actions.length, 2);
assert.strictEqual(actions[0].title, 'a');
assert.strictEqual(actions[1].title, 'a.b');
}
{
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b') } });
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b') } }, CancellationToken.None);
assert.equal(actions.length, 1);
assert.strictEqual(actions[0].title, 'a.b');
}
{
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b.c') } });
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b.c') } }, CancellationToken.None);
assert.equal(actions.length, 0);
}
});
@@ -156,14 +158,14 @@ suite('CodeAction', () => {
const provider = new class implements CodeActionProvider {
provideCodeActions(_model: any, _range: Range, context: CodeActionContext, _token: any): CodeAction[] {
return [
{ title: context.only, kind: context.only }
{ title: context.only || '', kind: context.only }
];
}
};
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } });
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);
assert.strictEqual(actions[0].title, 'a');
});
@@ -181,13 +183,13 @@ suite('CodeAction', () => {
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
{
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto' });
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto' }, CancellationToken.None);
assert.equal(actions.length, 1);
assert.strictEqual(actions[0].title, 'b');
}
{
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: CodeActionKind.Source, includeSourceActions: true } });
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: CodeActionKind.Source, includeSourceActions: true } }, CancellationToken.None);
assert.equal(actions.length, 1);
assert.strictEqual(actions[0].title, 'a');
}
@@ -211,8 +213,53 @@ suite('CodeAction', () => {
filter: {
kind: CodeActionKind.QuickFix
}
});
}, CancellationToken.None);
assert.strictEqual(actions.length, 0);
assert.strictEqual(wasInvoked, false);
});
test('getCodeActions requests for source actions should expand source actions range to entire document #53525', async function () {
const provider = new class implements CodeActionProvider {
provideCodeActions(model: ITextModel, range: Range): CodeAction[] {
return [{
title: rangeToString(range),
kind: CodeActionKind.Source.value,
}];
}
};
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
{
const actions = await getCodeActions(model, new Range(1, 1, 1, 1), {
type: 'manual',
filter: {
kind: CodeActionKind.Source,
includeSourceActions: true,
}
}, CancellationToken.None);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, rangeToString(model.getFullModelRange()));
}
{
const range = new Range(1, 1, 1, 2);
// But we should not expand for non-empty selections
const actions = await getCodeActions(model, range, {
type: 'manual',
filter: {
kind: CodeActionKind.Source,
includeSourceActions: true,
}
}, CancellationToken.None);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, rangeToString(range));
}
});
});
function rangeToString(range: Range): string {
return `${range.startLineNumber},${range.startColumn} ${range.endLineNumber},${range.endColumn} `;
}

View File

@@ -10,7 +10,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Selection } from 'vs/editor/common/core/selection';
import { TextModel } from 'vs/editor/common/model/textModel';
import { CodeActionProviderRegistry, LanguageIdentifier } from 'vs/editor/common/modes';
import { CodeActionOracle } from 'vs/editor/contrib/codeAction/codeActionModel';
import { CodeActionOracle, CodeActionsState } from 'vs/editor/contrib/codeAction/codeActionModel';
import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { MarkerService } from 'vs/platform/markers/common/markerService';
@@ -47,11 +47,11 @@ suite('CodeAction', () => {
const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.push(reg);
const oracle = new CodeActionOracle(editor, markerService, e => {
const oracle = new CodeActionOracle(editor, markerService, (e: CodeActionsState.Triggered) => {
assert.equal(e.trigger.type, 'auto');
assert.ok(e.actions);
e.actions.then(fixes => {
e.actions!.then(fixes => {
oracle.dispose();
assert.equal(fixes.length, 1);
done();
@@ -85,10 +85,10 @@ suite('CodeAction', () => {
return new Promise((resolve, reject) => {
const oracle = new CodeActionOracle(editor, markerService, e => {
const oracle = new CodeActionOracle(editor, markerService, (e: CodeActionsState.Triggered) => {
assert.equal(e.trigger.type, 'auto');
assert.ok(e.actions);
e.actions.then(fixes => {
e.actions!.then(fixes => {
oracle.dispose();
assert.equal(fixes.length, 1);
resolve(undefined);
@@ -107,7 +107,7 @@ suite('CodeAction', () => {
});
disposables.push(reg);
editor.getModel().setValue('// @ts-check\n2\ncon\n');
editor.getModel()!.setValue('// @ts-check\n2\ncon\n');
markerService.changeOne('fake', uri, [{
startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4,
@@ -120,7 +120,7 @@ suite('CodeAction', () => {
// case 1 - drag selection over multiple lines -> range of enclosed marker, position or marker
await new Promise(resolve => {
let oracle = new CodeActionOracle(editor, markerService, e => {
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);
@@ -130,7 +130,7 @@ suite('CodeAction', () => {
assert.deepEqual(e.position, { lineNumber: 3, column: 1 });
oracle.dispose();
resolve(null);
resolve(undefined);
}, 5);
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
@@ -142,7 +142,7 @@ suite('CodeAction', () => {
disposables.push(reg);
let triggerCount = 0;
const oracle = new CodeActionOracle(editor, markerService, e => {
const oracle = new CodeActionOracle(editor, markerService, (e: CodeActionsState.Triggered) => {
assert.equal(e.trigger.type, 'auto');
++triggerCount;