How Deep Is Your LSP Integration? Three Probes That Tell You
A comparison table of editors would be stale within a quarter. What survives is knowing how to find out what your own setup actually knows.
Why comparing tools directly is a trap
The obvious article here would rank editors and agents by how well they use language server data. It would also be wrong within a quarter — this tooling changes between releases, and a comparison table is stale before it ranks well.
What does not change as fast is the underlying question: what does a given integration actually know, and how would you find out? That question survives version bumps, and answering it for your own setup is more useful than any table.
Four levels of integration
| Level | The agent can | Symptom when this is the level |
|---|---|---|
| None | Read files, run commands | Invents symbols; greps constantly |
| Diagnostics only | See current errors and warnings | Fixes errors well; still invents when exploring |
| Query access | Ask for definitions, references, types | Navigates precisely; rarely guesses |
| Bidirectional | Also apply server-provided edits and code actions | Renames are exact across the project |
Most integrations sit at level one or two. Level three is where behaviour changes noticeably, because "not found" becomes a reliable answer rather than a gap the model fills.
Finding out which level you have
Vendor documentation is often vague on this, and the behaviour is easy to test directly. Three probes, five minutes.
Probe 1 — the nonexistent symbol
Ask about a function that does not exist, with a plausible name for your codebase:
Where is `validate_subscription_tier` defined?
An integration with query access says it cannot find it. One without invents a plausible location, or describes what such a function "would" do. This single probe separates level three from levels one and two more reliably than any documentation.
Probe 2 — the interface implementation
Pick an interface with several implementations and ask which ones exist. Text search cannot answer this — implementations do not mention the interface name in a greppable way in most languages. A complete, correct list means real symbol resolution.
Probe 3 — the fresh error
Introduce a type error in a file the agent has not read, then ask what is currently wrong in the project. An integration with diagnostics access names it immediately. One without has to be told where to look.
Integration depth changes between releases, silently and in both directions. Keep these three probes somewhere and re-run them after a major version bump. It takes five minutes and occasionally explains a change in output quality you would otherwise attribute to the model.
Getting to level three when your tool is at level one
If your agent can run shell commands, you can hand it query access without waiting for the vendor. Most language servers ship a CLI, or have one available.
#!/bin/bash
# lsq — language-server queries as shell commands
case "$1" in
def) pyright --outputjson --verifytypes "$2" | jq -r '...' ;;
refs) rg -n --json "\b$2\b" | jq -r '...' ;; # fallback
diag) mypy --no-error-summary --no-pretty "${2:-src/}" ;;
type) pyright --outputjson "$2" | jq -r '...' ;;
*) echo "usage: lsq {def|refs|diag|type} " ; exit 1 ;;
esac
## Code navigation
- `./lsq def <symbol>` — where it is defined, or "not found"
- `./lsq diag [path]` — current type errors
- Prefer these over grep for symbol questions. "Not found" is
authoritative: the symbol does not exist. Do not assume otherwise.
The final instruction is the one that changes behaviour. Exposing the tool is half the work; telling the agent that a negative result is trustworthy is the other half, and it is the half people skip.
Where integrations commonly fail
| Failure | Looks like | Check |
|---|---|---|
| Server not started | No diagnostics ever appear | Editor's LSP status output |
| Wrong interpreter or SDK | Stdlib symbols unresolved | Which environment the server uses |
| Index incomplete | Correct answers for some files only | Wait for indexing to finish; retry |
| Generated code missing | Holes exactly where the data model lives | Run codegen before starting |
| Monorepo root wrong | Cross-package resolution fails | Server's configured workspace root |
Row four is worth calling out. In repositories where protobuf or ORM stubs are generated during the build, a fresh checkout has no generated code, so the language server has no model of the data layer — which is precisely where most work happens. The agent then guesses about your core types, and the output degrades in a way nobody connects to a missing codegen step.
Diagnostics are the cheapest win
If you get only one thing from an integration, make it diagnostics. The reason is the feedback loop: an agent that sees type errors immediately after an edit converges, and one that has to run a command and parse text output does so more slowly and less reliably.
The measurable difference shows up in a specific behaviour. With diagnostics, an agent making a change to a shared interface sees every resulting error across the project at once and fixes them as a set. Without, it fixes the file in front of it, runs the build, discovers three more errors, fixes those, runs again — several round trips for the same outcome, each consuming context.
This is also why a broken language server degrades agent output so much more than it degrades human output. A person navigating a project with no working LSP is mildly inconvenienced. An agent loses its only source of continuous ground truth and falls back on plausibility.
What to standardise across a team
Mixed tooling is normal and mostly fine. Three things are worth agreeing on regardless of which editor people use:
- The language server configuration lives in the repository, not in personal settings, so everyone's agent has the same ground truth.
- Codegen runs in setup, so nobody works against an incomplete index.
- The query commands are named in the context file, so an agent on any surface reaches for them.
That third point is what makes tool choice largely irrelevant. If every agent reads the same context file, and that file names the same commands, then behaviour converges regardless of which editor produced it — which is a better outcome than standardising on a tool that will be superseded.
Do not compare tools; probe your own setup. Three tests — a nonexistent symbol, an interface's implementations, a fresh type error — tell you what your integration actually knows. If it is shallow, wrap the language server in shell commands yourself, and tell the agent explicitly that "not found" is authoritative.