
JetBrains' Java LSP in VS Code and Cursor: Separating the Reported 84% vs 31% Claim from Real Workflow Gains
This post separates JetBrains' reported 84% vs 31% Java LSP claim from what you can actually measure in VS Code and Cursor. I read the headline three times and still couldn't tell you 84% of what. Completions accepted? Diagnostics that were right? Index finished before the coffee did? There's no unit on the number, no benchmark, no server build attached — which makes it a marketing artifact, not a measurement.
None of that proves the underlying story is fake. A JetBrains Java LSP showing up in rival editors is a plausible direction for the ecosystem. My objection is that the percentage is doing work a methodology should be doing. So this post does two jobs: it separates the reported claim from anything you can actually verify, and it lays out the harness I use to compare Java language servers on a real repo. The only comparison that matters is the one you run against your own multi-module build.
What the Reported JetBrains Java LSP 84% vs 31% Claim Actually Says
The Source Trail: One Aggregator Item, No Primary JetBrains Announcement
All I have is a single Google News RSS entry: headline JetBrains Java LSP Hits VS Code, Cursor: 84% vs 31% [2026], publisher tech-insider.org, dated 2026-10-18, and a snippet that repeats the headline verbatim. No JetBrains announcement, no changelog entry, no benchmark repository, no named corpus, no editor version. The link is a Google News redirect wrapper, not a document.
So treat "84% vs 31%" as reported. I couldn't find a primary source that defines the metric. If JetBrains publishes a technical post with a benchmark behind it, the status of the claim changes; until then it's a number in a headline.
Why an Unlabeled 84% vs 31% Percentage Is Unfalsifiable
Nothing is wrong with 84 and 31 specifically. The problem is that at least three very different measurements fit those two numbers equally well, and each implies a different chunk of engineering work:
| Candidate metric | How it would be measured | What a bad score means |
|---|---|---|
| Completion acceptance rate | Client telemetry: offered items vs accepted items | Ranking or ranking-context is weak |
| Diagnostic accuracy | Precision/recall against a labeled error corpus | False positives erode trust in squiggles |
| Index / import wall-clock | Timed cold start on a fixed repo | You lose minutes every branch switch |
A 53-point gap on completion acceptance is a UX finding. The same gap on cold import is an architecture finding. Same headline, different universe.
What Would Make the JetBrains Java LSP Claim Checkable
Any one of these four would give me a fair basis:
- A named benchmark and a public corpus — repo, commit SHA, JDK version, build tool version.
- The exact editor and extension versions plus the language server build. JDT LS ships as a versioned jar; a JetBrains server would carry its own build id.
- The metric definition — numerator, denominator, and what counts as a "completion" or an "error".
- Sample size and variance, not two rounded integers.
Without them, the honest reading is: someone measured something, and the direction may be right.
How a Java Language Server Actually Reaches VS Code and Cursor
Three Layers: Editor, Extension Client, and JVM Language Server
Java support in VS Code isn't one thing. It's an editor, an extension client, and a JVM-side language server talking LSP. Right now the common path is Red Hat's vscode-java driving eclipse.jdt.ls.
That has an awkward consequence: upgrading the editor does not upgrade the server. Bump VS Code, switch to Cursor, and the JDT LS jar sits exactly where it was. A comparison that mixes editor versions with server versions is measuring two variables and crediting one.
Pin the extension and the language server build together in your repo notes. "Latest VS Code" plus "whatever server shipped in the extension bundle that day" is not a reproducible configuration, and it is how teams end up arguing about performance they cannot re-measure.
The Eclipse JDT LS Bridge Problem: LSP Is a Narrow Contract
LSP is deliberately narrow: document sync, diagnostics, completion, hover, a handful of refactors. It says nothing about IntelliJ-level structural search, framework-aware wiring, or the deep inspections that make an IDE feel smart.
Servers bridge the gap with escape hatches — workspace/executeCommand, codeAction/resolve, custom notifications. So the ceiling on Java tooling in VS Code is set by what the server chooses to expose, not by how good the editor is. An LSP server is not "the IDE ported." It's a subset with a network-shaped contract bolted on. Ship the best Java server in the world and the reachable feature surface is still bounded by the protocol plus the vendor's private commands.
Why Cursor's AI Edit Loop Stress-Tests Java Language Servers
Cursor changes the workload, not just the client. An AI edit loop rewrites several files in a burst, which turns steady-state editing into a reindex stress test. Two mechanics decide whether that feels fine or awful:
- Sync mode. If the server advertises
textDocumentSync.change: 1(Full), every keystroke resends the whole file.2(Incremental) sends ranges. On a 3,000-line class that's the difference between a responsive editor and a laggy one. - Diagnostic coalescing. Servers debounce
textDocument/publishDiagnostics. After a 12-file agent rewrite, the squiggles you see are stale by design until the queue drains.
Which is why I care more about queue drain time than about whether one completion was the right one.
A Reproducible Comparison You Can Run on Your Own Repo
Instrument the Language Server Side, Not the Editor UI
Editor stopwatches lie. Put a proxy between client and server and timestamp the actual protocol traffic. It spawns the server, forwards frames both ways, and writes JSONL to stderr so stdout stays clean.
import { spawn } from "node:child_process";
// usage: node lsp-tap.mjs <server-cmd> [...args] 2> trace.jsonl
const [cmd, ...args] = process.argv.slice(2);
const server = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
const t0 = performance.now();
const log = (o) =>
process.stderr.write(JSON.stringify({ t: +(performance.now() - t0).toFixed(1), ...o }) + "\n");
function readFrames(stream, onMessage) {
let buf = Buffer.alloc(0);
stream.on("data", (chunk) => {
buf = Buffer.concat([buf, chunk]);
for (;;) {
const headEnd = buf.indexOf("\r\n\r\n");
if (headEnd === -1) return;
const head = buf.subarray(0, headEnd).toString("ascii");
const len = Number(/content-length:\s*(\d+)/i.exec(head)?.[1]);
const start = headEnd + 4;
if (!Number.isFinite(len) || buf.length < start + len) return;
const body = buf.subarray(start, start + len).toString("utf8");
buf = buf.subarray(start + len);
try { onMessage(JSON.parse(body)); } catch { log({ bad: body.slice(0, 80) }); }
}
});
}
const write = (stream, msg) => {
const body = JSON.stringify(msg);
stream.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
};
const open = new Map(); // request id -> { method, start }
const prog = new Map(); // progress token -> start
const saveAt = new Map(); // uri -> timestamp of didSave
function observe(msg) {
if (msg.method && msg.id !== undefined) open.set(msg.id, { method: msg.method, start: performance.now() });
if (msg.method === "textDocument/didSave") saveAt.set(msg.params.textDocument.uri, performance.now());
if (msg.method === "textDocument/publishDiagnostics") {
const at = saveAt.get(msg.params.uri);
log({ ev: "diagnostics", uri: msg.params.uri, ms: at ? +(performance.now() - at).toFixed(1) : null,
n: msg.params.diagnostics.length });
}
if (msg.method === "$/progress") {
const v = msg.params.value, token = String(v.token);
if (v.kind === "begin") prog.set(token, performance.now());
if (v.kind === "end") {
const s = prog.get(token); prog.delete(token);
log({ ev: "progress-end", token, title: v.title ?? null,
ms: s ? +(performance.now() - s).toFixed(1) : null });
}
}
if (msg.id !== undefined && !msg.method) {
const req = open.get(msg.id);
if (req) { open.delete(msg.id);
log({ ev: "response", method: req.method, ms: +(performance.now() - req.start).toFixed(1) }); }
}
}
readFrames(process.stdin, (m) => { observe(m); write(server.stdin, m); });
readFrames(server.stdout, (m) => { observe(m); write(process.stdout, m); });Four Java LSP Measurements Worth Capturing
| Metric | Where it lives in the trace | Why it matters |
|---|---|---|
| Cold project import | progress-end for the import token minus the initialize response | the first ten minutes after opening a repo |
| Completion p95 | response for textDocument/completion | keystroke feel, not average feel |
| Diagnostic lag after save | didSave → first publishDiagnostics for the same URI | how long a red squiggle is trustworthy |
| Full reindex after branch switch | burst of workspace/didChangeWatchedFiles → next progress-end | the cost you pay every context switch |
Turn the LSP Trace into a Text Results Table
Aggregate the JSONL instead of eyeballing it:
node lsp-tap.mjs java -jar /path/to/language-server.jar >/dev/null 2>trace.jsonl
node -e '
const rows = require("fs").readFileSync("trace.jsonl","utf8").trim().split("\n")
.map(JSON.parse).filter(r => r.ev === "response" && r.method === "textDocument/completion")
.map(r => r.ms).sort((a,b) => a-b);
const q = p => rows[Math.min(rows.length - 1, Math.floor(p * rows.length))];
console.log({ n: rows.length, p50: q(0.5), p95: q(0.95) });'
One row per configuration, and include repo size — the numbers mean nothing without it:
| Editor + version | Server build | Repo (modules / LOC) | Import (s) | Completion p50 / p95 (ms) | Diag lag p95 (ms) | Reindex (s) |
|---|---|---|---|---|---|---|
VS Code + vscode-java (pinned) | JDT LS <build> | e.g. 14 / 180k | your run | your run | your run | your run |
| Cursor + same extension | same build | same repo | your run | your run | your run | your run |
That table is a template on purpose. I haven't run JetBrains' Java server — as far as I can establish, no public build is tied to the claim — so I'm not pasting numbers I didn't collect. The harness, though, is real and runs against any stdio LSP server today.
Where JetBrains Java LSP Workflow Gains Show Up — and Where They Don't
Large Multi-Module Gradle and Maven Builds
This is where index quality and incremental compilation earn their keep. A 15-module Gradle build with annotation processors is a different workload from a single app module: the server has to resolve inter-module dependencies, watch build outputs, and re-walk the classpath every time a subproject recompiles. A better index shows up as fewer "cannot resolve symbol" flashes and a shorter wait after ./gradlew build.
Small Single-Module Services Likely See the Smallest Delta
One module, one JDK, thirty files: current JDT LS is already fast enough that the perceived difference between servers is close to zero. I'd rather say that plainly than repeat a headline number, because the smaller the repo, the less any index improvement has room to show up.
My Position: Latency Beats Completion Accuracy
For daily Java work, index and diagnostic latency matter far more than completion accuracy, and neither 84% nor 31% maps to anything a developer feels. A wrong completion costs one keystroke to reject. A stale diagnostic costs a debugging detour. A three-minute reindex costs the thought you were holding. If a vendor wants to convince me, publish progress-end timings on a named corpus — not acceptance percentages.
Licensing, Telemetry, and Version Drift Costs Teams Skip
Mixing vendors adds a second update channel and a second bug surface. The extension updates on the marketplace cadence, the server jar updates on the vendor's cadence, and the two drift apart.
Two questions I couldn't answer from the available material: whether a JetBrains-shipped Java LSP would be free-standing or tied to a JetBrains subscription, and what telemetry it would emit. Both are licensing and procurement questions rather than technical ones, and both need answers before a team rollout — especially in orgs that route traffic through a proxy or keep an approved-vendor list. Pin versions in a lockfile-style note and treat a server bump as a change that requires re-running the harness.
Five Checks Before Switching a Java Team to a New LSP
Five gates. Each is a yes/no, and any "no" is a blocker rather than a discussion:
- Build-file resolution on a real multi-module repo. Open it cold and confirm every subproject resolves without manual
java.configuration.updateBuildConfigurationcycles. - Annotation processors and Lombok. Confirm generated sources land on the classpath and Lombok-annotated getters resolve. Historically that needed an agent on the server JVM; verify whether the bundle handles it or you still need
-javaagent. - Refactor correctness on a non-main branch. Rename across module boundaries and diff the result. LSP rename is only as good as the server's understanding of the code.
- Formatter and lint parity. Save the same file in both editors and diff. Whitespace churn in every commit is a real cost.
- Rollback path. Can you pin the old extension and server build and be back to a known state in five minutes? If not, you're not ready to switch.
What I Confirmed vs What I Could Not Test
Confirmed, or directly checkable:
- The three-layer model — editor → extension client → language server over LSP — and that the spec defines which messages a server can expose.
- The measurement approach above:
$/progress,textDocument/publishDiagnostics, and completion round-trips are standard traffic, and the proxy parses real frames. - The source trail: one aggregator item, dated 2026-10-18, with no primary document linked.
Not confirmed / not tested:
- The 84% vs 31% figures. No defined metric that I could find. Quote them as reported.
- Any performance number for a JetBrains Java LSP in VS Code or Cursor. I didn't run it, and I'm not aware of a public build I could run.
- Licensing and telemetry behaviour of a JetBrains-shipped server. Untested, and a procurement question.
- Cursor's extension availability for Java specifically. It pulls from Open VSX rather than the Microsoft marketplace, so availability depends on the publisher — check before assuming parity.
Further Reading
- Language Server Protocol specification (3.17) — the authoritative definition of
$/progress,textDocument/publishDiagnostics, and sync kinds. - eclipse.jdt.ls — the Java language server most VS Code Java setups run today; releases include build versions you can pin.
- vscode-java — the extension client, including the settings that control server JVM args.
Conclusion — Treat It as a Latency Question, Not a Percentage
The useful question isn't "is it 84% or 31%?" It's "how long after I save does the squiggle mean something, and how long after a branch switch can I navigate again?" Both are measurable in an afternoon with a fifty-line proxy, and both are what a Java developer actually feels.
If a JetBrains Java server does land in VS Code and Cursor, my prediction is that it matters a lot on large multi-module builds and barely at all on a single-module service. That's testable, which is more than I can say for the headline.


