Merge from master

This commit is contained in:
Raj Musuku
2019-02-21 17:56:04 -08:00
parent 5a146e34fa
commit 666ae11639
11482 changed files with 119352 additions and 255574 deletions

View File

@@ -3,11 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { flatten, isFalsyOrEmpty, mergeSort } from 'vs/base/common/arrays';
import { asWinJsPromise } from 'vs/base/common/async';
import { flatten, mergeSort, isNonEmptyArray } 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';
import { URI } from 'vs/base/common/uri';
import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
@@ -22,21 +21,43 @@ export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Sele
trigger: trigger && trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic
};
const promises = CodeActionProviderRegistry.all(model).map(support => {
return asWinJsPromise(token => 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;
const promises = CodeActionProviderRegistry.all(model)
.filter(provider => {
if (!provider.providedCodeActionKinds) {
return true;
}
onUnexpectedExternalError(err);
return [];
// 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 [];
});
});
});
return Promise.all(promises)
.then(flatten)
@@ -44,17 +65,17 @@ export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Sele
}
function isValidAction(filter: CodeActionFilter | undefined, action: CodeAction): boolean {
if (!action) {
return false;
}
return action && isValidActionKind(filter, action.kind);
}
function isValidActionKind(filter: CodeActionFilter | undefined, kind: string | undefined): boolean {
// Filter out actions by kind
if (filter && filter.kind && (!action.kind || !filter.kind.contains(action.kind))) {
if (filter && filter.kind && (!kind || !filter.kind.contains(kind))) {
return false;
}
// Don't return source actions unless they are explicitly requested
if (action.kind && CodeActionKind.Source.contains(action.kind) && (!filter || !filter.includeSourceActions)) {
if (kind && CodeActionKind.Source.contains(kind) && (!filter || !filter.includeSourceActions)) {
return false;
}
@@ -62,15 +83,13 @@ function isValidAction(filter: CodeActionFilter | undefined, action: CodeAction)
}
function codeActionsComparator(a: CodeAction, b: CodeAction): number {
const aHasDiags = !isFalsyOrEmpty(a.diagnostics);
const bHasDiags = !isFalsyOrEmpty(b.diagnostics);
if (aHasDiags) {
if (bHasDiags) {
if (isNonEmptyArray(a.diagnostics)) {
if (isNonEmptyArray(b.diagnostics)) {
return a.diagnostics[0].message.localeCompare(b.diagnostics[0].message);
} else {
return -1;
}
} else if (bHasDiags) {
} else if (isNonEmptyArray(b.diagnostics)) {
return 1;
} else {
return 0; // both have no diagnostics

View File

@@ -7,7 +7,6 @@ import { CancelablePromise } from 'vs/base/common/async';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
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 { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
@@ -94,7 +93,7 @@ export class QuickFixController implements IEditorContribution {
// 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)) {
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);
@@ -124,7 +123,7 @@ export class QuickFixController implements IEditorContribution {
}
private _handleLightBulbSelect(coords: { x: number, y: number }): void {
if (this._lightBulbWidget.model.actions) {
if (this._lightBulbWidget.model && this._lightBulbWidget.model.actions) {
this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords);
}
}
@@ -144,8 +143,8 @@ export class QuickFixController implements IEditorContribution {
this._lightBulbWidget.title = title;
}
private _onApplyCodeAction(action: CodeAction): TPromise<void> {
return TPromise.wrap(applyCodeAction(action, this._bulkEditService, this._commandService, this._editor));
private _onApplyCodeAction(action: CodeAction): Promise<void> {
return applyCodeAction(action, this._bulkEditService, this._commandService, this._editor);
}
}

View File

@@ -3,11 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { debounceEvent, Emitter, Event } from 'vs/base/common/event';
import { CancelablePromise, createCancelablePromise, TimeoutTimer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
@@ -24,22 +23,24 @@ export const SUPPORTED_CODE_ACTIONS = new RawContextKey<string>('supportedCodeAc
export class CodeActionOracle {
private _disposables: IDisposable[] = [];
private readonly _autoTriggerTimer = new TimeoutTimer();
constructor(
private _editor: ICodeEditor,
private readonly _markerService: IMarkerService,
private _signalChange: (e: CodeActionsComputeEvent) => any,
delay: number = 250,
private readonly _delay: number = 250,
private readonly _progressService?: IProgressService,
) {
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())
this._markerService.onMarkerChanged(e => this._onMarkerChanges(e)),
this._editor.onDidChangeCursorPosition(() => this._onCursorChange()),
);
}
dispose(): void {
this._disposables = dispose(this._disposables);
this._autoTriggerTimer.cancel();
}
trigger(trigger: CodeActionTrigger) {
@@ -48,21 +49,29 @@ export class CodeActionOracle {
}
private _onMarkerChanges(resources: URI[]): void {
const { uri } = this._editor.getModel();
for (const resource of resources) {
if (resource.toString() === uri.toString()) {
const model = this._editor.getModel();
if (!model) {
return;
}
if (resources.some(resource => resource.toString() === model.uri.toString())) {
this._autoTriggerTimer.cancelAndSet(() => {
this.trigger({ type: 'auto' });
return;
}
}, this._delay);
}
}
private _onCursorChange(): void {
this.trigger({ type: 'auto' });
this._autoTriggerTimer.cancelAndSet(() => {
this.trigger({ type: 'auto' });
}, this._delay);
}
private _getRangeOfMarker(selection: Selection): Range {
private _getRangeOfMarker(selection: Selection): Range | undefined {
const model = this._editor.getModel();
if (!model) {
return undefined;
}
for (const marker of this._markerService.read({ resource: model.uri })) {
if (Range.intersectRanges(marker, selection)) {
return Range.lift(marker);
@@ -72,9 +81,12 @@ export class CodeActionOracle {
}
private _getRangeOfSelectionUnlessWhitespaceEnclosed(trigger: CodeActionTrigger): Selection | undefined {
if (!this._editor.hasModel()) {
return undefined;
}
const model = this._editor.getModel();
const selection = this._editor.getSelection();
if (selection.isEmpty() && !(trigger.filter && trigger.filter.includeSourceActions)) {
if (selection.isEmpty() && trigger.type === 'auto') {
const { lineNumber, column } = selection.getPosition();
const line = model.getLineContent(lineNumber);
if (line.length === 0) {
@@ -97,7 +109,7 @@ export class CodeActionOracle {
}
}
}
return selection;
return selection ? selection : undefined;
}
private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): Thenable<CodeAction[] | undefined> {
@@ -109,15 +121,26 @@ export class CodeActionOracle {
position: undefined,
actions: undefined,
});
return TPromise.as(undefined);
return Promise.resolve(undefined);
} else {
const model = this._editor.getModel();
if (!model) {
// cancel
this._signalChange({
trigger,
rangeOrSelection: undefined,
position: undefined,
actions: undefined,
});
return Promise.resolve(undefined);
}
const markerRange = this._getRangeOfMarker(selection);
const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition();
const actions = createCancelablePromise(token => getCodeActions(model, selection, trigger, token));
if (this._progressService && trigger.type === 'manual') {
this._progressService.showWhile(TPromise.wrap(actions), 250);
this._progressService.showWhile(actions, 250);
}
this._signalChange({
@@ -133,16 +156,16 @@ export class CodeActionOracle {
export interface CodeActionsComputeEvent {
trigger: CodeActionTrigger;
rangeOrSelection: Range | Selection;
position: Position;
actions: CancelablePromise<CodeAction[]>;
rangeOrSelection: Range | Selection | undefined;
position: Position | undefined;
actions: CancelablePromise<CodeAction[]> | undefined;
}
export class CodeActionModel {
private _editor: ICodeEditor;
private _markerService: IMarkerService;
private _codeActionOracle: CodeActionOracle;
private _codeActionOracle?: CodeActionOracle;
private _onDidChangeFixes = new Emitter<CodeActionsComputeEvent>();
private _disposables: IDisposable[] = [];
private readonly _supportedCodeActions: IContextKey<string>;
@@ -177,12 +200,13 @@ export class CodeActionModel {
this._onDidChangeFixes.fire(undefined);
}
if (this._editor.getModel()
&& CodeActionProviderRegistry.has(this._editor.getModel())
const model = this._editor.getModel();
if (model
&& CodeActionProviderRegistry.has(model)
&& !this._editor.getConfiguration().readOnly) {
const supportedActions: string[] = [];
for (const provider of CodeActionProviderRegistry.all(this._editor.getModel())) {
for (const provider of CodeActionProviderRegistry.all(model)) {
if (Array.isArray(provider.providedCodeActionKinds)) {
supportedActions.push(...provider.providedCodeActionKinds);
}
@@ -201,6 +225,6 @@ export class CodeActionModel {
if (this._codeActionOracle) {
return this._codeActionOracle.trigger(trigger);
}
return TPromise.as(undefined);
return Promise.resolve(undefined);
}
}

View File

@@ -23,7 +23,7 @@ export class CodeActionKind {
}
}
export enum CodeActionAutoApply {
export const enum CodeActionAutoApply {
IfSingle = 1,
First = 2,
Never = 3

View File

@@ -8,7 +8,6 @@ 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';
@@ -25,12 +24,12 @@ export class CodeActionContextMenu {
constructor(
private readonly _editor: ICodeEditor,
private readonly _contextMenuService: IContextMenuService,
private readonly _onApplyCodeAction: (action: CodeAction) => TPromise<any>
private readonly _onApplyCodeAction: (action: CodeAction) => Promise<any>
) { }
show(fixes: Thenable<CodeAction[]>, at: { x: number; y: number } | Position) {
const actions = fixes.then(value => {
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(
@@ -41,24 +40,26 @@ export class CodeActionContextMenu {
}).then(actions => {
if (!this._editor.getDomNode()) {
// cancel when editor went off-dom
return TPromise.wrapError<any>(canceled());
return Promise.reject(canceled());
}
return actions;
});
}) : Promise.resolve([] as Action[]);
this._contextMenuService.showContextMenu({
getAnchor: () => {
if (Position.isIPosition(at)) {
at = this._toCoords(at);
}
return at;
},
getActions: () => TPromise.wrap(actions),
onHide: () => {
this._visible = false;
this._editor.focus();
},
autoSelectFirstItem: true
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
});
});
}

View File

@@ -11,7 +11,6 @@ import { dispose, IDisposable } 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 { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
import { CodeActionsComputeEvent } from './codeActionModel';
export class LightBulbWidget implements IDisposable, IContentWidget {
@@ -25,8 +24,8 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
readonly onClick: Event<{ x: number, y: number }> = this._onClick.event;
private _position: IContentWidgetPosition;
private _model: CodeActionsComputeEvent;
private _position: IContentWidgetPosition | null;
private _model: CodeActionsComputeEvent | null;
private _futureFixes = new CancellationTokenSource();
constructor(editor: ICodeEditor) {
@@ -40,7 +39,8 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
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()) {
const editorModel = this._editor.getModel();
if (!this.model || !this.model.position || !editorModel || this.model.position.lineNumber >= editorModel.getLineCount()) {
this._futureFixes.cancel();
}
}));
@@ -53,7 +53,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
const { lineHeight } = this._editor.getConfiguration();
let pad = Math.floor(lineHeight / 3);
if (this._position && this._position.position.lineNumber < this._model.position.lineNumber) {
if (this._position && this._model && this._model.position && this._position.position !== null && this._position.position.lineNumber < this._model.position.lineNumber) {
pad += lineHeight;
}
@@ -96,13 +96,13 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
return this._domNode;
}
getPosition(): IContentWidgetPosition {
getPosition(): IContentWidgetPosition | null {
return this._position;
}
set model(value: CodeActionsComputeEvent) {
set model(value: CodeActionsComputeEvent | null) {
if (this._position && (!value.position || this._position.position.lineNumber !== value.position.lineNumber)) {
if (!value || this._position && (!value.position || this._position.position && this._position.position.lineNumber !== value.position.lineNumber)) {
// hide when getting a 'hide'-request or when currently
// showing on another line
this.hide();
@@ -115,23 +115,23 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
const { token } = this._futureFixes;
this._model = value;
if (!this._model || !this._model.actions) {
return;
}
const selection = this._model.rangeOrSelection;
this._model.actions.then(fixes => {
if (!token.isCancellationRequested && fixes && fixes.length > 0) {
if (selection.isEmpty() && fixes.every(fix => fix.kind && CodeActionKind.Refactor.contains(fix.kind))) {
this.hide();
} else {
this._show();
}
if (!token.isCancellationRequested && fixes && fixes.length > 0 && selection) {
this._show();
} else {
this.hide();
}
}).catch(err => {
}).catch(() => {
this.hide();
});
}
get model(): CodeActionsComputeEvent {
get model(): CodeActionsComputeEvent | null {
return this._model;
}
@@ -148,7 +148,10 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
if (!config.contribInfo.lightbulbEnabled) {
return;
}
const { lineNumber } = this._model.position;
if (!this._model || !this._model.position) {
return;
}
const { lineNumber, column } = this._model.position;
const model = this._editor.getModel();
if (!model) {
return;
@@ -158,13 +161,21 @@ export class LightBulbWidget implements IDisposable, IContentWidget {
const lineContent = model.getLineContent(lineNumber);
const indent = TextModel.computeIndentLevel(lineContent, tabSize);
const lineHasSpace = config.fontInfo.spaceWidth * indent > 22;
const isFolded = (lineNumber) => {
return lineNumber > 2 && this._editor.getTopForLineNumber(lineNumber) === this._editor.getTopForLineNumber(lineNumber - 1);
};
let effectiveLineNumber = lineNumber;
if (!lineHasSpace) {
if (lineNumber > 1) {
if (lineNumber > 1 && !isFolded(lineNumber - 1)) {
effectiveLineNumber -= 1;
} else {
} else if (!isFolded(lineNumber + 1)) {
effectiveLineNumber += 1;
} else if (column * config.fontInfo.spaceWidth < 22) {
// cannot show lightbulb above/below and showing
// it inline would overlay the cursor...
this.hide();
return;
}
}

View File

@@ -2,11 +2,9 @@
* 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 { dispose, IDisposable } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { TextModel } from 'vs/editor/common/model/textModel';
import { CodeAction, CodeActionContext, CodeActionProvider, CodeActionProviderRegistry, Command, LanguageIdentifier, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes';
@@ -194,4 +192,27 @@ suite('CodeAction', () => {
assert.strictEqual(actions[0].title, 'a');
}
});
test('getCodeActions should not invoke code action providers filtered out by providedCodeActionKinds', async function () {
let wasInvoked = false;
const provider = new class implements CodeActionProvider {
provideCodeActions() {
wasInvoked = true;
return [];
}
providedCodeActionKinds = [CodeActionKind.Refactor.value];
};
disposables.push(CodeActionProviderRegistry.register('fooLang', provider));
const actions = await getCodeActions(model, new Range(1, 1, 2, 1), {
type: 'auto',
filter: {
kind: CodeActionKind.QuickFix
}
});
assert.strictEqual(actions.length, 0);
assert.strictEqual(wasInvoked, false);
});
});

View File

@@ -5,7 +5,7 @@
import * as assert from 'assert';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Selection } from 'vs/editor/common/core/selection';
import { TextModel } from 'vs/editor/common/model/textModel';
@@ -135,21 +135,33 @@ suite('CodeAction', () => {
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
});
});
// // case 2 - selection over multiple lines & manual trigger -> lightbulb
// await new TPromise(resolve => {
test('Orcale -> should only auto trigger once for cursor and marker update right after each other', done => {
const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
disposables.push(reg);
// editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
let triggerCount = 0;
const oracle = new CodeActionOracle(editor, markerService, e => {
assert.equal(e.trigger.type, 'auto');
++triggerCount;
// 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 }));
// give time for second trigger before completing test
setTimeout(() => {
oracle.dispose();
assert.strictEqual(triggerCount, 1);
done();
}, 50);
}, 5 /*delay*/);
// oracle.dispose();
// resolve(null);
// }, 5);
markerService.changeOne('fake', uri, [{
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
message: 'error',
severity: 1,
code: '',
source: ''
}]);
// oracle.trigger('manual');
// });
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
});
});