mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-21 04:20:11 -04:00
Merge VS Code 1.23.1 (#1520)
This commit is contained in:
84
src/vs/editor/contrib/codeAction/codeAction.ts
Normal file
84
src/vs/editor/contrib/codeAction/codeAction.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isFalsyOrEmpty, mergeSort, flatten } from 'vs/base/common/arrays';
|
||||
import { asWinJsPromise } from 'vs/base/common/async';
|
||||
import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { CodeActionFilter, CodeActionKind } from './codeActionTrigger';
|
||||
|
||||
export function getCodeActions(model: ITextModel, range: Range, filter?: CodeActionFilter): TPromise<CodeAction[]> {
|
||||
const codeActionContext = { only: filter && filter.kind ? filter.kind.value : undefined };
|
||||
|
||||
const promises = CodeActionProviderRegistry.all(model).map(support => {
|
||||
return asWinJsPromise(token => support.provideCodeActions(model, range, codeActionContext, token)).then(providedCodeActions => {
|
||||
if (!Array.isArray(providedCodeActions)) {
|
||||
return [];
|
||||
}
|
||||
return providedCodeActions.filter(action => isValidAction(filter, action));
|
||||
}, (err): CodeAction[] => {
|
||||
onUnexpectedExternalError(err);
|
||||
return [];
|
||||
});
|
||||
});
|
||||
|
||||
return TPromise.join(promises)
|
||||
.then(flatten)
|
||||
.then(allCodeActions => mergeSort(allCodeActions, codeActionsComparator));
|
||||
}
|
||||
|
||||
function isValidAction(filter: CodeActionFilter | undefined, action: CodeAction): boolean {
|
||||
if (!action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out actions by kind
|
||||
if (filter && filter.kind && (!action.kind || !filter.kind.contains(action.kind))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't return source actions unless they are explicitly requested
|
||||
if (action.kind && CodeActionKind.Source.contains(action.kind) && (!filter || !filter.includeSourceActions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function codeActionsComparator(a: CodeAction, b: CodeAction): number {
|
||||
const aHasDiags = !isFalsyOrEmpty(a.diagnostics);
|
||||
const bHasDiags = !isFalsyOrEmpty(b.diagnostics);
|
||||
if (aHasDiags) {
|
||||
if (bHasDiags) {
|
||||
return a.diagnostics[0].message.localeCompare(b.diagnostics[0].message);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (bHasDiags) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0; // both have no diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
registerLanguageCommand('_executeCodeActionProvider', function (accessor, args) {
|
||||
const { resource, range } = args;
|
||||
if (!(resource instanceof URI) || !Range.isIRange(range)) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
return getCodeActions(model, model.validateRange(range));
|
||||
});
|
||||
338
src/vs/editor/contrib/codeAction/codeActionCommands.ts
Normal file
338
src/vs/editor/contrib/codeAction/codeActionCommands.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { BulkEdit } from 'vs/editor/browser/services/bulkEdit';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { CodeAction } from 'vs/editor/common/modes';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { MessageController } from 'vs/editor/contrib/message/messageController';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { optional } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IMarkerService } from 'vs/platform/markers/common/markers';
|
||||
import { CodeActionModel, CodeActionsComputeEvent, SUPPORTED_CODE_ACTIONS } from './codeActionModel';
|
||||
import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeActionTrigger';
|
||||
import { CodeActionContextMenu } from './codeActionWidget';
|
||||
import { LightBulbWidget } from './lightBulbWidget';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
|
||||
function contextKeyForSupportedActions(kind: CodeActionKind) {
|
||||
return ContextKeyExpr.regex(
|
||||
SUPPORTED_CODE_ACTIONS.keys()[0],
|
||||
new RegExp('(\\s|^)' + escapeRegExpCharacters(kind.value) + '\\b'));
|
||||
}
|
||||
|
||||
export class QuickFixController implements IEditorContribution {
|
||||
|
||||
private static readonly ID = 'editor.contrib.quickFixController';
|
||||
|
||||
public static get(editor: ICodeEditor): QuickFixController {
|
||||
return editor.getContribution<QuickFixController>(QuickFixController.ID);
|
||||
}
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _model: CodeActionModel;
|
||||
private _codeActionContextMenu: CodeActionContextMenu;
|
||||
private _lightBulbWidget: LightBulbWidget;
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(editor: ICodeEditor,
|
||||
@IMarkerService markerService: IMarkerService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@optional(IFileService) private _fileService: IFileService
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._model = new CodeActionModel(this._editor, markerService, contextKeyService);
|
||||
this._codeActionContextMenu = new CodeActionContextMenu(editor, contextMenuService, action => this._onApplyCodeAction(action));
|
||||
this._lightBulbWidget = new LightBulbWidget(editor);
|
||||
|
||||
this._updateLightBulbTitle();
|
||||
|
||||
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._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitle, this)
|
||||
);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._model.dispose();
|
||||
dispose(this._disposables);
|
||||
}
|
||||
|
||||
private _onCodeActionsEvent(e: CodeActionsComputeEvent): void {
|
||||
if (e && 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 (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);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
this._lightBulbWidget.model = e;
|
||||
}
|
||||
} else {
|
||||
this._lightBulbWidget.hide();
|
||||
}
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return QuickFixController.ID;
|
||||
}
|
||||
|
||||
private _handleLightBulbSelect(coords: { x: number, y: number }): void {
|
||||
this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords);
|
||||
}
|
||||
|
||||
public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): TPromise<CodeAction[] | undefined> {
|
||||
return this._model.trigger({ type: 'manual', filter, autoApply });
|
||||
}
|
||||
|
||||
private _updateLightBulbTitle(): void {
|
||||
const kb = this._keybindingService.lookupKeybinding(QuickFixAction.Id);
|
||||
let title: string;
|
||||
if (kb) {
|
||||
title = nls.localize('quickFixWithKb', "Show Fixes ({0})", kb.getLabel());
|
||||
} else {
|
||||
title = nls.localize('quickFix', "Show Fixes");
|
||||
}
|
||||
this._lightBulbWidget.title = title;
|
||||
}
|
||||
|
||||
private async _onApplyCodeAction(action: CodeAction): TPromise<void> {
|
||||
await applyCodeAction(action, this._textModelService, this._fileService, this._commandService, this._editor);
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyCodeAction(
|
||||
action: CodeAction,
|
||||
textModelService: ITextModelService,
|
||||
fileService: IFileService,
|
||||
commandService: ICommandService,
|
||||
editor: ICodeEditor,
|
||||
) {
|
||||
if (action.edit) {
|
||||
await BulkEdit.perform(action.edit.edits, textModelService, fileService, editor);
|
||||
}
|
||||
if (action.command) {
|
||||
await commandService.executeCommand(action.command.id, ...action.command.arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function showCodeActionsForEditorSelection(
|
||||
editor: ICodeEditor,
|
||||
notAvailableMessage: string,
|
||||
filter?: CodeActionFilter,
|
||||
autoApply?: CodeActionAutoApply
|
||||
) {
|
||||
const controller = QuickFixController.get(editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = editor.getPosition();
|
||||
controller.triggerFromEditorSelection(filter, autoApply).then(codeActions => {
|
||||
if (!codeActions || !codeActions.length) {
|
||||
MessageController.get(editor).showMessage(notAvailableMessage, pos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class QuickFixAction extends EditorAction {
|
||||
|
||||
static readonly Id = 'editor.action.quickFix';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: QuickFixAction.Id,
|
||||
label: nls.localize('quickfix.trigger.label', "Quick Fix..."),
|
||||
alias: 'Quick Fix',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.US_DOT
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CodeActionCommandArgs {
|
||||
public static fromUser(arg: any): CodeActionCommandArgs {
|
||||
if (!arg || typeof arg !== 'object') {
|
||||
return new CodeActionCommandArgs(CodeActionKind.Empty, CodeActionAutoApply.IfSingle);
|
||||
}
|
||||
return new CodeActionCommandArgs(
|
||||
CodeActionCommandArgs.getKindFromUser(arg),
|
||||
CodeActionCommandArgs.getApplyFromUser(arg));
|
||||
}
|
||||
|
||||
private static getApplyFromUser(arg: any) {
|
||||
switch (typeof arg.apply === 'string' ? arg.apply.toLowerCase() : '') {
|
||||
case 'first':
|
||||
return CodeActionAutoApply.First;
|
||||
|
||||
case 'never':
|
||||
return CodeActionAutoApply.Never;
|
||||
|
||||
case 'ifsingle':
|
||||
default:
|
||||
return CodeActionAutoApply.IfSingle;
|
||||
}
|
||||
}
|
||||
|
||||
private static getKindFromUser(arg: any) {
|
||||
return typeof arg.kind === 'string'
|
||||
? new CodeActionKind(arg.kind)
|
||||
: CodeActionKind.Empty;
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly kind: CodeActionKind,
|
||||
public readonly apply: CodeActionAutoApply
|
||||
) { }
|
||||
}
|
||||
|
||||
export class CodeActionCommand extends EditorCommand {
|
||||
|
||||
static readonly Id = 'editor.action.codeAction';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: CodeActionCommand.Id,
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider)
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class RefactorAction extends EditorAction {
|
||||
|
||||
static readonly Id = 'editor.action.refactor';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: RefactorAction.Id,
|
||||
label: nls.localize('refactor.label', "Refactor..."),
|
||||
alias: 'Refactor',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R,
|
||||
mac: {
|
||||
primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R
|
||||
}
|
||||
},
|
||||
menuOpts: {
|
||||
group: '1_modification',
|
||||
order: 2,
|
||||
when: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.Refactor)),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return showCodeActionsForEditorSelection(editor,
|
||||
nls.localize('editor.action.refactor.noneMessage', "No refactorings available"),
|
||||
{ kind: CodeActionKind.Refactor },
|
||||
CodeActionAutoApply.Never);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SourceAction extends EditorAction {
|
||||
|
||||
static readonly Id = 'editor.action.sourceAction';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: SourceAction.Id,
|
||||
label: nls.localize('source.label', "Source Action..."),
|
||||
alias: 'Source Action',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider),
|
||||
menuOpts: {
|
||||
group: '1_modification',
|
||||
order: 2.1,
|
||||
when: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.Source)),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return showCodeActionsForEditorSelection(editor,
|
||||
nls.localize('editor.action.source.noneMessage', "No source actions available"),
|
||||
{ kind: CodeActionKind.Source, includeSourceActions: true },
|
||||
CodeActionAutoApply.Never);
|
||||
}
|
||||
}
|
||||
|
||||
export class OrganizeImportsAction extends EditorAction {
|
||||
|
||||
static readonly Id = 'editor.action.organizeImports';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: OrganizeImportsAction.Id,
|
||||
label: nls.localize('organizeImports.label', "Organize Imports"),
|
||||
alias: 'Organize Imports',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.SourceOrganizeImports)),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_O
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return showCodeActionsForEditorSelection(editor,
|
||||
nls.localize('editor.action.organize.noneMessage', "No organize imports action available"),
|
||||
{ kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true },
|
||||
CodeActionAutoApply.IfSingle);
|
||||
}
|
||||
}
|
||||
15
src/vs/editor/contrib/codeAction/codeActionContributions.ts
Normal file
15
src/vs/editor/contrib/codeAction/codeActionContributions.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { SourceAction, QuickFixController, QuickFixAction, CodeActionCommand, RefactorAction, OrganizeImportsAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
|
||||
|
||||
|
||||
registerEditorContribution(QuickFixController);
|
||||
registerEditorAction(QuickFixAction);
|
||||
registerEditorAction(RefactorAction);
|
||||
registerEditorAction(SourceAction);
|
||||
registerEditorAction(OrganizeImportsAction);
|
||||
registerEditorCommand(new CodeActionCommand());
|
||||
204
src/vs/editor/contrib/codeAction/codeActionModel.ts
Normal file
204
src/vs/editor/contrib/codeAction/codeActionModel.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event, debounceEvent } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { CodeAction, 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 { getCodeActions } from './codeAction';
|
||||
import { CodeActionTrigger } from './codeActionTrigger';
|
||||
|
||||
export const SUPPORTED_CODE_ACTIONS = new RawContextKey<string>('supportedCodeAction', '');
|
||||
|
||||
export class CodeActionOracle {
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private _editor: ICodeEditor,
|
||||
private _markerService: IMarkerService,
|
||||
private _signalChange: (e: CodeActionsComputeEvent) => any,
|
||||
delay: number = 250
|
||||
) {
|
||||
this._disposables.push(
|
||||
debounceEvent(this._markerService.onMarkerChanged, (last, cur) => last ? last.concat(cur) : cur, delay / 2)(e => this._onMarkerChanges(e)),
|
||||
debounceEvent(this._editor.onDidChangeCursorPosition, (last, cur) => cur, delay)(e => this._onCursorChange())
|
||||
);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
|
||||
trigger(trigger: CodeActionTrigger) {
|
||||
let rangeOrSelection = this._getRangeOfMarker() || this._getRangeOfSelectionUnlessWhitespaceEnclosed();
|
||||
if (!rangeOrSelection && trigger.type === 'manual') {
|
||||
rangeOrSelection = this._editor.getSelection();
|
||||
}
|
||||
return this._createEventAndSignalChange(trigger, rangeOrSelection);
|
||||
}
|
||||
|
||||
private _onMarkerChanges(resources: URI[]): void {
|
||||
const { uri } = this._editor.getModel();
|
||||
for (const resource of resources) {
|
||||
if (resource.toString() === uri.toString()) {
|
||||
this.trigger({ type: 'auto' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onCursorChange(): void {
|
||||
this.trigger({ type: 'auto' });
|
||||
}
|
||||
|
||||
private _getRangeOfMarker(): Range {
|
||||
const selection = this._editor.getSelection();
|
||||
const model = this._editor.getModel();
|
||||
for (const marker of this._markerService.read({ resource: model.uri })) {
|
||||
if (Range.intersectRanges(marker, selection)) {
|
||||
return Range.lift(marker);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getRangeOfSelectionUnlessWhitespaceEnclosed(): Selection {
|
||||
const model = this._editor.getModel();
|
||||
const selection = this._editor.getSelection();
|
||||
if (selection.isEmpty()) {
|
||||
const { lineNumber, column } = selection.getPosition();
|
||||
const line = model.getLineContent(lineNumber);
|
||||
if (line.length === 0) {
|
||||
// empty line
|
||||
return undefined;
|
||||
} else if (column === 1) {
|
||||
// look only right
|
||||
if (/\s/.test(line[0])) {
|
||||
return undefined;
|
||||
}
|
||||
} else if (column === model.getLineMaxColumn(lineNumber)) {
|
||||
// look only left
|
||||
if (/\s/.test(line[line.length - 1])) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
// look left and right
|
||||
if (/\s/.test(line[column - 2]) && /\s/.test(line[column - 1])) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
private _createEventAndSignalChange(trigger: CodeActionTrigger, rangeOrSelection: Range | Selection): TPromise<CodeAction[] | undefined> {
|
||||
if (!rangeOrSelection) {
|
||||
// cancel
|
||||
this._signalChange({
|
||||
trigger,
|
||||
range: undefined,
|
||||
position: undefined,
|
||||
actions: undefined,
|
||||
});
|
||||
return TPromise.as(undefined);
|
||||
} else {
|
||||
// actual
|
||||
const model = this._editor.getModel();
|
||||
const range = model.validateRange(rangeOrSelection);
|
||||
const position = rangeOrSelection instanceof Selection ? rangeOrSelection.getPosition() : rangeOrSelection.getStartPosition();
|
||||
const actions = getCodeActions(model, range, trigger && trigger.filter);
|
||||
|
||||
this._signalChange({
|
||||
trigger,
|
||||
range,
|
||||
position,
|
||||
actions
|
||||
});
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodeActionsComputeEvent {
|
||||
trigger: CodeActionTrigger;
|
||||
range: Range;
|
||||
position: Position;
|
||||
actions: TPromise<CodeAction[]>;
|
||||
}
|
||||
|
||||
export class CodeActionModel {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _markerService: IMarkerService;
|
||||
private _codeActionOracle: CodeActionOracle;
|
||||
private _onDidChangeFixes = new Emitter<CodeActionsComputeEvent>();
|
||||
private _disposables: IDisposable[] = [];
|
||||
private readonly _supportedCodeActions: IContextKey<string>;
|
||||
|
||||
constructor(editor: ICodeEditor, markerService: IMarkerService, contextKeyService: IContextKeyService) {
|
||||
this._editor = editor;
|
||||
this._markerService = markerService;
|
||||
|
||||
this._supportedCodeActions = SUPPORTED_CODE_ACTIONS.bindTo(contextKeyService);
|
||||
|
||||
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._update();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables = dispose(this._disposables);
|
||||
dispose(this._codeActionOracle);
|
||||
}
|
||||
|
||||
get onDidChangeFixes(): Event<CodeActionsComputeEvent> {
|
||||
return this._onDidChangeFixes.event;
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
|
||||
if (this._codeActionOracle) {
|
||||
this._codeActionOracle.dispose();
|
||||
this._codeActionOracle = undefined;
|
||||
this._onDidChangeFixes.fire(undefined);
|
||||
}
|
||||
|
||||
if (this._editor.getModel()
|
||||
&& CodeActionProviderRegistry.has(this._editor.getModel())
|
||||
&& !this._editor.getConfiguration().readOnly) {
|
||||
|
||||
const supportedActions: string[] = [];
|
||||
for (const provider of CodeActionProviderRegistry.all(this._editor.getModel())) {
|
||||
if (Array.isArray(provider.providedCodeActionKinds)) {
|
||||
supportedActions.push(...provider.providedCodeActionKinds);
|
||||
}
|
||||
}
|
||||
|
||||
this._supportedCodeActions.set(supportedActions.join(' '));
|
||||
|
||||
this._codeActionOracle = new CodeActionOracle(this._editor, this._markerService, p => this._onDidChangeFixes.fire(p));
|
||||
this._codeActionOracle.trigger({ type: 'auto' });
|
||||
} else {
|
||||
this._supportedCodeActions.reset();
|
||||
}
|
||||
}
|
||||
|
||||
trigger(trigger: CodeActionTrigger): TPromise<CodeAction[] | undefined> {
|
||||
if (this._codeActionOracle) {
|
||||
return this._codeActionOracle.trigger(trigger);
|
||||
}
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
}
|
||||
40
src/vs/editor/contrib/codeAction/codeActionTrigger.ts
Normal file
40
src/vs/editor/contrib/codeAction/codeActionTrigger.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
|
||||
export class CodeActionKind {
|
||||
private static readonly sep = '.';
|
||||
|
||||
public static readonly Empty = new CodeActionKind('');
|
||||
public static readonly Refactor = new CodeActionKind('refactor');
|
||||
public static readonly Source = new CodeActionKind('source');
|
||||
public static readonly SourceOrganizeImports = new CodeActionKind('source.organizeImports');
|
||||
|
||||
constructor(
|
||||
public readonly value: string
|
||||
) { }
|
||||
|
||||
public contains(other: string): boolean {
|
||||
return this.value === other || startsWith(other, this.value + CodeActionKind.sep);
|
||||
}
|
||||
}
|
||||
|
||||
export enum CodeActionAutoApply {
|
||||
IfSingle = 1,
|
||||
First = 2,
|
||||
Never = 3
|
||||
}
|
||||
|
||||
export interface CodeActionFilter {
|
||||
readonly kind?: CodeActionKind;
|
||||
readonly includeSourceActions?: boolean;
|
||||
}
|
||||
|
||||
export interface CodeActionTrigger {
|
||||
readonly type: 'auto' | 'manual';
|
||||
readonly filter?: CodeActionFilter;
|
||||
readonly autoApply?: CodeActionAutoApply;
|
||||
}
|
||||
79
src/vs/editor/contrib/codeAction/codeActionWidget.ts
Normal file
79
src/vs/editor/contrib/codeAction/codeActionWidget.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { CodeAction } from 'vs/editor/common/modes';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
|
||||
export class CodeActionContextMenu {
|
||||
|
||||
private _visible: boolean;
|
||||
private _onDidExecuteCodeAction = new Emitter<void>();
|
||||
|
||||
readonly onDidExecuteCodeAction: Event<void> = this._onDidExecuteCodeAction.event;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
private readonly _contextMenuService: IContextMenuService,
|
||||
private readonly _onApplyCodeAction: (action: CodeAction) => TPromise<any>
|
||||
) { }
|
||||
|
||||
show(fixes: TPromise<CodeAction[]>, at: { x: number; y: number } | Position) {
|
||||
|
||||
const actions = 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 TPromise.wrapError<any>(canceled());
|
||||
}
|
||||
return actions;
|
||||
});
|
||||
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => {
|
||||
if (Position.isIPosition(at)) {
|
||||
at = this._toCoords(at);
|
||||
}
|
||||
return at;
|
||||
},
|
||||
getActions: () => actions,
|
||||
onHide: () => { this._visible = false; },
|
||||
autoSelectFirstItem: true
|
||||
});
|
||||
}
|
||||
|
||||
get isVisible(): boolean {
|
||||
return this._visible;
|
||||
}
|
||||
|
||||
private _toCoords(position: Position): { x: number, y: number } {
|
||||
|
||||
this._editor.revealPosition(position, ScrollType.Immediate);
|
||||
this._editor.render();
|
||||
|
||||
// Translate to absolute editor position
|
||||
const cursorCoords = this._editor.getScrolledVisiblePosition(this._editor.getPosition());
|
||||
const editorCoords = getDomNodePagePosition(this._editor.getDomNode());
|
||||
const x = editorCoords.left + cursorCoords.left;
|
||||
const y = editorCoords.top + cursorCoords.top + cursorCoords.height;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
27
src/vs/editor/contrib/codeAction/lightBulbWidget.css
Normal file
27
src/vs/editor/contrib/codeAction/lightBulbWidget.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .lightbulb-glyph {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 16px;
|
||||
width: 20px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .lightbulb-glyph:hover {
|
||||
cursor: pointer;
|
||||
/* transform: scale(1.3, 1.3); */
|
||||
}
|
||||
|
||||
.monaco-editor.vs .lightbulb-glyph {
|
||||
background: url('lightbulb.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .lightbulb-glyph,
|
||||
.monaco-editor.hc-black .lightbulb-glyph {
|
||||
background: url('lightbulb-dark.svg') center center no-repeat;
|
||||
}
|
||||
174
src/vs/editor/contrib/codeAction/lightBulbWidget.ts
Normal file
174
src/vs/editor/contrib/codeAction/lightBulbWidget.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { IDisposable, dispose } 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';
|
||||
|
||||
export class LightBulbWidget implements IDisposable, 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 _position: IContentWidgetPosition;
|
||||
private _model: CodeActionsComputeEvent;
|
||||
private _futureFixes = new CancellationTokenSource();
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
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(_ => {
|
||||
// cancel when the line in question has been removed
|
||||
if (this._model && this.model.position.lineNumber >= this._editor.getModel().getLineCount()) {
|
||||
this._futureFixes.cancel();
|
||||
}
|
||||
}));
|
||||
this._disposables.push(dom.addStandardDisposableListener(this._domNode, 'click', e => {
|
||||
// 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
|
||||
// doesn't cover the line-text
|
||||
const { top, height } = dom.getDomNodePagePosition(this._domNode);
|
||||
const { lineHeight } = this._editor.getConfiguration();
|
||||
|
||||
let pad = Math.floor(lineHeight / 3);
|
||||
if (this._position.position.lineNumber < this._model.position.lineNumber) {
|
||||
pad += lineHeight;
|
||||
}
|
||||
|
||||
this._onClick.fire({
|
||||
x: e.posx,
|
||||
y: top + height + pad
|
||||
});
|
||||
}));
|
||||
this._disposables.push(dom.addDisposableListener(this._domNode, 'mouseenter', (e: MouseEvent) => {
|
||||
if ((e.buttons & 1) !== 1) {
|
||||
return;
|
||||
}
|
||||
// mouse enters lightbulb while the primary/left button
|
||||
// is being pressed -> hide the lightbulb and block future
|
||||
// showings until mouse is released
|
||||
this.hide();
|
||||
const monitor = new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>();
|
||||
monitor.startMonitoring(standardMouseMoveMerger, () => { }, () => {
|
||||
monitor.dispose();
|
||||
});
|
||||
}));
|
||||
this._disposables.push(this._editor.onDidChangeConfiguration(e => {
|
||||
// hide when told to do so
|
||||
if (e.contribInfo && !this._editor.getConfiguration().contribInfo.lightbulbEnabled) {
|
||||
this.hide();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._disposables);
|
||||
this._editor.removeContentWidget(this);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'LightBulbWidget';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
set model(value: CodeActionsComputeEvent) {
|
||||
|
||||
if (this._position && (!value.position || this._position.position.lineNumber !== value.position.lineNumber)) {
|
||||
// hide when getting a 'hide'-request or when currently
|
||||
// showing on another line
|
||||
this.hide();
|
||||
} else if (this._futureFixes) {
|
||||
// cancel pending show request in any case
|
||||
this._futureFixes.cancel();
|
||||
}
|
||||
|
||||
this._futureFixes = new CancellationTokenSource();
|
||||
const { token } = this._futureFixes;
|
||||
this._model = value;
|
||||
|
||||
this._model.actions.done(fixes => {
|
||||
if (!token.isCancellationRequested && fixes && fixes.length > 0) {
|
||||
this._show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}, err => {
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
get model(): CodeActionsComputeEvent {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
set title(value: string) {
|
||||
this._domNode.title = value;
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this._domNode.title;
|
||||
}
|
||||
|
||||
private _show(): void {
|
||||
const config = this._editor.getConfiguration();
|
||||
if (!config.contribInfo.lightbulbEnabled) {
|
||||
return;
|
||||
}
|
||||
const { lineNumber } = this._model.position;
|
||||
const model = this._editor.getModel();
|
||||
const tabSize = model.getOptions().tabSize;
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
const indent = TextModel.computeIndentLevel(lineContent, tabSize);
|
||||
const lineHasSpace = config.fontInfo.spaceWidth * indent > 22;
|
||||
|
||||
let effectiveLineNumber = lineNumber;
|
||||
if (!lineHasSpace) {
|
||||
if (lineNumber > 1) {
|
||||
effectiveLineNumber -= 1;
|
||||
} else {
|
||||
effectiveLineNumber += 1;
|
||||
}
|
||||
}
|
||||
|
||||
this._position = {
|
||||
position: { lineNumber: effectiveLineNumber, column: 1 },
|
||||
preference: LightBulbWidget._posPref
|
||||
};
|
||||
this._editor.layoutContentWidget(this);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this._position = null;
|
||||
this._model = null;
|
||||
this._futureFixes.cancel();
|
||||
this._editor.layoutContentWidget(this);
|
||||
}
|
||||
}
|
||||
1
src/vs/editor/contrib/codeAction/lightbulb-dark.svg
Normal file
1
src/vs/editor/contrib/codeAction/lightbulb-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><path fill="#1E1E1E" d="M13.5 4.2C13.1 2.1 10.8 0 9.3 0H6.7c-.4 0-.6.2-.6.2C4 .8 2.5 2.7 2.5 4.9c0 .5-.1 2.3 1.7 3.8.5.5 1.2 2 1.3 2.4v3.3L7.1 16h2l1.5-1.6V11c.1-.4.8-1.9 1.3-2.3 1.1-.9 1.5-1.9 1.6-2.7V4.2z"/><g><g fill="#C5C5C5"><path d="M6.5 12h3v1h-3zM7.5 15h1.1l.9-1h-3z"/></g><path fill="#DDB204" d="M12.6 5c0-2.3-1.8-4.1-4.1-4.1-.1 0-1.4.1-1.4.1-2.1.3-3.7 2-3.7 4 0 .1-.2 1.6 1.4 3 .7.7 1.5 2.4 1.6 2.9l.1.1h3l.1-.2c.1-.5.9-2.2 1.6-2.9 1.6-1.3 1.4-2.8 1.4-2.9zm-3 1l-.5 3h-.6V6c1.1 0 .9-1 .9-1H6.5v.1c0 .2.1.9 1 .9v3H7l-.2-.7L6.5 6c-.7 0-.9-.4-1-.7v-.4c0-.8.9-.9.9-.9h3.1s1 .1 1 1c0 0 .1 1-.9 1z"/></g><path fill="#252526" d="M10.5 5c0-.9-1-1-1-1H6.4s-.9.1-.9.9v.4c0 .3.3.7.9.7l.4 2.3.2.7h.5V6c-1 0-1-.7-1-.9V5h3s.1 1-.9 1v3h.6l.5-3c.9 0 .8-1 .8-1z"/></svg>
|
||||
|
After Width: | Height: | Size: 880 B |
1
src/vs/editor/contrib/codeAction/lightbulb.svg
Normal file
1
src/vs/editor/contrib/codeAction/lightbulb.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><path fill="#F6F6F6" d="M13.5 4.2C13.1 2.1 10.8 0 9.3 0H6.7c-.4 0-.6.2-.6.2C4 .8 2.5 2.7 2.5 4.9c0 .5-.1 2.3 1.7 3.8.5.5 1.2 2 1.3 2.4v3.3L7.1 16h2l1.5-1.6V11c.1-.4.8-1.9 1.3-2.3 1.1-.9 1.5-1.9 1.6-2.7V4.2z"/><g><g fill="#848484"><path d="M6.5 12h3v1h-3zM7.5 15h1.1l.9-1h-3z"/></g><path fill="#fc0" d="M12.6 5c0-2.3-1.8-4.1-4.1-4.1-.1 0-1.4.1-1.4.1-2.1.3-3.7 2-3.7 4 0 .1-.2 1.6 1.4 3 .7.7 1.5 2.4 1.6 2.9l.1.1h3l.1-.2c.1-.5.9-2.2 1.6-2.9 1.6-1.3 1.4-2.8 1.4-2.9zm-3 1l-.5 3h-.6V6c1.1 0 .9-1 .9-1H6.5v.1c0 .2.1.9 1 .9v3H7l-.2-.7L6.5 6c-.7 0-.9-.4-1-.7v-.4c0-.8.9-.9.9-.9h3.1s1 .1 1 1c0 0 .1 1-.9 1z"/></g><path fill="#F0EFF1" d="M10.5 5c0-.9-1-1-1-1H6.4s-.9.1-.9.9v.4c0 .3.3.7.9.7l.4 2.3.2.7h.5V6c-1 0-1-.7-1-.9V5h3s.1 1-.9 1v3h.6l.5-3c.9 0 .8-1 .8-1z"/></svg>
|
||||
|
After Width: | Height: | Size: 877 B |
197
src/vs/editor/contrib/codeAction/test/codeAction.test.ts
Normal file
197
src/vs/editor/contrib/codeAction/test/codeAction.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, ResourceTextEdit, CodeAction, CodeActionContext } from 'vs/editor/common/modes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
|
||||
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
|
||||
import { MarkerSeverity } from 'vs/platform/markers/common/markers';
|
||||
|
||||
suite('CodeAction', () => {
|
||||
|
||||
let langId = new LanguageIdentifier('fooLang', 17);
|
||||
let uri = URI.parse('untitled:path');
|
||||
let model: TextModel;
|
||||
let disposables: IDisposable[] = [];
|
||||
let testData = {
|
||||
diagnostics: {
|
||||
abc: {
|
||||
title: 'bTitle',
|
||||
diagnostics: [{
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 1,
|
||||
severity: MarkerSeverity.Error,
|
||||
message: 'abc'
|
||||
}]
|
||||
},
|
||||
bcd: {
|
||||
title: 'aTitle',
|
||||
diagnostics: [{
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 1,
|
||||
severity: MarkerSeverity.Error,
|
||||
message: 'bcd'
|
||||
}]
|
||||
}
|
||||
},
|
||||
command: {
|
||||
abc: {
|
||||
command: new class implements Command {
|
||||
id: '1';
|
||||
title: 'abc';
|
||||
},
|
||||
title: 'Extract to inner function in function "test"'
|
||||
}
|
||||
},
|
||||
spelling: {
|
||||
bcd: {
|
||||
diagnostics: [],
|
||||
edit: new class implements WorkspaceEdit {
|
||||
edits: ResourceTextEdit[];
|
||||
},
|
||||
title: 'abc'
|
||||
}
|
||||
},
|
||||
tsLint: {
|
||||
abc: {
|
||||
$ident: 57,
|
||||
arguments: [],
|
||||
id: '_internal_command_delegation',
|
||||
title: 'abc'
|
||||
},
|
||||
bcd: {
|
||||
$ident: 47,
|
||||
arguments: [],
|
||||
id: '_internal_command_delegation',
|
||||
title: 'bcd'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setup(function () {
|
||||
model = TextModel.createFromString('test1\ntest2\ntest3', undefined, langId, uri);
|
||||
disposables = [model];
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
dispose(disposables);
|
||||
});
|
||||
|
||||
test('CodeActions are sorted by type, #38623', async function () {
|
||||
|
||||
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.push(CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
const expected = [
|
||||
// CodeActions with a diagnostics array are shown first ordered by diagnostics.message
|
||||
testData.diagnostics.abc,
|
||||
testData.diagnostics.bcd,
|
||||
|
||||
// CodeActions without diagnostics are shown in the given order without any further sorting
|
||||
testData.command.abc,
|
||||
testData.spelling.bcd, // empty diagnostics array
|
||||
testData.tsLint.bcd,
|
||||
testData.tsLint.abc
|
||||
];
|
||||
|
||||
const actions = await getCodeActions(model, new Range(1, 1, 2, 1));
|
||||
assert.equal(actions.length, 6);
|
||||
assert.deepEqual(actions, expected);
|
||||
});
|
||||
|
||||
test('getCodeActions should filter by scope', async function () {
|
||||
const provider = new class implements CodeActionProvider {
|
||||
provideCodeActions(): CodeAction[] {
|
||||
return [
|
||||
{ title: 'a', kind: 'a' },
|
||||
{ title: 'b', kind: 'b' },
|
||||
{ title: 'a.b', kind: 'a.b' }
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
{
|
||||
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a') });
|
||||
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), { kind: new CodeActionKind('a.b') });
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].title, 'a.b');
|
||||
}
|
||||
|
||||
{
|
||||
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a.b.c') });
|
||||
assert.equal(actions.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('getCodeActions should forward requested scope to providers', async function () {
|
||||
const provider = new class implements CodeActionProvider {
|
||||
provideCodeActions(_model: any, _range: Range, context: CodeActionContext, _token: any): CodeAction[] {
|
||||
return [
|
||||
{ title: context.only, kind: context.only }
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a') });
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].title, 'a');
|
||||
});
|
||||
|
||||
test('getCodeActions should not return source code action by default', async function () {
|
||||
const provider = new class implements CodeActionProvider {
|
||||
provideCodeActions(): CodeAction[] {
|
||||
return [
|
||||
{ title: 'a', kind: CodeActionKind.Source.value },
|
||||
{ title: 'b', kind: 'b' }
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
{
|
||||
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), {});
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].title, 'b');
|
||||
}
|
||||
|
||||
{
|
||||
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: CodeActionKind.Source, includeSourceActions: true });
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].title, 'a');
|
||||
}
|
||||
});
|
||||
});
|
||||
194
src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts
Normal file
194
src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { MarkerService } from 'vs/platform/markers/common/markerService';
|
||||
import { CodeActionOracle } from 'vs/editor/contrib/codeAction/codeActionModel';
|
||||
import { CodeActionProviderRegistry, LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
|
||||
suite('CodeAction', () => {
|
||||
|
||||
const languageIdentifier = new LanguageIdentifier('foo-lang', 3);
|
||||
let uri = URI.parse('untitled:path');
|
||||
let model: TextModel;
|
||||
let markerService: MarkerService;
|
||||
let editor: ICodeEditor;
|
||||
let reg: IDisposable;
|
||||
|
||||
setup(() => {
|
||||
reg = CodeActionProviderRegistry.register(languageIdentifier.language, {
|
||||
provideCodeActions() {
|
||||
return [{ id: 'test-command', title: 'test', arguments: [] }];
|
||||
}
|
||||
});
|
||||
markerService = new MarkerService();
|
||||
model = TextModel.createFromString('foobar foo bar\nfarboo far boo', undefined, languageIdentifier, uri);
|
||||
editor = createTestCodeEditor(model);
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
reg.dispose();
|
||||
editor.dispose();
|
||||
model.dispose();
|
||||
markerService.dispose();
|
||||
});
|
||||
|
||||
test('Orcale -> marker added', done => {
|
||||
|
||||
const oracle = new CodeActionOracle(editor, markerService, e => {
|
||||
assert.equal(e.trigger.type, 'auto');
|
||||
assert.ok(e.actions);
|
||||
|
||||
e.actions.then(fixes => {
|
||||
oracle.dispose();
|
||||
assert.equal(fixes.length, 1);
|
||||
done();
|
||||
}, done);
|
||||
});
|
||||
|
||||
// start here
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
});
|
||||
|
||||
test('Orcale -> position changed', () => {
|
||||
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
editor.setPosition({ lineNumber: 2, column: 1 });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const oracle = new CodeActionOracle(editor, markerService, e => {
|
||||
assert.equal(e.trigger.type, 'auto');
|
||||
assert.ok(e.actions);
|
||||
e.actions.then(fixes => {
|
||||
oracle.dispose();
|
||||
assert.equal(fixes.length, 1);
|
||||
resolve(undefined);
|
||||
}, reject);
|
||||
});
|
||||
// start here
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
test('Oracle -> marker wins over selection', () => {
|
||||
|
||||
let range: Range;
|
||||
let reg = CodeActionProviderRegistry.register(languageIdentifier.language, {
|
||||
provideCodeActions(doc, _range) {
|
||||
range = _range;
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
let fixes: TPromise<any>[] = [];
|
||||
let oracle = new CodeActionOracle(editor, markerService, e => {
|
||||
fixes.push(e.actions);
|
||||
}, 10);
|
||||
|
||||
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 13 });
|
||||
|
||||
return TPromise.join<any>([TPromise.timeout(20)].concat(fixes)).then(_ => {
|
||||
|
||||
// -> marker wins
|
||||
assert.deepEqual(range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6 });
|
||||
|
||||
// 'auto' triggered, non-empty selection BUT within a marker
|
||||
editor.setSelection({ startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 4 });
|
||||
|
||||
return TPromise.join([TPromise.timeout(20)].concat(fixes)).then(_ => {
|
||||
reg.dispose();
|
||||
oracle.dispose();
|
||||
|
||||
// assert marker
|
||||
assert.deepEqual(range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Lightbulb is in the wrong place, #29933', async function () {
|
||||
let reg = CodeActionProviderRegistry.register(languageIdentifier.language, {
|
||||
provideCodeActions(doc, _range) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
editor.getModel().setValue('// @ts-check\n2\ncon\n');
|
||||
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
// 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 => {
|
||||
assert.equal(e.trigger.type, 'auto');
|
||||
assert.deepEqual(e.range, { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4 });
|
||||
assert.deepEqual(e.position, { lineNumber: 3, column: 1 });
|
||||
|
||||
oracle.dispose();
|
||||
resolve(null);
|
||||
}, 5);
|
||||
|
||||
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
|
||||
});
|
||||
|
||||
// // case 2 - selection over multiple lines & manual trigger -> lightbulb
|
||||
// await new TPromise(resolve => {
|
||||
|
||||
// editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
|
||||
|
||||
// let oracle = new QuickFixOracle(editor, markerService, e => {
|
||||
// assert.equal(e.type, 'manual');
|
||||
// assert.ok(e.range.equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 }));
|
||||
|
||||
// oracle.dispose();
|
||||
// resolve(null);
|
||||
// }, 5);
|
||||
|
||||
// oracle.trigger('manual');
|
||||
// });
|
||||
|
||||
|
||||
reg.dispose();
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user