diff --git a/src/vs/workbench/contrib/markers/electron-browser/markers.ts b/src/vs/workbench/contrib/markers/electron-browser/markers.ts index 2ae1c9289030c3edffd214f4c6a6bfd37c5be169..706cd7ba117d677d2b006d91ca7aeaadbfe014bb 100644 --- a/src/vs/workbench/contrib/markers/electron-browser/markers.ts +++ b/src/vs/workbench/contrib/markers/electron-browser/markers.ts @@ -4,26 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkersModel, compareMarkersByUri, Marker } from './markersModel'; +import { MarkersModel, compareMarkersByUri } from './markersModel'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IMarkerService, MarkerSeverity, IMarker, IMarkerData } from 'vs/platform/markers/common/markers'; +import { IMarkerService, MarkerSeverity, IMarker } from 'vs/platform/markers/common/markers'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { localize } from 'vs/nls'; import Constants from './constants'; import { URI } from 'vs/base/common/uri'; import { groupBy } from 'vs/base/common/arrays'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IAction, Action } from 'vs/base/common/actions'; -import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { CodeAction } from 'vs/editor/common/modes'; -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 { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; export const IMarkersWorkbenchService = createDecorator('markersWorkbenchService'); @@ -35,8 +24,6 @@ export interface IFilter { export interface IMarkersWorkbenchService { _serviceBrand: any; readonly markersModel: MarkersModel; - hasQuickFixes(marker: Marker): Promise; - getQuickFixActions(marker: Marker): Promise; } export class MarkersWorkbenchService extends Disposable implements IMarkersWorkbenchService { @@ -44,17 +31,9 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb readonly markersModel: MarkersModel; - private readonly allFixesCache: Map> = new Map>(); - private readonly codeActionsPromises: Map>> = new Map>>(); - private readonly codeActions: Map> = new Map>(); - constructor( @IMarkerService private readonly markerService: IMarkerService, @IInstantiationService instantiationService: IInstantiationService, - @IBulkEditService private readonly bulkEditService: IBulkEditService, - @ICommandService private readonly commandService: ICommandService, - @IEditorService private readonly editorService: IEditorService, - @IModelService private readonly modelService: IModelService ) { super(); this.markersModel = this._register(instantiationService.createInstance(MarkersModel, this.readMarkers())); @@ -68,17 +47,6 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb private onMarkerChanged(resources: URI[]): void { for (const resource of resources) { - const allFixes = this.allFixesCache.get(resource.toString()); - if (allFixes) { - allFixes.cancel(); - this.allFixesCache.delete(resource.toString()); - } - const codeActions = this.codeActionsPromises.get(resource.toString()); - if (codeActions) { - codeActions.forEach(promise => promise.cancel()); - this.codeActionsPromises.delete(resource.toString()); - } - this.codeActions.delete(resource.toString()); this.markersModel.setResourceMarkers(resource, this.readMarkers(resource)); } } @@ -87,93 +55,6 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb return this.markerService.read({ resource, severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); } - getQuickFixActions(marker: Marker): Promise { - const markerKey = IMarkerData.makeKey(marker.marker); - let codeActionsPerMarker = this.codeActions.get(marker.resource.toString()); - if (!codeActionsPerMarker) { - codeActionsPerMarker = new Map(); - this.codeActions.set(marker.resource.toString(), codeActionsPerMarker); - } - const codeActions = codeActionsPerMarker.get(markerKey); - if (codeActions) { - return Promise.resolve(this.toActions(codeActions, marker)); - } else { - let codeActionsPromisesPerMarker = this.codeActionsPromises.get(marker.resource.toString()); - if (!codeActionsPromisesPerMarker) { - codeActionsPromisesPerMarker = new Map>(); - this.codeActionsPromises.set(marker.resource.toString(), codeActionsPromisesPerMarker); - } - if (!codeActionsPromisesPerMarker.has(markerKey)) { - const codeActionsPromise = this.getFixes(marker); - codeActionsPromisesPerMarker.set(markerKey, codeActionsPromise); - codeActionsPromise.then(codeActions => codeActionsPerMarker!.set(markerKey, codeActions)); - } - // Wait for 100ms for code actions fetching. - return timeout(100).then(() => this.toActions(codeActionsPerMarker!.get(markerKey) || [], marker)); - } - } - - private toActions(codeActions: CodeAction[], marker: Marker): IAction[] { - return codeActions.map(codeAction => new Action( - codeAction.command ? codeAction.command.id : codeAction.title, - codeAction.title, - undefined, - true, - () => { - return this.openFileAtMarker(marker) - .then(() => applyCodeAction(codeAction, this.bulkEditService, this.commandService)); - })); - } - - async hasQuickFixes(marker: Marker): Promise { - if (!this.modelService.getModel(marker.resource)) { - // Return early, If the model is not yet created - return false; - } - let allFixesPromise = this.allFixesCache.get(marker.resource.toString()); - if (!allFixesPromise) { - allFixesPromise = this._getFixes(marker.resource); - this.allFixesCache.set(marker.resource.toString(), allFixesPromise); - } - const allFixes = await allFixesPromise; - if (allFixes.length) { - const markerKey = IMarkerData.makeKey(marker.marker); - for (const fix of allFixes) { - if (fix.diagnostics && fix.diagnostics.some(d => IMarkerData.makeKey(d) === markerKey)) { - return true; - } - } - } - return false; - } - - private openFileAtMarker(element: Marker): Promise { - const { resource, selection } = { resource: element.resource, selection: element.range }; - return this.editorService.openEditor({ - resource, - options: { - selection, - preserveFocus: true, - pinned: false, - revealIfVisible: true - }, - }, ACTIVE_GROUP).then(() => undefined); - } - - private getFixes(marker: Marker): CancelablePromise { - return this._getFixes(marker.resource, new Range(marker.range.startLineNumber, marker.range.startColumn, marker.range.endLineNumber, marker.range.endColumn)); - } - - private _getFixes(uri: URI, range?: Range): CancelablePromise { - return createCancelablePromise(cancellationToken => { - const model = this.modelService.getModel(uri); - if (model) { - return getCodeActions(model, range ? range : model.getFullModelRange(), { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }, cancellationToken); - } - return Promise.resolve([]); - }); - } - } export class ActivityUpdater extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/markers/electron-browser/markersPanel.ts b/src/vs/workbench/contrib/markers/electron-browser/markersPanel.ts index 47bd53267b6274d3d2b6fab658ab4b7f6e4f7bd9..ff973cff87926d9a8abf699f792c8fadcd49c4ed 100644 --- a/src/vs/workbench/contrib/markers/electron-browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/electron-browser/markersPanel.ts @@ -385,6 +385,18 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { this.filterInputActionItem.focus(); } })); + + this._register(Event.any(this.tree.onDidChangeSelection, this.tree.onDidChangeFocus)(() => { + const elements: TreeElement[] = [...this.tree.getSelection(), ...this.tree.getFocus()]; + for (const element of elements) { + if (element instanceof Marker) { + const viewModel = this.markersViewModel.getViewModel(element); + if (viewModel) { + viewModel.showLightBulb(); + } + } + } + })); } private createActions(): void { @@ -611,34 +623,35 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { e.browserEvent.preventDefault(); e.browserEvent.stopPropagation(); - this._getMenuActions(e.element).then(actions => { - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions, - getActionItem: (action) => { - const keybinding = this.keybindingService.lookupKeybinding(action.id); - if (keybinding) { - return new ActionItem(action, action, { label: true, keybinding: keybinding.getLabel() }); - } - return null; - }, - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - this.tree.domFocus(); - } + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => this.getMenuActions(e.element), + getActionItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionItem(action, action, { label: true, keybinding: keybinding.getLabel() }); } - }); + return null; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.tree.domFocus(); + } + } }); } - private async _getMenuActions(element: TreeElement): Promise { + private getMenuActions(element: TreeElement): IAction[] { const result: IAction[] = []; if (element instanceof Marker) { - const quickFixActions = await this.markersWorkbenchService.getQuickFixActions(element); - if (quickFixActions.length) { - result.push(...quickFixActions); - result.push(new Separator()); + const viewModel = this.markersViewModel.getViewModel(element); + if (viewModel) { + const quickFixActions = viewModel.quickFixAction.quickFixes; + if (quickFixActions.length) { + result.push(...quickFixActions); + result.push(new Separator()); + } } } diff --git a/src/vs/workbench/contrib/markers/electron-browser/markersPanelActions.ts b/src/vs/workbench/contrib/markers/electron-browser/markersPanelActions.ts index 31a2a425faf0bbbf4191e044726ff2540db590c3..e05f65c46e38ef5f255ba9a984da92c944b3e4f6 100644 --- a/src/vs/workbench/contrib/markers/electron-browser/markersPanelActions.ts +++ b/src/vs/workbench/contrib/markers/electron-browser/markersPanelActions.ts @@ -5,7 +5,7 @@ import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; -import { Action, IActionChangeEvent } from 'vs/base/common/actions'; +import { Action, IActionChangeEvent, IAction } from 'vs/base/common/actions'; import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -25,9 +25,7 @@ import { localize } from 'vs/nls'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; -import { Marker } from 'vs/workbench/contrib/markers/electron-browser/markersModel'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { isEqual } from 'vs/base/common/resources'; +import { Marker } from 'vs/workbench/parts/markers/electron-browser/markersModel'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { FilterOptions } from 'vs/workbench/contrib/markers/electron-browser/markersFilterOptions'; @@ -296,28 +294,25 @@ export class QuickFixAction extends Action { public static readonly ID: string = 'workbench.actions.problems.quickfix'; - private updated: boolean = false; private disposables: IDisposable[] = []; private readonly _onShowQuickFixes: Emitter = new Emitter(); readonly onShowQuickFixes: Event = this._onShowQuickFixes.event; + private _quickFixes: IAction[] = []; + get quickFixes(): IAction[] { + return this._quickFixes; + } + set quickFixes(quickFixes: IAction[]) { + this._quickFixes = quickFixes; + this.enabled = this._quickFixes.length > 0; + } + + constructor( readonly marker: Marker, - @IModelService modelService: IModelService, - @IMarkersWorkbenchService private readonly markerWorkbenchService: IMarkersWorkbenchService, ) { super(QuickFixAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX, 'markers-panel-action-quickfix', false); - this.disposables.push(this._onShowQuickFixes); - if (modelService.getModel(this.marker.resource)) { - this.update(); - } else { - modelService.onModelAdded(model => { - if (isEqual(model.uri, marker.resource)) { - this.update(); - } - }, this, this.disposables); - } } run(): Promise { @@ -325,13 +320,6 @@ export class QuickFixAction extends Action { return Promise.resolve(); } - private update(): void { - if (!this.updated) { - this.markerWorkbenchService.hasQuickFixes(this.marker).then(hasFixes => this.enabled = hasFixes); - this.updated = true; - } - } - dispose(): void { dispose(this.disposables); super.dispose(); @@ -342,7 +330,6 @@ export class QuickFixActionItem extends ActionItem { constructor(action: QuickFixAction, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IMarkersWorkbenchService private readonly markerWorkbenchService: IMarkersWorkbenchService ) { super(null, action, { icon: true, label: false }); } @@ -360,11 +347,12 @@ export class QuickFixActionItem extends ActionItem { return; } const elementPosition = DOM.getDomNodePagePosition(this.element); - this.markerWorkbenchService.getQuickFixActions((this.getAction()).marker).then(actions => { + const quickFixes = (this.getAction()).quickFixes; + if (quickFixes.length) { this.contextMenuService.showContextMenu({ getAnchor: () => ({ x: elementPosition.left + 10, y: elementPosition.top + elementPosition.height + 4 }), - getActions: () => actions + getActions: () => quickFixes }); - }); + } } } diff --git a/src/vs/workbench/contrib/markers/electron-browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/electron-browser/markersTreeViewer.ts index b0d2488be69defa7a58fb1108a9fc3e2259c2920..c68847f9ea4dd680ed5f0a15cf1a7eeeb4e13d73 100644 --- a/src/vs/workbench/contrib/markers/electron-browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/electron-browser/markersTreeViewer.ts @@ -19,7 +19,7 @@ import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/l import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { QuickFixAction, QuickFixActionItem } from 'vs/workbench/contrib/markers/electron-browser/markersPanelActions'; import { ILabelService } from 'vs/platform/label/common/label'; -import { dirname, basename } from 'vs/base/common/resources'; +import { dirname, basename, isEqual } from 'vs/base/common/resources'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeFilter, TreeVisibility, TreeFilterResult, ITreeRenderer, ITreeNode, ITreeDragAndDrop, ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree'; import { FilterOptions } from 'vs/workbench/contrib/markers/electron-browser/markersFilterOptions'; @@ -28,11 +28,22 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { Action } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; +import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; +import { IModelService } from 'vs/editor/common/services/modelService'; +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 { ITextModel } from 'vs/editor/common/model'; +import { CodeAction } from 'vs/editor/common/modes'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; export type TreeElement = ResourceMarkers | Marker | RelatedInformation; @@ -235,7 +246,7 @@ class MarkerWidget extends Disposable { private disposables: IDisposable[] = []; constructor( - parent: HTMLElement, + private parent: HTMLElement, private readonly markersViewModel: MarkersViewModel, instantiationService: IInstantiationService ) { @@ -262,6 +273,8 @@ class MarkerWidget extends Disposable { this.renderMultilineActionbar(element); this.renderMessageAndDetails(element, filterData); + this.disposables.push(dom.addDisposableListener(this.parent, dom.EventType.MOUSE_OVER, () => this.markersViewModel.onMarkerMouseHover(element))); + this.disposables.push(dom.addDisposableListener(this.parent, dom.EventType.MOUSE_LEAVE, () => this.markersViewModel.onMarkerMouseLeave(element))); } private renderQuickfixActionbar(marker: Marker): void { @@ -474,11 +487,26 @@ export class MarkerViewModel extends Disposable { private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; + private modelPromise: CancelablePromise | null = null; + private codeActionsPromise: CancelablePromise | null = null; + constructor( private readonly marker: Marker, - @IInstantiationService private instantiationService: IInstantiationService + @IModelService private modelService: IModelService, + @IInstantiationService private instantiationService: IInstantiationService, + @IBulkEditService private readonly bulkEditService: IBulkEditService, + @ICommandService private readonly commandService: ICommandService, + @IEditorService private readonly editorService: IEditorService ) { super(); + this._register(toDisposable(() => { + if (this.modelPromise) { + this.modelPromise.cancel(); + } + if (this.codeActionsPromise) { + this.codeActionsPromise.cancel(); + } + })); } private _multiline: boolean = true; @@ -500,6 +528,91 @@ export class MarkerViewModel extends Disposable { } return this._quickFixAction; } + + showLightBulb(): void { + this.setQuickFixes(true); + } + + showQuickfixes(): void { + this.setQuickFixes(false).then(() => this.quickFixAction.run()); + } + + async getQuickFixes(waitForModel: boolean): Promise { + const codeActions = await this.getCodeActions(waitForModel); + return codeActions ? this.toActions(codeActions) : []; + } + + private async setQuickFixes(waitForModel: boolean): Promise { + const quickFixes = await this.getQuickFixes(waitForModel); + this.quickFixAction.quickFixes = quickFixes; + } + + private getCodeActions(waitForModel: boolean): Promise { + if (this.codeActionsPromise !== null) { + return this.codeActionsPromise; + } + return this.getModel(waitForModel) + .then(model => { + if (model) { + if (!this.codeActionsPromise) { + this.codeActionsPromise = createCancelablePromise(cancellationToken => { + console.log('Fetching code actions for ', this.marker.marker.message); + return getCodeActions(model, new Range(this.marker.range.startLineNumber, this.marker.range.startColumn, this.marker.range.endLineNumber, this.marker.range.endColumn), { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }, cancellationToken); + }); + } + return this.codeActionsPromise; + } + return null; + }); + } + + private toActions(codeActions: CodeAction[]): IAction[] { + return codeActions.map(codeAction => new Action( + codeAction.command ? codeAction.command.id : codeAction.title, + codeAction.title, + undefined, + true, + () => { + return this.openFileAtMarker(this.marker) + .then(() => applyCodeAction(codeAction, this.bulkEditService, this.commandService)); + })); + } + + private openFileAtMarker(element: Marker): Promise { + const { resource, selection } = { resource: element.resource, selection: element.range }; + return this.editorService.openEditor({ + resource, + options: { + selection, + preserveFocus: true, + pinned: false, + revealIfVisible: true + }, + }, ACTIVE_GROUP).then(() => undefined); + } + + private getModel(waitForModel: boolean): Promise { + const model = this.modelService.getModel(this.marker.resource); + if (model) { + return Promise.resolve(model); + } + if (waitForModel) { + if (this.modelPromise === null) { + this.modelPromise = createCancelablePromise(cancellationToken => { + return new Promise((c) => { + this._register(this.modelService.onModelAdded(model => { + if (isEqual(model.uri, this.marker.resource)) { + c(model); + } + })); + }); + }); + } + return this.modelPromise; + } + return Promise.resolve(null); + } + } export class MarkersViewModel extends Disposable { @@ -512,6 +625,9 @@ export class MarkersViewModel extends Disposable { private bulkUpdate: boolean = false; + private hoveredMarker: Marker; + private hoverDelayer: Delayer = new Delayer(300); + constructor( multiline: boolean = true, @IInstantiationService private instantiationService: IInstantiationService @@ -546,6 +662,9 @@ export class MarkersViewModel extends Disposable { dispose(value.disposables); } this.markersViewStates.delete(marker.hash); + if (this.hoveredMarker === marker) { + this.hoveredMarker = null; + } } this.markersPerResource.delete(resource.toString()); } @@ -555,6 +674,24 @@ export class MarkersViewModel extends Disposable { return value ? value.viewModel : null; } + onMarkerMouseHover(marker: Marker): void { + this.hoveredMarker = marker; + this.hoverDelayer.trigger(() => { + if (this.hoveredMarker) { + const model = this.getViewModel(this.hoveredMarker); + if (model) { + model.showLightBulb(); + } + } + }); + } + + onMarkerMouseLeave(marker: Marker): void { + if (this.hoveredMarker === marker) { + this.hoveredMarker = null; + } + } + private _multiline: boolean = true; get multiline(): boolean { return this._multiline;