From 75a90e84567bb177a8a5a641235811ddf831aa5f Mon Sep 17 00:00:00 2001 From: krassowski Date: Mon, 31 Aug 2020 15:56:35 +0100 Subject: [PATCH] Implement context menu for diagnostics panel --- .../src/features/diagnostics/diagnostics.ts | 188 +++++++++++++++++- .../src/features/diagnostics/index.ts | 44 +--- .../src/features/diagnostics/listing.tsx | 32 ++- 3 files changed, 215 insertions(+), 49 deletions(-) diff --git a/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.ts b/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.ts index c97e767c1..bd33b3902 100644 --- a/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.ts +++ b/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.ts @@ -6,8 +6,10 @@ import { DiagnosticSeverity } from '../../lsp'; import { DefaultMap, uris_equal } from '../../utils'; import { MainAreaWidget } from '@jupyterlab/apputils'; import { + DIAGNOSTICS_LISTING_CLASS, DiagnosticsDatabase, DiagnosticsListing, + IDiagnosticsRow, IEditorDiagnostic } from './listing'; import { VirtualDocument } from '../../virtual/document'; @@ -15,14 +17,23 @@ import { FeatureSettings } from '../../feature'; import { CodeMirrorIntegration } from '../../editor_integration/codemirror'; import { CodeDiagnostics as LSPDiagnosticsSettings } from '../../_diagnostics'; import { CodeMirrorVirtualEditor } from '../../virtual/codemirror_editor'; -import { LabIcon } from '@jupyterlab/ui-components'; +import { copyIcon, LabIcon } from '@jupyterlab/ui-components'; import diagnosticsSvg from '../../../style/icons/diagnostics.svg'; +import { Menu } from '@lumino/widgets'; +import { JupyterFrontEnd } from '@jupyterlab/application'; +import { jumpToIcon } from '../jump_to'; export const diagnosticsIcon = new LabIcon({ name: 'lsp:diagnostics', svgstr: diagnosticsSvg }); +const CMD_COLUMN_VISIBILITY = 'lsp-set-column-visibility'; +const CMD_JUMP_TO_DIAGNOSTIC = 'lsp-jump-to-diagnostic'; +const CMD_COPY_DIAGNOSTIC = 'lsp-copy-diagnostic'; +const CMD_IGNORE_DIAGNOSTIC_CODE = 'lsp-ignore-diagnostic-code'; +const CMD_IGNORE_DIAGNOSTIC_MSG = 'lsp-ignore-diagnostic-message'; + class DiagnosticsPanel { private _content: DiagnosticsListing = null; private _widget: MainAreaWidget = null; @@ -61,6 +72,179 @@ class DiagnosticsPanel { } this.widget.content.update(); } + + register(app: JupyterFrontEnd) { + const widget = this.widget; + + let get_column = (name: string) => { + // TODO: a hashmap in the panel itself? + for (let column of widget.content.columns) { + if (column.name === name) { + return column; + } + } + }; + + /** Columns Menu **/ + let columns_menu = new Menu({ commands: app.commands }); + columns_menu.title.label = 'Panel columns'; + + app.commands.addCommand(CMD_COLUMN_VISIBILITY, { + execute: args => { + let column = get_column(args['name'] as string); + column.is_visible = !column.is_visible; + widget.update(); + }, + label: args => args['name'] as string, + isToggled: args => { + let column = get_column(args['name'] as string); + return column.is_visible; + } + }); + + for (let column of widget.content.columns) { + columns_menu.addItem({ + command: CMD_COLUMN_VISIBILITY, + args: { name: column.name } + }); + } + app.contextMenu.addItem({ + selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' th', + submenu: columns_menu, + type: 'submenu' + }); + + /** Diagnostics Menu **/ + let ignore_diagnostics_menu = new Menu({ commands: app.commands }); + ignore_diagnostics_menu.title.label = 'Ignore diagnostics like this'; + + let get_row = (): IDiagnosticsRow => { + let tr = app.contextMenuHitTest( + node => node.tagName.toLowerCase() == 'tr' + ); + if (!tr) { + return; + } + return this.widget.content.get_diagnostic(tr.dataset.key); + }; + + ignore_diagnostics_menu.addItem({ + command: CMD_IGNORE_DIAGNOSTIC_CODE + }); + ignore_diagnostics_menu.addItem({ + command: CMD_IGNORE_DIAGNOSTIC_MSG + }); + app.commands.addCommand(CMD_IGNORE_DIAGNOSTIC_CODE, { + execute: () => { + const diagnostic = get_row().data.diagnostic; + let current = this.content.model.settings.composite.ignoreCodes; + this.content.model.settings.set('ignoreCodes', [ + ...current, + diagnostic.code + ]); + widget.update(); + }, + isVisible: () => { + const row = get_row(); + if (!row) { + return false; + } + const diagnostic = row.data.diagnostic; + return !!diagnostic.code; + }, + label: () => { + const row = get_row(); + if (!row) { + return ''; + } + const diagnostic = row.data.diagnostic; + return `Ignore diagnostics with "${diagnostic.code}" code`; + } + }); + app.commands.addCommand(CMD_IGNORE_DIAGNOSTIC_MSG, { + execute: () => { + const row = get_row(); + const diagnostic = row.data.diagnostic; + let current = this.content.model.settings.composite + .ignoreMessagesPatterns; + this.content.model.settings.set('ignoreMessagesPatterns', [ + ...current, + diagnostic.message + ]); + // TODO trigger actual db update + widget.update(); + }, + isVisible: () => { + const row = get_row(); + if (!row) { + return false; + } + const diagnostic = row.data.diagnostic; + return !!diagnostic.message; + }, + label: () => { + const row = get_row(); + if (!row) { + return ''; + } + const diagnostic = row.data.diagnostic; + return `Ignore diagnostics with "${diagnostic.message}" message`; + } + }); + + app.commands.addCommand(CMD_JUMP_TO_DIAGNOSTIC, { + execute: () => { + const row = get_row(); + this.widget.content.jump_to(row); + }, + label: 'Jump to location', + icon: jumpToIcon + }); + + app.commands.addCommand(CMD_COPY_DIAGNOSTIC, { + execute: () => { + const row = get_row(); + if (!row) { + return; + } + const message = row.data.diagnostic.message; + navigator.clipboard + .writeText(message) + .then(() => { + this.content.model.status_message.set( + `Successfully copied "${message}" to clipboard` + ); + }) + .catch(() => { + console.log( + 'Could not copy with clipboard.writeText interface, falling back' + ); + window.prompt( + 'Your browser protects clipboard from write operations; please copy the message manually', + message + ); + }); + }, + label: "Copy diagnostics' message", + icon: copyIcon + }); + + app.contextMenu.addItem({ + selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' tbody tr', + command: CMD_COPY_DIAGNOSTIC + }); + app.contextMenu.addItem({ + selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' tbody tr', + command: CMD_JUMP_TO_DIAGNOSTIC + }); + app.contextMenu.addItem({ + selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' tbody tr', + submenu: ignore_diagnostics_menu, + type: 'submenu' + }); + + this.is_registered = true; + } } export const diagnostics_panel = new DiagnosticsPanel(); @@ -105,6 +289,8 @@ export class DiagnosticsCM extends CodeMirrorIntegration { diagnostics_panel.content.model.diagnostics = this.diagnostics_db; diagnostics_panel.content.model.virtual_editor = this.virtual_editor; diagnostics_panel.content.model.adapter = this.adapter; + diagnostics_panel.content.model.settings = this.settings; + diagnostics_panel.content.model.status_message = this.status_message; diagnostics_panel.update(); }; diff --git a/packages/jupyterlab-lsp/src/features/diagnostics/index.ts b/packages/jupyterlab-lsp/src/features/diagnostics/index.ts index 56688e34f..dad8b48ba 100644 --- a/packages/jupyterlab-lsp/src/features/diagnostics/index.ts +++ b/packages/jupyterlab-lsp/src/features/diagnostics/index.ts @@ -1,7 +1,5 @@ import { ILSPFeatureManager, PLUGIN_ID } from '../../tokens'; import { FeatureSettings, IFeatureCommand } from '../../feature'; -import { Menu } from '@lumino/widgets'; -import { DIAGNOSTICS_LISTING_CLASS } from './listing'; import { JupyterFrontEnd, JupyterFrontEndPlugin @@ -22,46 +20,12 @@ const COMMANDS: IFeatureCommand[] = [ let diagnostics_feature = features.get(FEATURE_ID) as DiagnosticsCM; diagnostics_feature.switchDiagnosticsPanelSource(); - let panel_widget = diagnostics_panel.widget; - - let get_column = (name: string) => { - // TODO: a hashmap in the panel itself? - for (let column of panel_widget.content.columns) { - if (column.name === name) { - return column; - } - } - }; - if (!diagnostics_panel.is_registered) { - let columns_menu = new Menu({ commands: app.commands }); - app.commands.addCommand(CMD_COLUMN_VISIBILITY, { - execute: args => { - let column = get_column(args['name'] as string); - column.is_visible = !column.is_visible; - panel_widget.update(); - }, - label: args => args['name'] as string, - isToggled: args => { - let column = get_column(args['name'] as string); - return column.is_visible; - } - }); - columns_menu.title.label = 'Panel columns'; - for (let column of panel_widget.content.columns) { - columns_menu.addItem({ - command: CMD_COLUMN_VISIBILITY, - args: { name: column.name } - }); - } - app.contextMenu.addItem({ - selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' th', - submenu: columns_menu, - type: 'submenu' - }); - diagnostics_panel.is_registered = true; + diagnostics_panel.register(app); } + const panel_widget = diagnostics_panel.widget; + if (!panel_widget.isAttached) { app.shell.add(panel_widget, 'main', { ref: adapter.widget_id, @@ -77,8 +41,6 @@ const COMMANDS: IFeatureCommand[] = [ } ]; -const CMD_COLUMN_VISIBILITY = 'lsp-set-column-visibility'; - export const DIAGNOSTICS_PLUGIN: JupyterFrontEndPlugin = { id: FEATURE_ID, requires: [ILSPFeatureManager, ISettingRegistry], diff --git a/packages/jupyterlab-lsp/src/features/diagnostics/listing.tsx b/packages/jupyterlab-lsp/src/features/diagnostics/listing.tsx index 383aff321..ea208de27 100644 --- a/packages/jupyterlab-lsp/src/features/diagnostics/listing.tsx +++ b/packages/jupyterlab-lsp/src/features/diagnostics/listing.tsx @@ -9,11 +9,13 @@ import { VirtualDocument } from '../../virtual/document'; import '../../../style/diagnostics_listing.css'; import { DiagnosticSeverity } from '../../lsp'; import { CodeMirrorVirtualEditor } from '../../virtual/codemirror_editor'; -import { WidgetAdapter } from '../../adapters/adapter'; +import { StatusMessage, WidgetAdapter } from '../../adapters/adapter'; import { IVirtualEditor } from '../../virtual/editor'; import { CodeEditor } from '@jupyterlab/codeeditor'; import { IDocumentWidget } from '@jupyterlab/docregistry'; import { DocumentLocator, focus_on } from '../../components/utils'; +import { FeatureSettings } from '../../feature'; +import { CodeDiagnostics as LSPDiagnosticsSettings } from '../../_diagnostics'; /** * Diagnostic which is localized at a specific editor (cell) within a notebook @@ -39,7 +41,7 @@ export class DiagnosticsDatabase extends Map< } } -interface IDiagnosticsRow { +export interface IDiagnosticsRow { data: IEditorDiagnostic; key: string; document: VirtualDocument; @@ -132,6 +134,7 @@ export function message_without_code(diagnostic: lsProtocol.Diagnostic) { export class DiagnosticsListing extends VDomRenderer { sort_key = 'Severity'; sort_direction = 1; + private _diagnostics: Map; columns = [ new Column({ @@ -245,6 +248,7 @@ export class DiagnosticsListing extends VDomRenderer { } ); let flattened: IDiagnosticsRow[] = [].concat.apply([], by_document); + this._diagnostics = new Map(flattened.map(row => [row.key, row])); let sorted_column = this.columns.filter( column => column.name === this.sort_key @@ -263,8 +267,6 @@ export class DiagnosticsListing extends VDomRenderer { ); let elements = sorted.map(row => { - let cm_editor = row.data.editor; - let cells = columns_to_display.map(column => column.render_cell(row, context) ); @@ -272,10 +274,9 @@ export class DiagnosticsListing extends VDomRenderer { return ( { - focus_on(cm_editor.getWrapperElement()); - cm_editor.getDoc().setCursor(row.data.range.start); - cm_editor.focus(); + this.jump_to(row); }} > {cells} @@ -296,6 +297,21 @@ export class DiagnosticsListing extends VDomRenderer { ); } + + get_diagnostic(key: string): IDiagnosticsRow { + if (!this._diagnostics.has(key)) { + console.warn('Could not find the diagnostics row with key', key); + return; + } + return this._diagnostics.get(key); + } + + jump_to(row: IDiagnosticsRow) { + const cm_editor = row.data.editor; + focus_on(cm_editor.getWrapperElement()); + cm_editor.getDoc().setCursor(row.data.range.start); + cm_editor.focus(); + } } export namespace DiagnosticsListing { @@ -306,6 +322,8 @@ export namespace DiagnosticsListing { diagnostics: DiagnosticsDatabase; virtual_editor: CodeMirrorVirtualEditor; adapter: WidgetAdapter; + settings: FeatureSettings; + status_message: StatusMessage; constructor() { super();