Skip to content

Commit 75a90e8

Browse files
committed
Implement context menu for diagnostics panel
1 parent c2d0877 commit 75a90e8

File tree

3 files changed

+215
-49
lines changed

3 files changed

+215
-49
lines changed

packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.ts

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,34 @@ import { DiagnosticSeverity } from '../../lsp';
66
import { DefaultMap, uris_equal } from '../../utils';
77
import { MainAreaWidget } from '@jupyterlab/apputils';
88
import {
9+
DIAGNOSTICS_LISTING_CLASS,
910
DiagnosticsDatabase,
1011
DiagnosticsListing,
12+
IDiagnosticsRow,
1113
IEditorDiagnostic
1214
} from './listing';
1315
import { VirtualDocument } from '../../virtual/document';
1416
import { FeatureSettings } from '../../feature';
1517
import { CodeMirrorIntegration } from '../../editor_integration/codemirror';
1618
import { CodeDiagnostics as LSPDiagnosticsSettings } from '../../_diagnostics';
1719
import { CodeMirrorVirtualEditor } from '../../virtual/codemirror_editor';
18-
import { LabIcon } from '@jupyterlab/ui-components';
20+
import { copyIcon, LabIcon } from '@jupyterlab/ui-components';
1921
import diagnosticsSvg from '../../../style/icons/diagnostics.svg';
22+
import { Menu } from '@lumino/widgets';
23+
import { JupyterFrontEnd } from '@jupyterlab/application';
24+
import { jumpToIcon } from '../jump_to';
2025

2126
export const diagnosticsIcon = new LabIcon({
2227
name: 'lsp:diagnostics',
2328
svgstr: diagnosticsSvg
2429
});
2530

31+
const CMD_COLUMN_VISIBILITY = 'lsp-set-column-visibility';
32+
const CMD_JUMP_TO_DIAGNOSTIC = 'lsp-jump-to-diagnostic';
33+
const CMD_COPY_DIAGNOSTIC = 'lsp-copy-diagnostic';
34+
const CMD_IGNORE_DIAGNOSTIC_CODE = 'lsp-ignore-diagnostic-code';
35+
const CMD_IGNORE_DIAGNOSTIC_MSG = 'lsp-ignore-diagnostic-message';
36+
2637
class DiagnosticsPanel {
2738
private _content: DiagnosticsListing = null;
2839
private _widget: MainAreaWidget<DiagnosticsListing> = null;
@@ -61,6 +72,179 @@ class DiagnosticsPanel {
6172
}
6273
this.widget.content.update();
6374
}
75+
76+
register(app: JupyterFrontEnd) {
77+
const widget = this.widget;
78+
79+
let get_column = (name: string) => {
80+
// TODO: a hashmap in the panel itself?
81+
for (let column of widget.content.columns) {
82+
if (column.name === name) {
83+
return column;
84+
}
85+
}
86+
};
87+
88+
/** Columns Menu **/
89+
let columns_menu = new Menu({ commands: app.commands });
90+
columns_menu.title.label = 'Panel columns';
91+
92+
app.commands.addCommand(CMD_COLUMN_VISIBILITY, {
93+
execute: args => {
94+
let column = get_column(args['name'] as string);
95+
column.is_visible = !column.is_visible;
96+
widget.update();
97+
},
98+
label: args => args['name'] as string,
99+
isToggled: args => {
100+
let column = get_column(args['name'] as string);
101+
return column.is_visible;
102+
}
103+
});
104+
105+
for (let column of widget.content.columns) {
106+
columns_menu.addItem({
107+
command: CMD_COLUMN_VISIBILITY,
108+
args: { name: column.name }
109+
});
110+
}
111+
app.contextMenu.addItem({
112+
selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' th',
113+
submenu: columns_menu,
114+
type: 'submenu'
115+
});
116+
117+
/** Diagnostics Menu **/
118+
let ignore_diagnostics_menu = new Menu({ commands: app.commands });
119+
ignore_diagnostics_menu.title.label = 'Ignore diagnostics like this';
120+
121+
let get_row = (): IDiagnosticsRow => {
122+
let tr = app.contextMenuHitTest(
123+
node => node.tagName.toLowerCase() == 'tr'
124+
);
125+
if (!tr) {
126+
return;
127+
}
128+
return this.widget.content.get_diagnostic(tr.dataset.key);
129+
};
130+
131+
ignore_diagnostics_menu.addItem({
132+
command: CMD_IGNORE_DIAGNOSTIC_CODE
133+
});
134+
ignore_diagnostics_menu.addItem({
135+
command: CMD_IGNORE_DIAGNOSTIC_MSG
136+
});
137+
app.commands.addCommand(CMD_IGNORE_DIAGNOSTIC_CODE, {
138+
execute: () => {
139+
const diagnostic = get_row().data.diagnostic;
140+
let current = this.content.model.settings.composite.ignoreCodes;
141+
this.content.model.settings.set('ignoreCodes', [
142+
...current,
143+
diagnostic.code
144+
]);
145+
widget.update();
146+
},
147+
isVisible: () => {
148+
const row = get_row();
149+
if (!row) {
150+
return false;
151+
}
152+
const diagnostic = row.data.diagnostic;
153+
return !!diagnostic.code;
154+
},
155+
label: () => {
156+
const row = get_row();
157+
if (!row) {
158+
return '';
159+
}
160+
const diagnostic = row.data.diagnostic;
161+
return `Ignore diagnostics with "${diagnostic.code}" code`;
162+
}
163+
});
164+
app.commands.addCommand(CMD_IGNORE_DIAGNOSTIC_MSG, {
165+
execute: () => {
166+
const row = get_row();
167+
const diagnostic = row.data.diagnostic;
168+
let current = this.content.model.settings.composite
169+
.ignoreMessagesPatterns;
170+
this.content.model.settings.set('ignoreMessagesPatterns', [
171+
...current,
172+
diagnostic.message
173+
]);
174+
// TODO trigger actual db update
175+
widget.update();
176+
},
177+
isVisible: () => {
178+
const row = get_row();
179+
if (!row) {
180+
return false;
181+
}
182+
const diagnostic = row.data.diagnostic;
183+
return !!diagnostic.message;
184+
},
185+
label: () => {
186+
const row = get_row();
187+
if (!row) {
188+
return '';
189+
}
190+
const diagnostic = row.data.diagnostic;
191+
return `Ignore diagnostics with "${diagnostic.message}" message`;
192+
}
193+
});
194+
195+
app.commands.addCommand(CMD_JUMP_TO_DIAGNOSTIC, {
196+
execute: () => {
197+
const row = get_row();
198+
this.widget.content.jump_to(row);
199+
},
200+
label: 'Jump to location',
201+
icon: jumpToIcon
202+
});
203+
204+
app.commands.addCommand(CMD_COPY_DIAGNOSTIC, {
205+
execute: () => {
206+
const row = get_row();
207+
if (!row) {
208+
return;
209+
}
210+
const message = row.data.diagnostic.message;
211+
navigator.clipboard
212+
.writeText(message)
213+
.then(() => {
214+
this.content.model.status_message.set(
215+
`Successfully copied "${message}" to clipboard`
216+
);
217+
})
218+
.catch(() => {
219+
console.log(
220+
'Could not copy with clipboard.writeText interface, falling back'
221+
);
222+
window.prompt(
223+
'Your browser protects clipboard from write operations; please copy the message manually',
224+
message
225+
);
226+
});
227+
},
228+
label: "Copy diagnostics' message",
229+
icon: copyIcon
230+
});
231+
232+
app.contextMenu.addItem({
233+
selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' tbody tr',
234+
command: CMD_COPY_DIAGNOSTIC
235+
});
236+
app.contextMenu.addItem({
237+
selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' tbody tr',
238+
command: CMD_JUMP_TO_DIAGNOSTIC
239+
});
240+
app.contextMenu.addItem({
241+
selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' tbody tr',
242+
submenu: ignore_diagnostics_menu,
243+
type: 'submenu'
244+
});
245+
246+
this.is_registered = true;
247+
}
64248
}
65249

66250
export const diagnostics_panel = new DiagnosticsPanel();
@@ -105,6 +289,8 @@ export class DiagnosticsCM extends CodeMirrorIntegration {
105289
diagnostics_panel.content.model.diagnostics = this.diagnostics_db;
106290
diagnostics_panel.content.model.virtual_editor = this.virtual_editor;
107291
diagnostics_panel.content.model.adapter = this.adapter;
292+
diagnostics_panel.content.model.settings = this.settings;
293+
diagnostics_panel.content.model.status_message = this.status_message;
108294
diagnostics_panel.update();
109295
};
110296

packages/jupyterlab-lsp/src/features/diagnostics/index.ts

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { ILSPFeatureManager, PLUGIN_ID } from '../../tokens';
22
import { FeatureSettings, IFeatureCommand } from '../../feature';
3-
import { Menu } from '@lumino/widgets';
4-
import { DIAGNOSTICS_LISTING_CLASS } from './listing';
53
import {
64
JupyterFrontEnd,
75
JupyterFrontEndPlugin
@@ -22,46 +20,12 @@ const COMMANDS: IFeatureCommand[] = [
2220
let diagnostics_feature = features.get(FEATURE_ID) as DiagnosticsCM;
2321
diagnostics_feature.switchDiagnosticsPanelSource();
2422

25-
let panel_widget = diagnostics_panel.widget;
26-
27-
let get_column = (name: string) => {
28-
// TODO: a hashmap in the panel itself?
29-
for (let column of panel_widget.content.columns) {
30-
if (column.name === name) {
31-
return column;
32-
}
33-
}
34-
};
35-
3623
if (!diagnostics_panel.is_registered) {
37-
let columns_menu = new Menu({ commands: app.commands });
38-
app.commands.addCommand(CMD_COLUMN_VISIBILITY, {
39-
execute: args => {
40-
let column = get_column(args['name'] as string);
41-
column.is_visible = !column.is_visible;
42-
panel_widget.update();
43-
},
44-
label: args => args['name'] as string,
45-
isToggled: args => {
46-
let column = get_column(args['name'] as string);
47-
return column.is_visible;
48-
}
49-
});
50-
columns_menu.title.label = 'Panel columns';
51-
for (let column of panel_widget.content.columns) {
52-
columns_menu.addItem({
53-
command: CMD_COLUMN_VISIBILITY,
54-
args: { name: column.name }
55-
});
56-
}
57-
app.contextMenu.addItem({
58-
selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' th',
59-
submenu: columns_menu,
60-
type: 'submenu'
61-
});
62-
diagnostics_panel.is_registered = true;
24+
diagnostics_panel.register(app);
6325
}
6426

27+
const panel_widget = diagnostics_panel.widget;
28+
6529
if (!panel_widget.isAttached) {
6630
app.shell.add(panel_widget, 'main', {
6731
ref: adapter.widget_id,
@@ -77,8 +41,6 @@ const COMMANDS: IFeatureCommand[] = [
7741
}
7842
];
7943

80-
const CMD_COLUMN_VISIBILITY = 'lsp-set-column-visibility';
81-
8244
export const DIAGNOSTICS_PLUGIN: JupyterFrontEndPlugin<void> = {
8345
id: FEATURE_ID,
8446
requires: [ILSPFeatureManager, ISettingRegistry],

packages/jupyterlab-lsp/src/features/diagnostics/listing.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import { VirtualDocument } from '../../virtual/document';
99
import '../../../style/diagnostics_listing.css';
1010
import { DiagnosticSeverity } from '../../lsp';
1111
import { CodeMirrorVirtualEditor } from '../../virtual/codemirror_editor';
12-
import { WidgetAdapter } from '../../adapters/adapter';
12+
import { StatusMessage, WidgetAdapter } from '../../adapters/adapter';
1313
import { IVirtualEditor } from '../../virtual/editor';
1414
import { CodeEditor } from '@jupyterlab/codeeditor';
1515
import { IDocumentWidget } from '@jupyterlab/docregistry';
1616
import { DocumentLocator, focus_on } from '../../components/utils';
17+
import { FeatureSettings } from '../../feature';
18+
import { CodeDiagnostics as LSPDiagnosticsSettings } from '../../_diagnostics';
1719

1820
/**
1921
* Diagnostic which is localized at a specific editor (cell) within a notebook
@@ -39,7 +41,7 @@ export class DiagnosticsDatabase extends Map<
3941
}
4042
}
4143

42-
interface IDiagnosticsRow {
44+
export interface IDiagnosticsRow {
4345
data: IEditorDiagnostic;
4446
key: string;
4547
document: VirtualDocument;
@@ -132,6 +134,7 @@ export function message_without_code(diagnostic: lsProtocol.Diagnostic) {
132134
export class DiagnosticsListing extends VDomRenderer<DiagnosticsListing.Model> {
133135
sort_key = 'Severity';
134136
sort_direction = 1;
137+
private _diagnostics: Map<string, IDiagnosticsRow>;
135138

136139
columns = [
137140
new Column({
@@ -245,6 +248,7 @@ export class DiagnosticsListing extends VDomRenderer<DiagnosticsListing.Model> {
245248
}
246249
);
247250
let flattened: IDiagnosticsRow[] = [].concat.apply([], by_document);
251+
this._diagnostics = new Map(flattened.map(row => [row.key, row]));
248252

249253
let sorted_column = this.columns.filter(
250254
column => column.name === this.sort_key
@@ -263,19 +267,16 @@ export class DiagnosticsListing extends VDomRenderer<DiagnosticsListing.Model> {
263267
);
264268

265269
let elements = sorted.map(row => {
266-
let cm_editor = row.data.editor;
267-
268270
let cells = columns_to_display.map(column =>
269271
column.render_cell(row, context)
270272
);
271273

272274
return (
273275
<tr
274276
key={row.key}
277+
data-key={row.key}
275278
onClick={() => {
276-
focus_on(cm_editor.getWrapperElement());
277-
cm_editor.getDoc().setCursor(row.data.range.start);
278-
cm_editor.focus();
279+
this.jump_to(row);
279280
}}
280281
>
281282
{cells}
@@ -296,6 +297,21 @@ export class DiagnosticsListing extends VDomRenderer<DiagnosticsListing.Model> {
296297
</table>
297298
);
298299
}
300+
301+
get_diagnostic(key: string): IDiagnosticsRow {
302+
if (!this._diagnostics.has(key)) {
303+
console.warn('Could not find the diagnostics row with key', key);
304+
return;
305+
}
306+
return this._diagnostics.get(key);
307+
}
308+
309+
jump_to(row: IDiagnosticsRow) {
310+
const cm_editor = row.data.editor;
311+
focus_on(cm_editor.getWrapperElement());
312+
cm_editor.getDoc().setCursor(row.data.range.start);
313+
cm_editor.focus();
314+
}
299315
}
300316

301317
export namespace DiagnosticsListing {
@@ -306,6 +322,8 @@ export namespace DiagnosticsListing {
306322
diagnostics: DiagnosticsDatabase;
307323
virtual_editor: CodeMirrorVirtualEditor;
308324
adapter: WidgetAdapter<any>;
325+
settings: FeatureSettings<LSPDiagnosticsSettings>;
326+
status_message: StatusMessage;
309327

310328
constructor() {
311329
super();

0 commit comments

Comments
 (0)