Language Server Protocol
Your Editor Doesn't Understand Your Code
Developer toolingdeveloper tooling / editors
The myth
"My editor has language support built in. Autocomplete, go-to-definition, rename — that's the editor being smart about my language."
Actually
Your editor is a text widget speaking JSON-RPC over a pipe to a separate process that happens to contain a compiler: tsserver, gopls, rust-analyzer, pyright, clangd. The editor knows nothing about your language. It knows one protocol.
That's the whole trick. M editors × N languages used to mean M×N integrations. LSP made it M+N.
What's actually on the wire
Content-Length-framed JSON over stdio:
Content-Length: 148
{"jsonrpc":"2.0","id":1,"method":"textDocument/definition",
"params":{"textDocument":{"uri":"file:///app/src/user.ts"},
"position":{"line":42,"character":17}}}
Reply: a Location — uri + range. Hover, completion, rename, diagnostics all have this same shape.
Notice what is NOT in that request: your file's text. The server keeps its own in-memory copy, seeded by didOpen and patched by didChange deltas on every keystroke. Your buffer and the server's buffer are two different objects kept in sync by hand.
Three facts that explain 90% of "my LSP is broken" days
1️⃣ Positions are UTF-16 code units — not characters, not bytes.
In "👋 hi", hi starts at character 3: the emoji is 2 UTF-16 units (but 1 character, 4 bytes). A server that counts bytes puts your squiggle in the wrong column. That's why diagnostics drift on lines with emoji or CJK. LSP 3.17 added a negotiable positionEncoding; most servers still speak utf-16.
2️⃣ The server needs your build config, not your files.
gopls wants the module root. clangd wants compile_commands.json. pyright wants the venv. Open one file without its project and you get single-file mode: syntax only, no cross-file jumps. Half of "go-to-definition stopped working" is just a wrong root directory.
3️⃣ Diagnostics are pushed, not pulled.
The server sends publishDiagnostics whenever it decides to. Your red squiggles are a snapshot from another process — possibly pre-save, possibly from a stale generation of the file. They are a hint, not a verdict. CI is the verdict.
Watch it happen
VS Code: settings → "typescript.tsserver.trace": "verbose", then Output panel → TypeScript.
Neovim: :lua vim.lsp.set_log_level("debug") then :LspLog
Type a single character and count the messages that fly by.
Your exercise
Open just a file — not its folder — from a project of yours, then try go-to-definition on an imported symbol. Does it resolve?
Then answer this: which single file's presence tells your language server where your project starts? Delete it (in a scratch copy) and watch your smart editor turn back into a text box.