microsoft / microsoft/monaco-editor

[Bug] [LSP] `textDocument/codeAction` request omits diagnostic fields the server needs to produce quickfixes (`data`, `code`, `source`, …)

Open
#5,342 2 comments 0 reactions 4 assignees View on GitHub

@hediet is already working on this.

Since Jun 5, 2026.

Dominant language
JavaScript
Stars
46.8k
Forks
4.1k
Avg merge
17h 58m
Merged PRs (30d)
1

Description

Reproducible in vscode.dev or in VS Code Desktop?
  • Not reproducible in vscode.dev or VS Code Desktop
Reproducible in the monaco editor playground?
Monaco Editor Playground Link
Summary

LspCodeActionProvider.provideCodeActions constructs context.diagnostics for the outgoing textDocument/codeAction request from context.markers, but forwards only range, message, and severity. LSP diagnostic fields that servers commonly key off — code, source, tags, relatedInformation, codeDescription, and especially data (the server-roundtripped opaque payload) — are dropped, even though most are present on IMarker and the data / codeDescription payloads are still available on the original Diagnostic objects this client just received from publishDiagnostics / textDocument/diagnostic.

Effect

For any server that uses Diagnostic.data to carry quickfix metadata (the LSP 3.16+ mechanism for diagnostic-targeted code actions, gated on dataSupport: true — which this client already advertises in LspDiagnosticsFeature.ts:25), the server can't identify the diagnostic in the codeAction request and returns no quickfix.* actions. The Monaco lightbulb only shows source-level actions (source.fixAll.*, source.organizeImports.*), and the user-facing "Remove unused import" / "Add missing return" / etc. quickfixes never appear.

Concrete real-world example: ruff server attaches data to every diagnostic. Without it ruff returns zero quickfix actions, even though the same server emits all of them when a fully-formed diagnostic is round-tripped back.

Reproduction (3-shape probe against ruff)

Initialize → didOpen ("import os\n\nprint('hi')\n" → F401 unused import) → wait for publishDiagnostics. Then send three textDocument/codeAction requests with different context.diagnostics:

Shape context.diagnostics payload Result
A — full Diagnostic as ruff sent it [{ range, message, severity, code: "F401", source: "Ruff", data: {…} }] 4 actions, including 2 quickfix.* ("Remove unused import: 'os'", "Disable for this line")
B — stripped to range/message/severity (what LspCodeActionProvider sends today) [{ range, message, severity }] 2 source-only actions, no quickfixes
C — empty [] 2 source-only actions, no quickfixes

Shape B is what every Monaco user sees today. range + message alone is theoretically enough to route the request, but in practice servers like ruff key off data and silently fall back to source actions.

Root cause

monaco-lsp-client/src/adapters/languageFeatures/LspCodeActionFeature.ts:80-85:

diagnostics: context.markers.map(marker => ({
    range: this._client.bridge.translateRange(model, monaco.Range.lift(marker)),
    message: marker.message,
    severity: toLspDiagnosticSeverity(marker.severity),
})),

code, source, tags, and relatedInformation are recoverable directly from marker — they're already preserved by toDiagnosticMarker in common.ts:372-399 when LSP diagnostics are turned into IMarkers. But data and codeDescription cannot survive the round-trip through IMarker because Monaco's marker model doesn't have fields for them. They're available on the original Diagnostic objects in LspDiagnosticsFeature, but that feature doesn't expose them.

Proposed fix
  1. New DiagnosticsCache (URI → Diagnostic[]) populated by LspDiagnosticsFeature whenever it receives diagnostics (push and pull paths), cleaned on model.onWillDispose.
  2. Expose it via LspConnection so any feature can read it.
  3. LspCodeActionProvider.provideCodeActions looks up each marker's matching Diagnostic (by URI + range + message) and forwards the original object verbatim. Markers without a cache hit (e.g. set by an unrelated owner) fall back to a synthesized Diagnostic that still passes through code, source, tags, and relatedInformation from IMarker — so foreign markers still contribute the fields they have.

Diff in the linked PR.

LSP 3.17 spec reference

Per § Diagnostic:

A data entry field that is preserved between a textDocument/publishDiagnostics notification and textDocument/codeAction request. […] Servers are allowed to attach information to a diagnostic, e.g. for purposes of identifying it later.

The current implementation makes this round-tripping impossible.

Monaco Editor Playground Code

Reproduction Steps

No response

Actual (Problematic) Behavior

No response

Expected Behavior

No response

Additional Context

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.