UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

295 lines (270 loc) 8.65 kB
import { JupyterFrontEnd } from '@jupyterlab/application'; import { MainAreaWidget, Notification } from '@jupyterlab/apputils'; import { nullTranslator, TranslationBundle } from '@jupyterlab/translation'; import { LabIcon, copyIcon } from '@jupyterlab/ui-components'; import { Menu } from '@lumino/widgets'; import diagnosticsSvg from '../../../style/icons/diagnostics.svg'; import { jumpToIcon } from '../jump_to'; import { DiagnosticsFeature } from './feature'; import { DIAGNOSTICS_LISTING_CLASS, DiagnosticsDatabase, DiagnosticsListing, IDiagnosticsRow } from './listing'; 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'; /** * Escape pattern to form a base of a regular expression. * The snippet comes from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping * and is in the Public Domain (CC0): * > Any copyright is dedicated to the Public Domain. * > http://creativecommons.org/publicdomain/zero/1.0/ */ function escapeRegExp(string: string) { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); } class DiagnosticsPanel { private _content: DiagnosticsListing | null = null; private _widget: MainAreaWidget<DiagnosticsListing> | null = null; feature: DiagnosticsFeature; isRegistered = false; trans: TranslationBundle; constructor(trans: TranslationBundle) { this.trans = trans; } get widget() { if (this._widget == null || this._widget.content.model == null) { if (this._widget && !this._widget.isDisposed) { this._widget.dispose(); } this._widget = this.initWidget(); } return this._widget; } get content() { return this.widget.content; } protected initWidget() { this._content = new DiagnosticsListing( new DiagnosticsListing.Model(this.trans) ); this._content.model.diagnostics = new DiagnosticsDatabase(); this._content.addClass('lsp-diagnostics-panel-content'); const widget = new MainAreaWidget({ content: this._content }); widget.id = 'lsp-diagnostics-panel'; widget.title.label = this.trans.__('Diagnostics Panel'); widget.title.closable = true; widget.title.icon = diagnosticsIcon; return widget; } update() { // if not attached, do not bother to update if (!this.widget.isAttached) { return; } this.widget.content.update(); } register(app: JupyterFrontEnd) { const widget = this.widget; let getColumn = (id: string) => { // TODO: a hashmap in the panel itself? for (let column of widget.content.columns) { if (column.id === id) { return column; } } return undefined; }; /** Columns Menu **/ let columnsMenu = new Menu({ commands: app.commands }); columnsMenu.title.label = this.trans.__('Panel columns'); app.commands.addCommand(CMD_COLUMN_VISIBILITY, { execute: args => { let column = getColumn(args['id'] as string)!; column.isVisible = !column.isVisible; widget.update(); }, label: args => this.trans.__(args['id'] as string), isToggled: args => { let column = getColumn(args['id'] as string); return column ? column.isVisible : false; } }); for (let column of widget.content.columns) { columnsMenu.addItem({ command: CMD_COLUMN_VISIBILITY, args: { id: column.id } }); } app.contextMenu.addItem({ selector: '.' + DIAGNOSTICS_LISTING_CLASS + ' th', submenu: columnsMenu, type: 'submenu' }); /** Diagnostics Menu **/ let ignoreDiagnosticsMenu = new Menu({ commands: app.commands }); ignoreDiagnosticsMenu.title.label = this.trans.__( 'Ignore diagnostics like this' ); let getRow = (): IDiagnosticsRow | undefined => { let tr = app.contextMenuHitTest( node => node.tagName.toLowerCase() == 'tr' ); if (!tr) { return; } return this.widget.content.getDiagnostic(tr.dataset.key!); }; ignoreDiagnosticsMenu.addItem({ command: CMD_IGNORE_DIAGNOSTIC_CODE }); ignoreDiagnosticsMenu.addItem({ command: CMD_IGNORE_DIAGNOSTIC_MSG }); app.commands.addCommand(CMD_IGNORE_DIAGNOSTIC_CODE, { execute: () => { const row = getRow(); if (!row) { console.warn( 'LPS: diagnostics row not found for ignore code execute()' ); return; } const diagnostic = row.data.diagnostic; let current = this.content.model.settings.composite.ignoreCodes; this.content.model.settings.set('ignoreCodes', [ ...current, diagnostic.code ]); this.feature.refreshDiagnostics(); }, isVisible: () => { const row = getRow(); if (!row) { return false; } const diagnostic = row.data.diagnostic; return !!diagnostic.code; }, label: () => { const row = getRow(); if (!row) { return ''; } const diagnostic = row.data.diagnostic; return this.trans.__( 'Ignore diagnostics with "%1" code', diagnostic.code ); } }); app.commands.addCommand(CMD_IGNORE_DIAGNOSTIC_MSG, { execute: () => { const row = getRow(); if (!row) { console.warn( 'LPS: diagnostics row not found for ignore message execute()' ); return; } const diagnostic = row.data.diagnostic; let current = this.content.model.settings.composite.ignoreMessagesPatterns; this.content.model.settings.set('ignoreMessagesPatterns', [ ...current, escapeRegExp(diagnostic.message) ]); this.feature.refreshDiagnostics(); }, isVisible: () => { const row = getRow(); if (!row) { return false; } const diagnostic = row.data.diagnostic; return !!diagnostic.message; }, label: () => { const row = getRow(); if (!row) { return ''; } const diagnostic = row.data.diagnostic; return this.trans.__( 'Ignore diagnostics with "%1" message', diagnostic.message ); } }); app.commands.addCommand(CMD_JUMP_TO_DIAGNOSTIC, { execute: () => { const row = getRow(); if (!row) { console.warn('LPS: diagnostics row not found for jump execute()'); return; } return this.widget.content.jumpTo(row); }, label: this.trans.__('Jump to location'), icon: jumpToIcon }); app.commands.addCommand(CMD_COPY_DIAGNOSTIC, { execute: () => { const row = getRow(); if (!row) { console.warn('LPS: diagnostics row not found for copy execute()'); return; } const message = row.data.diagnostic.message; navigator.clipboard .writeText(message) .then(() => { Notification.info( this.trans.__('Successfully copied "%1" to clipboard', message), { autoClose: 3 * 1000 } ); }) .catch(() => { console.warn( 'Could not copy with clipboard.writeText interface, falling back' ); window.prompt( this.trans.__( 'Your browser protects clipboard from write operations; please copy the message manually' ), message ); }); }, label: this.trans.__("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: ignoreDiagnosticsMenu, type: 'submenu' }); this.isRegistered = true; } } export const diagnosticsPanel = new DiagnosticsPanel( nullTranslator.load('jupyterlab_lsp') );