diff --git a/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.spec.ts b/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.spec.ts index 83e6ac23f..edc5cd585 100644 --- a/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.spec.ts +++ b/packages/jupyterlab-lsp/src/features/diagnostics/diagnostics.spec.ts @@ -25,6 +25,7 @@ import { MockNotebookAdapter } from '../../testutils'; import { foreignCodeExtractors } from '../../transclusions/ipython/extractors'; +import { VirtualDocument } from '../../virtual/document'; import { diagnosticsPanel } from './diagnostics'; import { DiagnosticsFeature } from './feature'; @@ -114,7 +115,7 @@ describe('Diagnostics', () => { uri: env.documentOptions.path, diagnostics: diagnostics }, - env.adapter.virtualDocument!, + env.adapter.virtualDocument as VirtualDocument, env.adapter ); await framePromise(); @@ -138,7 +139,7 @@ describe('Diagnostics', () => { uri: env.documentOptions.path, diagnostics: diagnostics }, - env.adapter.virtualDocument!, + env.adapter.virtualDocument as VirtualDocument, env.adapter ); await framePromise(); @@ -163,7 +164,7 @@ describe('Diagnostics', () => { uri: env.documentOptions.path, diagnostics: diagnostics }, - env.adapter.virtualDocument!, + env.adapter.virtualDocument as VirtualDocument, env.adapter ); await framePromise(); @@ -188,7 +189,7 @@ describe('Diagnostics', () => { uri: env.documentOptions.path, diagnostics: diagnostics }, - env.adapter.virtualDocument!, + env.adapter.virtualDocument as VirtualDocument, env.adapter ); await framePromise(); @@ -297,7 +298,7 @@ describe('Diagnostics', () => { } ] }, - env.adapter.virtualDocument!, + env.adapter.virtualDocument as VirtualDocument, env.adapter ); await framePromise(); @@ -359,7 +360,7 @@ describe('Diagnostics', () => { } as lsProtocol.PublishDiagnosticsParams; // test guards against wrongly propagated responses: - await feature.handleDiagnostic(response, document, env.adapter); + await feature.handleDiagnostic(response, document as VirtualDocument, env.adapter); await env.adapter.updateDocuments(); await (env.adapter as MockNotebookAdapter).foreingDocumentOpened.promise; @@ -381,7 +382,7 @@ describe('Diagnostics', () => { response.uri = foreignDocument.uri; // correct propagation - await feature.handleDiagnostic(response, foreignDocument, env.adapter); + await feature.handleDiagnostic(response, foreignDocument as VirtualDocument, env.adapter); await framePromise(); await framePromise(); @@ -423,7 +424,7 @@ describe('Diagnostics', () => { } ] }, - document!, + document as VirtualDocument, env.adapter ); diff --git a/packages/jupyterlab-lsp/src/features/diagnostics/feature.ts b/packages/jupyterlab-lsp/src/features/diagnostics/feature.ts index 8a36c2418..4cf38d49b 100644 --- a/packages/jupyterlab-lsp/src/features/diagnostics/feature.ts +++ b/packages/jupyterlab-lsp/src/features/diagnostics/feature.ts @@ -1,4 +1,4 @@ -import { linter, Diagnostic, lintGutter } from '@codemirror/lint'; +import { linter, Diagnostic, Action, lintGutter } from '@codemirror/lint'; import { StateField, StateEffect, StateEffectType } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; import { INotebookShell } from '@jupyter-notebook/application'; @@ -13,9 +13,9 @@ import { WidgetLSPAdapter, IEditorPosition, IVirtualPosition, - ILSPConnection, - VirtualDocument + ILSPConnection } from '@jupyterlab/lsp'; +import { LSPConnection } from '@jupyterlab/lsp/lib/connection'; import { TranslationBundle } from '@jupyterlab/translation'; import { PromiseDelegate } from '@lumino/coreutils'; import * as lsProtocol from 'vscode-languageserver-protocol'; @@ -27,6 +27,9 @@ import { DiagnosticSeverity, DiagnosticTag } from '../../lsp'; import { PLUGIN_ID } from '../../tokens'; import { urisEqual } from '../../utils'; import { BrowserConsole } from '../../virtual/console'; +import { VirtualDocument } from '../../virtual/document'; +import { EditApplicator } from '../../edits'; + import { diagnosticsPanel } from './diagnostics'; import { DiagnosticsDatabase } from './listing'; @@ -43,6 +46,49 @@ const SeverityMap: Record< 4: 'hint' }; + +class DiagnosticView implements Diagnostic { + constructor( + protected options: Diagnostic, + protected actionsPromise: Promise<(lsProtocol.Command | lsProtocol.CodeAction)[] | null>, + protected applicator: EditApplicator + ) { + // no-op + this.from = options.from; + this.to = options.to; + this.severity = options.severity; + this.markClass = options.markClass; + this.source = options.source; + this.message = options.message; + this._actions = []; + actionsPromise.then((response) => { + if (!response) { + return; + } + this._actions = response.filter(actionOrCommand => (actionOrCommand as lsProtocol.CodeAction).edit).map((action: lsProtocol.CodeAction) => { + return { + name: action.title, + apply: () => { + applicator.applyEdit(action.edit!); + console.log('applying'); + } + } + }); + }); + } + private _actions: Action[] + from: number; + to: number; + severity: 'error' | 'warning' | 'info' | 'hint'; + markClass?: string; + source?: string; + message: string; + + get actions(): Action[] { + return this._actions; + } +} + export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { readonly id = DiagnosticsFeature.id; readonly capabilities: lsProtocol.ClientCapabilities = { @@ -51,6 +97,16 @@ export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { tagSupport: { valueSet: [DiagnosticTag.Deprecated, DiagnosticTag.Unnecessary] } + }, + codeAction: { + dynamicRegistration: true, + /* + codeActionLiteralSupport: { + codeActionKind: { + valueSet: ['quickfix'] + } + } + */ } } }; @@ -73,12 +129,12 @@ export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { // TODO: unregister connection.serverNotifications['textDocument/publishDiagnostics'].connect( async (connection: ILSPConnection, diagnostics) => { - await this.handleDiagnostic(diagnostics, virtualDocument, adapter); + await this.handleDiagnostic(diagnostics, virtualDocument as VirtualDocument, adapter); } ); virtualDocument.foreignDocumentClosed.connect((document, context) => { // TODO: check if we need to cast - this.clearDocumentDiagnostics(adapter, context.foreignDocument); + this.clearDocumentDiagnostics(adapter, context.foreignDocument as VirtualDocument); }); }); @@ -206,15 +262,42 @@ export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { classNames.push('cm-lsp-diagnostic-tag-' + DiagnosticTag[tag]); } - diagnostics.push({ + const virtualDocument = adapter.virtualDocument!; + const connection = this.connectionManager.connections.get( + virtualDocument.uri + )!; + let actionsPromise: Promise<(lsProtocol.Command | lsProtocol.CodeAction)[] | null>; + const applicator = new EditApplicator(editorDiagnostic.document, adapter); + + if (connection.serverCapabilities.codeActionProvider) { + const params: lsProtocol.CodeActionParams = { + textDocument: { + uri: editorDiagnostic.document.documentInfo.uri + }, + context: { + diagnostics: [editorDiagnostic.diagnostic], + //only: ['quickfix'] + }, + range: editorDiagnostic.diagnostic.range + }; + // @ts-ignore + const messageConnection = (connection as LSPConnection).connection; + actionsPromise = messageConnection.sendRequest( + 'textDocument/codeAction', + params + ); + } else { + actionsPromise = Promise.resolve(null); + } + + diagnostics.push(new DiagnosticView({ from: Math.min(from, view.state.doc.length), to: Math.min(to, view.state.doc.length), severity: severity, message: diagnostic.message, source: diagnostic.source, markClass: classNames.join(' ') - // TODO: actions - }); + }, actionsPromise, applicator)); } } return diagnostics; @@ -419,7 +502,8 @@ export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { setDiagnostics( response: lsProtocol.PublishDiagnosticsParams, document: VirtualDocument, - adapter: WidgetLSPAdapter + adapter: WidgetLSPAdapter, + force = false ) { let diagnosticsList: IEditorDiagnostic[] = []; // TODO: test case for severity class always being set, even if diagnostic has no severity @@ -504,7 +588,8 @@ export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { range: { start: startInEditor, end: endInEditor - } + }, + document }); } } @@ -535,7 +620,8 @@ export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { !( editorsWithDiagnostics.has(editor) || editorsWhichHadDiagnostics.has(editor) - ) + ) && + !force ) { continue; } @@ -585,7 +671,8 @@ export class DiagnosticsFeature extends Feature implements IDiagnosticsFeature { this.setDiagnostics( this._lastResponse, this._lastDocument, - this._lastAdapter + this._lastAdapter, + true ); } diagnosticsPanel.update(); diff --git a/packages/jupyterlab-lsp/src/features/diagnostics/tokens.ts b/packages/jupyterlab-lsp/src/features/diagnostics/tokens.ts index 445475097..4c9e42805 100644 --- a/packages/jupyterlab-lsp/src/features/diagnostics/tokens.ts +++ b/packages/jupyterlab-lsp/src/features/diagnostics/tokens.ts @@ -3,6 +3,7 @@ import { Token } from '@lumino/coreutils'; import * as lsProtocol from 'vscode-languageserver-protocol'; import { PLUGIN_ID } from '../../tokens'; +import { VirtualDocument } from '../../virtual/document'; /** * Diagnostic which is localized at a specific editor (cell) within a notebook @@ -15,6 +16,7 @@ export interface IEditorDiagnostic { start: IEditorPosition; end: IEditorPosition; }; + document: VirtualDocument } export interface IReadonlyDiagnosticsDatabase {