JetBrains' Java LSP in VS Code and Cursor: Separating the Reported 84% vs 31% Claim from Real Workflow Gains

JetBrains' Java LSP in VS Code and Cursor: Separating the Reported 84% vs 31% Claim from Real Workflow Gains

pr0h0•
jetbrainsjavalspvscodecursor
AI Usage (83%)

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 metricHow it would be measuredWhat a bad score means
Completion acceptance rateClient telemetry: offered items vs accepted itemsRanking or ranking-context is weak
Diagnostic accuracyPrecision/recall against a labeled error corpusFalse positives erode trust in squiggles
Index / import wall-clockTimed cold start on a fixed repoYou 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:

  1. A named benchmark and a public corpus — repo, commit SHA, JDK version, build tool version.
  2. 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.
  3. The metric definition — numerator, denominator, and what counts as a "completion" or an "error".
  4. 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.

lsp-tap.mjs
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

MetricWhere it lives in the traceWhy it matters
Cold project importprogress-end for the import token minus the initialize responsethe first ten minutes after opening a repo
Completion p95response for textDocument/completionkeystroke feel, not average feel
Diagnostic lag after savedidSave → first publishDiagnostics for the same URIhow long a red squiggle is trustworthy
Full reindex after branch switchburst of workspace/didChangeWatchedFiles → next progress-endthe 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 + versionServer buildRepo (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 / 180kyour runyour runyour runyour run
Cursor + same extensionsame buildsame repoyour runyour runyour runyour 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:

  1. Build-file resolution on a real multi-module repo. Open it cold and confirm every subproject resolves without manual java.configuration.updateBuildConfiguration cycles.
  2. 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.
  3. 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.
  4. Formatter and lint parity. Save the same file in both editors and diff. Whitespace churn in every commit is a real cost.
  5. 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.

Share this post

More posts

Comments