diff --git a/monaco-lsp-client/src/adapters/DiagnosticsCache.ts b/monaco-lsp-client/src/adapters/DiagnosticsCache.ts new file mode 100644 index 0000000000..3cc5b163ea --- /dev/null +++ b/monaco-lsp-client/src/adapters/DiagnosticsCache.ts @@ -0,0 +1,32 @@ +import { Diagnostic } from '../../src/types'; + +/** + * Per-document cache of the last set of LSP `Diagnostic` objects received + * from the server (via `publishDiagnostics` or `textDocument/diagnostic`). + * + * Monaco's `IMarker` model can't carry the full Diagnostic shape: `data` and + * `codeDescription` have no corresponding IMarker field, so once a Diagnostic + * is translated to a marker those values are lost. Features that need to + * round-trip the original Diagnostic to the server (notably + * `textDocument/codeAction`, which the spec requires to include `data`) read + * from this cache to recover the missing fields. + * + * Keyed by Monaco URI string (`model.uri.toString()`) so cleanup can be done + * directly from `monaco.editor.onWillDisposeModel` without needing the + * text-model bridge. + */ +export class DiagnosticsCache { + private readonly _byUri = new Map(); + + public set(uri: string, diagnostics: readonly Diagnostic[]): void { + this._byUri.set(uri, diagnostics); + } + + public get(uri: string): readonly Diagnostic[] | undefined { + return this._byUri.get(uri); + } + + public delete(uri: string): void { + this._byUri.delete(uri); + } +} diff --git a/monaco-lsp-client/src/adapters/LspClient.ts b/monaco-lsp-client/src/adapters/LspClient.ts index 1c5d73211e..912edfe46c 100644 --- a/monaco-lsp-client/src/adapters/LspClient.ts +++ b/monaco-lsp-client/src/adapters/LspClient.ts @@ -1,90 +1,97 @@ -import { IMessageTransport, TypedChannel } from "@hediet/json-rpc"; -import { LspCompletionFeature } from "./languageFeatures/LspCompletionFeature"; -import { LspHoverFeature } from "./languageFeatures/LspHoverFeature"; -import { LspSignatureHelpFeature } from "./languageFeatures/LspSignatureHelpFeature"; -import { LspDefinitionFeature } from "./languageFeatures/LspDefinitionFeature"; -import { LspDeclarationFeature } from "./languageFeatures/LspDeclarationFeature"; -import { LspTypeDefinitionFeature } from "./languageFeatures/LspTypeDefinitionFeature"; -import { LspImplementationFeature } from "./languageFeatures/LspImplementationFeature"; -import { LspReferencesFeature } from "./languageFeatures/LspReferencesFeature"; -import { LspDocumentHighlightFeature } from "./languageFeatures/LspDocumentHighlightFeature"; -import { LspDocumentSymbolFeature } from "./languageFeatures/LspDocumentSymbolFeature"; -import { LspRenameFeature } from "./languageFeatures/LspRenameFeature"; -import { LspCodeActionFeature } from "./languageFeatures/LspCodeActionFeature"; -import { LspCodeLensFeature } from "./languageFeatures/LspCodeLensFeature"; -import { LspDocumentLinkFeature } from "./languageFeatures/LspDocumentLinkFeature"; -import { LspFormattingFeature } from "./languageFeatures/LspFormattingFeature"; -import { LspRangeFormattingFeature } from "./languageFeatures/LspRangeFormattingFeature"; -import { LspOnTypeFormattingFeature } from "./languageFeatures/LspOnTypeFormattingFeature"; -import { LspFoldingRangeFeature } from "./languageFeatures/LspFoldingRangeFeature"; -import { LspSelectionRangeFeature } from "./languageFeatures/LspSelectionRangeFeature"; -import { LspInlayHintsFeature } from "./languageFeatures/LspInlayHintsFeature"; -import { LspSemanticTokensFeature } from "./languageFeatures/LspSemanticTokensFeature"; -import { LspDiagnosticsFeature } from "./languageFeatures/LspDiagnosticsFeature"; -import { api } from "../../src/types"; -import { LspConnection } from "./LspConnection"; +import { IMessageTransport, TypedChannel } from '@hediet/json-rpc'; +import { LspCompletionFeature } from './languageFeatures/LspCompletionFeature'; +import { LspHoverFeature } from './languageFeatures/LspHoverFeature'; +import { LspSignatureHelpFeature } from './languageFeatures/LspSignatureHelpFeature'; +import { LspDefinitionFeature } from './languageFeatures/LspDefinitionFeature'; +import { LspDeclarationFeature } from './languageFeatures/LspDeclarationFeature'; +import { LspTypeDefinitionFeature } from './languageFeatures/LspTypeDefinitionFeature'; +import { LspImplementationFeature } from './languageFeatures/LspImplementationFeature'; +import { LspReferencesFeature } from './languageFeatures/LspReferencesFeature'; +import { LspDocumentHighlightFeature } from './languageFeatures/LspDocumentHighlightFeature'; +import { LspDocumentSymbolFeature } from './languageFeatures/LspDocumentSymbolFeature'; +import { LspRenameFeature } from './languageFeatures/LspRenameFeature'; +import { LspCodeActionFeature } from './languageFeatures/LspCodeActionFeature'; +import { LspCodeLensFeature } from './languageFeatures/LspCodeLensFeature'; +import { LspDocumentLinkFeature } from './languageFeatures/LspDocumentLinkFeature'; +import { LspFormattingFeature } from './languageFeatures/LspFormattingFeature'; +import { LspRangeFormattingFeature } from './languageFeatures/LspRangeFormattingFeature'; +import { LspOnTypeFormattingFeature } from './languageFeatures/LspOnTypeFormattingFeature'; +import { LspFoldingRangeFeature } from './languageFeatures/LspFoldingRangeFeature'; +import { LspSelectionRangeFeature } from './languageFeatures/LspSelectionRangeFeature'; +import { LspInlayHintsFeature } from './languageFeatures/LspInlayHintsFeature'; +import { LspSemanticTokensFeature } from './languageFeatures/LspSemanticTokensFeature'; +import { LspDiagnosticsFeature } from './languageFeatures/LspDiagnosticsFeature'; +import { api } from '../../src/types'; +import { DiagnosticsCache } from './DiagnosticsCache'; +import { LspConnection } from './LspConnection'; import { LspCapabilitiesRegistry } from './LspCapabilitiesRegistry'; -import { TextDocumentSynchronizer } from "./TextDocumentSynchronizer"; -import { DisposableStore, IDisposable } from "../utils"; +import { TextDocumentSynchronizer } from './TextDocumentSynchronizer'; +import { DisposableStore, IDisposable, Disposable } from '../utils'; export class MonacoLspClient { - private _connection: LspConnection; - private readonly _capabilitiesRegistry: LspCapabilitiesRegistry; - private readonly _bridge: TextDocumentSynchronizer; + private _connection: LspConnection; + private readonly _capabilitiesRegistry: LspCapabilitiesRegistry; + private readonly _bridge: TextDocumentSynchronizer; - private _initPromise: Promise; + private _initPromise: Promise; - constructor(transport: IMessageTransport) { - const c = TypedChannel.fromTransport(transport); - const s = api.getServer(c, {}); - c.startListen(); + constructor(transport: IMessageTransport) { + const c = TypedChannel.fromTransport(transport); + const s = api.getServer(c, {}); + c.startListen(); - this._capabilitiesRegistry = new LspCapabilitiesRegistry(c); - this._bridge = new TextDocumentSynchronizer(s.server, this._capabilitiesRegistry); + this._capabilitiesRegistry = new LspCapabilitiesRegistry(c); + this._bridge = new TextDocumentSynchronizer(s.server, this._capabilitiesRegistry); - this._connection = new LspConnection(s.server, this._bridge, this._capabilitiesRegistry, c); - this.createFeatures(); + this._connection = new LspConnection( + s.server, + this._bridge, + this._capabilitiesRegistry, + c, + new DiagnosticsCache() + ); + this.createFeatures(); - this._initPromise = this._init(); - } + this._initPromise = this._init(); + } - private async _init() { - const result = await this._connection.server.initialize({ - processId: null, - capabilities: this._capabilitiesRegistry.getClientCapabilities(), - rootUri: null, - }); + private async _init() { + const result = await this._connection.server.initialize({ + processId: null, + capabilities: this._capabilitiesRegistry.getClientCapabilities(), + rootUri: null + }); - this._connection.server.initialized({}); - this._capabilitiesRegistry.setServerCapabilities(result.capabilities); - } + this._connection.server.initialized({}); + this._capabilitiesRegistry.setServerCapabilities(result.capabilities); + } - protected createFeatures(): IDisposable { - const store = new DisposableStore(); + protected createFeatures(): IDisposable { + const store = new DisposableStore(); - store.add(new LspCompletionFeature(this._connection)); - store.add(new LspHoverFeature(this._connection)); - store.add(new LspSignatureHelpFeature(this._connection)); - store.add(new LspDefinitionFeature(this._connection)); - store.add(new LspDeclarationFeature(this._connection)); - store.add(new LspTypeDefinitionFeature(this._connection)); - store.add(new LspImplementationFeature(this._connection)); - store.add(new LspReferencesFeature(this._connection)); - store.add(new LspDocumentHighlightFeature(this._connection)); - store.add(new LspDocumentSymbolFeature(this._connection)); - store.add(new LspRenameFeature(this._connection)); - store.add(new LspCodeActionFeature(this._connection)); - store.add(new LspCodeLensFeature(this._connection)); - store.add(new LspDocumentLinkFeature(this._connection)); - store.add(new LspFormattingFeature(this._connection)); - store.add(new LspRangeFormattingFeature(this._connection)); - store.add(new LspOnTypeFormattingFeature(this._connection)); - store.add(new LspFoldingRangeFeature(this._connection)); - store.add(new LspSelectionRangeFeature(this._connection)); - store.add(new LspInlayHintsFeature(this._connection)); - store.add(new LspSemanticTokensFeature(this._connection)); - store.add(new LspDiagnosticsFeature(this._connection)); + store.add(new LspCompletionFeature(this._connection)); + store.add(new LspHoverFeature(this._connection)); + store.add(new LspSignatureHelpFeature(this._connection)); + store.add(new LspDefinitionFeature(this._connection)); + store.add(new LspDeclarationFeature(this._connection)); + store.add(new LspTypeDefinitionFeature(this._connection)); + store.add(new LspImplementationFeature(this._connection)); + store.add(new LspReferencesFeature(this._connection)); + store.add(new LspDocumentHighlightFeature(this._connection)); + store.add(new LspDocumentSymbolFeature(this._connection)); + store.add(new LspRenameFeature(this._connection)); + store.add(new LspCodeActionFeature(this._connection)); + store.add(new LspCodeLensFeature(this._connection)); + store.add(new LspDocumentLinkFeature(this._connection)); + store.add(new LspFormattingFeature(this._connection)); + store.add(new LspRangeFormattingFeature(this._connection)); + store.add(new LspOnTypeFormattingFeature(this._connection)); + store.add(new LspFoldingRangeFeature(this._connection)); + store.add(new LspSelectionRangeFeature(this._connection)); + store.add(new LspInlayHintsFeature(this._connection)); + store.add(new LspSemanticTokensFeature(this._connection)); + store.add(new LspDiagnosticsFeature(this._connection)); - return store; - } + return store; + } } diff --git a/monaco-lsp-client/src/adapters/LspConnection.ts b/monaco-lsp-client/src/adapters/LspConnection.ts index b9f619a204..f94905cec7 100644 --- a/monaco-lsp-client/src/adapters/LspConnection.ts +++ b/monaco-lsp-client/src/adapters/LspConnection.ts @@ -1,5 +1,6 @@ import { TypedChannel } from '@hediet/json-rpc'; import { api } from '../../src/types'; +import { DiagnosticsCache } from './DiagnosticsCache'; import { ITextModelBridge } from './ITextModelBridge'; import { LspCapabilitiesRegistry } from './LspCapabilitiesRegistry'; @@ -9,5 +10,6 @@ export class LspConnection { public readonly bridge: ITextModelBridge, public readonly capabilities: LspCapabilitiesRegistry, public readonly connection: TypedChannel, + public readonly diagnosticsCache: DiagnosticsCache, ) { } } diff --git a/monaco-lsp-client/src/adapters/languageFeatures/LspCodeActionFeature.ts b/monaco-lsp-client/src/adapters/languageFeatures/LspCodeActionFeature.ts index aed4e0dd47..13300b0f2b 100644 --- a/monaco-lsp-client/src/adapters/languageFeatures/LspCodeActionFeature.ts +++ b/monaco-lsp-client/src/adapters/languageFeatures/LspCodeActionFeature.ts @@ -1,9 +1,24 @@ import * as monaco from 'monaco-editor-core'; -import { capabilities, CodeActionRegistrationOptions, Command, WorkspaceEdit, CodeAction } from '../../../src/types'; +import { + capabilities, + CodeAction, + CodeActionRegistrationOptions, + Command, + Diagnostic, + DiagnosticTag, + WorkspaceEdit, +} from '../../../src/types'; import { Disposable } from '../../utils'; import { LspConnection } from '../LspConnection'; -import { toMonacoLanguageSelector } from './common'; -import { lspCodeActionKindToMonacoCodeActionKind, toMonacoCodeActionKind, toLspDiagnosticSeverity, toLspCodeActionTriggerKind, toMonacoCommand } from './common'; +import { + lspCodeActionKindToMonacoCodeActionKind, + toLspCodeActionTriggerKind, + toLspDiagnosticSeverity, + toLspDiagnosticTag, + toMonacoCodeActionKind, + toMonacoCommand, + toMonacoLanguageSelector, +} from './common'; export class LspCodeActionFeature extends Disposable { constructor( @@ -73,16 +88,15 @@ class LspCodeActionProvider implements monaco.languages.CodeActionProvider { token: monaco.CancellationToken ): Promise { const translated = this._client.bridge.translate(model, range.getStartPosition()); + const cachedDiagnostics = this._client.diagnosticsCache.get(model.uri.toString()); const result = await this._client.server.textDocumentCodeAction({ textDocument: translated.textDocument, range: this._client.bridge.translateRange(model, range), context: { - diagnostics: context.markers.map(marker => ({ - range: this._client.bridge.translateRange(model, monaco.Range.lift(marker)), - message: marker.message, - severity: toLspDiagnosticSeverity(marker.severity), - })), + diagnostics: context.markers.map(marker => + toLspDiagnosticForCodeAction(marker, model, this._client, cachedDiagnostics), + ), triggerKind: toLspCodeActionTriggerKind(context.trigger), }, }); @@ -123,6 +137,52 @@ class LspCodeActionProvider implements monaco.languages.CodeActionProvider { } } +/** + * Build the `Diagnostic` we send to the server as part of + * `textDocument/codeAction#context.diagnostics`. + * + * Servers (notably `ruff server`) require fields that don't survive the + * round-trip through Monaco's `IMarker` — specifically `data`, which the LSP + * spec mandates be round-tripped. We look up the original `Diagnostic` in + * `DiagnosticsCache` by URI + range + message and forward it verbatim. For + * markers that didn't come from this LSP session (e.g. set under a different + * owner by another extension), we synthesize a Diagnostic that still passes + * through `code` / `source` / `tags` / `relatedInformation` from `IMarker`. + */ +function toLspDiagnosticForCodeAction( + marker: monaco.editor.IMarkerData, + model: monaco.editor.ITextModel, + client: LspConnection, + cached: readonly Diagnostic[] | undefined, +): Diagnostic { + const lspRange = client.bridge.translateRange(model, monaco.Range.lift(marker)); + const original = cached?.find(d => + d.range.start.line === lspRange.start.line && + d.range.start.character === lspRange.start.character && + d.range.end.line === lspRange.end.line && + d.range.end.character === lspRange.end.character && + d.message === marker.message + ); + if (original) { + return original; + } + const markerCode = marker.code; + const code = + markerCode === undefined + ? undefined + : typeof markerCode === 'string' + ? markerCode + : markerCode.value; + return { + range: lspRange, + message: marker.message, + severity: toLspDiagnosticSeverity(marker.severity), + code, + source: marker.source, + tags: marker.tags?.map(tag => toLspDiagnosticTag(tag)).filter((tag): tag is DiagnosticTag => tag !== undefined), + }; +} + function toMonacoWorkspaceEdit( edit: WorkspaceEdit, client: LspConnection diff --git a/monaco-lsp-client/src/adapters/languageFeatures/LspDiagnosticsFeature.ts b/monaco-lsp-client/src/adapters/languageFeatures/LspDiagnosticsFeature.ts index 2ba7bdeaae..5d6a32ddad 100644 --- a/monaco-lsp-client/src/adapters/languageFeatures/LspDiagnosticsFeature.ts +++ b/monaco-lsp-client/src/adapters/languageFeatures/LspDiagnosticsFeature.ts @@ -37,6 +37,10 @@ export class LspDiagnosticsFeature extends Disposable { (params) => this._handlePublishDiagnostics(params) )); + this._register(monaco.editor.onWillDisposeModel(model => { + this._connection.diagnosticsCache.delete(model.uri.toString()); + })); + this._register(this._connection.capabilities.registerCapabilityHandler( capabilities.textDocumentDiagnostic, true, @@ -91,6 +95,7 @@ export class LspDiagnosticsFeature extends Disposable { return; } + this._connection.diagnosticsCache.set(model.uri.toString(), params.diagnostics); const markers = params.diagnostics.map(diagnostic => toDiagnosticMarker(diagnostic) ); @@ -163,6 +168,7 @@ class ModelDiagnosticProvider extends Disposable { // Full diagnostic report this._previousResultId = report.resultId; + this._connection.diagnosticsCache.set(this._model.uri.toString(), report.items); const markers = report.items.map(diagnostic => toDiagnosticMarker(diagnostic)); monaco.editor.setModelMarkers(this._model, this._markerOwner, markers); @@ -188,6 +194,7 @@ class ModelDiagnosticProvider extends Disposable { } if (report.kind === 'full') { + this._connection.diagnosticsCache.set(model.uri.toString(), report.items); const markers = report.items.map((diagnostic: Diagnostic) => toDiagnosticMarker(diagnostic)); monaco.editor.setModelMarkers(model, this._markerOwner, markers); } diff --git a/monaco-lsp-client/src/adapters/languageFeatures/common.ts b/monaco-lsp-client/src/adapters/languageFeatures/common.ts index 4d648e7d30..97f11fb36a 100644 --- a/monaco-lsp-client/src/adapters/languageFeatures/common.ts +++ b/monaco-lsp-client/src/adapters/languageFeatures/common.ts @@ -263,6 +263,15 @@ export function toMonacoDiagnosticTag(tag: DiagnosticTag): monaco.MarkerTag | un return lspDiagnosticTagToMonacoMarkerTag.get(tag); } +export const monacoMarkerTagToLspDiagnosticTag = new Map([ + [monaco.MarkerTag.Unnecessary, DiagnosticTag.Unnecessary], + [monaco.MarkerTag.Deprecated, DiagnosticTag.Deprecated], +]); + +export function toLspDiagnosticTag(tag: monaco.MarkerTag): DiagnosticTag | undefined { + return monacoMarkerTagToLspDiagnosticTag.get(tag); +} + // ============================================================================ // Signature Help Trigger Kind // ============================================================================