The Language Server Protocol defines language capabilities such as completion, go-to-definition, and diagnostics as a protocol between the editor and the language service. It reduces the repetitive work of different editors adapting to each language, but it does not require the language service to adopt a specific compiler, thread model, or database.
A language server can reuse a compiler frontend or maintain a dedicated analysis engine tailored for interactive use. It deals with source code that is constantly changing and often incomplete, requiring it to balance response speed, result completeness, and version correctness. IDE language features can also be implemented internally within the editor; LSP is not the only source.
The Protocol Defines Capabilities Before Exchanging Requests and Notifications
LSP uses a JSON-RPC message model. Requests have an ID and correspond to a result or an error; notifications have no corresponding response. During the initialization phase, both parties exchange capabilities and then use features according to the negotiated results. For example, textDocument/publishDiagnostics is a notification sent by the server and should not be listed as a standard query initiated by the editor.
Document synchronization typically includes notifications such as didOpen, didChange, and didClose. Whether to use the full text or incremental edits is determined by the capability agreement. For open documents, the server must analyze the unsaved content synchronized by the editor, rather than re-reading the old file from disk every time. LSP 3.17 Specification
Understanding Analysis Dependencies via a Field Completion
When a user types account., the expression is incomplete. The analysis engine can retain the receiver account, the dot, and the missing member node, continue parsing the binding and type of account, and then enumerate candidate members. Candidates may also be constrained by visibility, generic substitution, traits, extension methods, and expected types.
Therefore, completion is not just "listing all names in the symbol table." The syntactic position determines which type of candidate is expected, name resolution determines the specific declaration, type analysis determines which members are available, and sorting and auto-imports improve the interactive experience.
Compilers can also perform error recovery rather than stopping at the first incomplete expression. The difference lies mainly in the goal: interactive systems more frequently require providing locally useful results even with erroneous source code.
Common Capabilities Quick Reference
Message or Request
Core Work
Boundaries Not to Ignore
completion
Context, visible symbols, members, and sorting
Incomplete syntax, auto-imports, and replacement ranges
definition
Resolve specific definition from usage location
Overloads, macro expansions, and generated code locations
references
Query references by symbol identity
Same name different symbols, cross-project, and index completeness
hover
Type, documentation, and related info
Type instantiation, error recovery state
signatureHelp
Candidate signatures and parameter positions for current call
Nested calls and overloads
rename
Generate WorkspaceEdit for associated locations
Name capture, conflicts, file versions, and writability
publishDiagnostics
Server pushes diagnostics
Eviction of old version results; pull diagnostics interface also exists
semanticTokens
Return categorized text ranges
Position encoding, capabilities, and legend
Rename is not a global string replacement. Variables with the same name in different scopes are not the same symbol; names in macro expansions may not have directly modifiable source locations. After the server generates a set of edits, the client still needs to apply them according to the protocol and version state.
Incremental Computation: Caching Query Dependencies, Not Just Files
Consider the query chain: "file text → syntax tree → declaration signature → function body type → diagnostics." Modifying a local expression within a function body may invalidate that function's type result without affecting the exported signature; modifying a public type may affect multiple dependent files.
An incremental engine records dependencies and input versions, recalculating only affected queries, and can prevent invalidation from propagating further if the result hasn't changed. "Recalculating only the changed file" is too narrow, while "recalculating the entire project on any change" is too broad. Project configuration, compilation options, dependency versions, and generated files are also inputs.
The architecture of rust-analyzer separates syntax, semantics, and incremental queries, reflecting this organization for interactive analysis; this does not mean all results must be persisted to disk indexes. rust-analyzer Architecture
Project Indexing: Connecting Declarations and Usages via Symbol Identity
Project-level indexes help find symbols, references, and relationships in files that are not currently open. It can combine dynamic results from currently open files, background-built indexes, and optional external indexes. Caching or disk persistence are performance measures, not mandatory requirements of LSP.
clangd combines indexes from different sources, balancing the current editing state with information about the entire project. clangd Indexing Design
Indexes must interpret source code according to the actual compilation context. For C/C++, the same file may represent different programs under different macro definitions or include paths; using only file modification time as a cache key may reuse incorrect results. When indexing is not yet complete, it should be clear which queries can return partial results and which operations need to wait for more information.
Position Information: Characters Do Not Always Equal Bytes
LSP positions are typically represented by zero-based line numbers and line offsets. 3.17 supports negotiating position encoding; if no other encoding is negotiated, the UTF-16 compatible default is retained. The number of UTF-8 bytes, Unicode code points, and UTF-16 code units may differ.
For example, a non-BMP character occupies two code units in UTF-16 but may occupy four bytes in UTF-8. If the server directly treats byte offsets as LSP characters, completion replacements or diagnostic ranges near Chinese characters and emojis will be offset. The order of applying edits, newline representation, and range endpoints should also comply with the protocol. See the LSP Meta Model for specific data structures.
Sequential Edits Use the Intermediate Text
Suppose version 7 contains the single line a😀b, with UTF-16 position encoding. The a occupies one unit and the emoji two, so b starts at offset 3, although its Unicode code-point index is 2. Inserting X at offset 1 produces aX😀b, moving b to UTF-16 offset 4. Replacing range [4,5) with B then produces aX😀B.
Changes within one didChange notification are also applied in array order: the second range refers to the intermediate text, not the original version 7. The message's version denotes the state after all changes. See the document synchronization definition.
This conversion model handles one line of valid Unicode. Positions beyond line length clamp to the end. It rejects positions inside a surrogate pair to avoid splitting the emoji; that is this model's policy, not a claim that every implementation must repair such positions identically.
defindex16(text,offset):ifoffset<0:raiseValueError("negative position")units=0forindex,charinenumerate(text):ifunits==offset:returnindexunits+=len(char.encode("utf-16-le"))//2ifunits>offset:raiseValueError("inside surrogate pair")returnlen(text)# At or beyond the end of the line
defapply(text,changes):forstart,end,replacementinchanges:ifstart>end:raiseValueError("reversed range")lo, hi=index16(text,start), index16(text,end)text=text[:lo]+replacement+text[hi:]returntextassertindex16("a😀b",3)==2assertindex16("a😀b",99)==3assertapply("a😀b",[(1,1,"X"),(4,5,"B")])=="aX😀B"assertapply("a😀b",[(1,3,"")])=="ab"forchangesin[[(2,3,"")],[(3,1,"")],[(-1,0,"")]]:try:apply("a😀b",changes)exceptValueError:passelse:raiseAssertionError("invalid edit accepted")
A complete implementation also needs line indexing, CRLF handling, multiline replacements and whole-document changes. After every edit, text and line indexes must agree; retaining old line starts while changing text corrupts subsequent conversions. See the Position definition.
Why does one cursor have three different offsets?
Three counters describe one character boundary, not three cursors. Crossing the emoji adds four UTF-8 bytes, two UTF-16 units and one code point. Inserting X moves b, so the next replacement needs the new UTF-16 range. Buttons select complete edit snapshots; the boundary slider observes without editing.
Preparing the visual
Why does one cursor have three different offsets?
UTF-8 bytes, UTF-16 units and code points differ. Sequential changes use the intermediate text; reusing old ranges can target the wrong place or split a character.
{"id":"lsp-position-units","title":"Why does one cursor have three different offsets?","summary":"UTF-8 bytes, UTF-16 units and code points differ. Sequential changes use the intermediate text; reusing old ranges can target the wrong place or split a character.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"stage\"><button data-mode=\"0\" data-i18n=\"original\"></button><button data-mode=\"1\" data-i18n=\"inserted\"></button><button data-mode=\"2\" data-i18n=\"replaced\"></button></div><div class=\"control\"><label for=\"cursor\" data-i18n=\"cursor\"></label><output for=\"cursor\" id=\"cursor-value\"></output><input id=\"cursor\" type=\"range\" min=\"0\" max=\"3\" value=\"2\" step=\"1\"></div><div id=\"characters\" class=\"track\"></div><div id=\"units\" class=\"table\"></div><div class=\"metrics\"><div><span data-i18n=\"utf8\"></span><strong id=\"utf8\"></strong></div><div><span data-i18n=\"utf16\"></span><strong id=\"utf16\"></strong></div><div><span data-i18n=\"points\"></span><strong id=\"points\"></strong></div></div><p id=\"edit\" class=\"formula\"></p><p class=\"hint\" data-i18n=\"hint\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}.metrics strong{font-size:20px}#units .tr{grid-template-columns:.5fr 1fr 1fr}#units .tr>*{min-height:3.2em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let stage=0;function draw(){const text=['a😀b','aX😀b','aX😀B'][stage],chars=[...text];$('#cursor').max=chars.length;const n=Math.min(+$('#cursor').value,chars.length),prefix=chars.slice(0,n).join('');set('cursor-value',n);cells('characters',Array.from({length:4},(_,i)=>({text:chars[i]||'·',cls:i<n?'on':i<chars.length?'gold':'empty'})));table('units',[['char','UTF-8','UTF-16'],...Array.from({length:4},(_,i)=>{const c=chars[i];return c?[c,new TextEncoder().encode(c).length,c.length]:['·','·','·']})]);set('utf8',new TextEncoder().encode(prefix).length);set('utf16',prefix.length);set('points',n);set('edit',stage===0?'v7: a😀b; b starts at UTF-16 3':stage===1?'[1,1) ← X → aX😀b; b starts at 4':'[4,5) ← B → aX😀B');$('#characters').dataset.text=text;pressed(String(stage));}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{stage=+b.dataset.mode;draw()});$('#cursor').oninput=draw;draw();","audio":false,"strings":{"cursor":"Character boundary","heading":"Why does one cursor have three different offsets?","hint":"Insert at [1,1); then replace [4,5) in the intermediate text. Boundary selection never splits the emoji.","inserted":"After inserting X","intro":"For a😀b, select a character boundary. Apply insertion X then replacement B and inspect which text the second range belongs to.","original":"Original v7","points":"Unicode / code point","replaced":"After replacing b","reset":"Reset","scope":"One valid Unicode line, UTF-16 LSP positions. The model rejects surrogate splitting; excludes CRLF, multiline edits and complete endpoint policy.","scopeLabel":"Model scope and assumptions","stage":"Text snapshot","utf16":"UTF-16 / unit","utf8":"UTF-8 / byte"}}
Semantic Highlighting vs. Syntax Highlighting
Regular expressions can identify local lexical patterns; Tree-sitter's highlights.scm primarily queries syntax tree nodes, rather than treating the entire file as a pure regex match. It can leverage syntactic structure but usually does not undertake complete cross-file type parsing. Tree-sitter Highlighting Documentation
LSP semantic tokens allow the server to return richer categories and modifiers, such as whether an identifier is a type, a parameter, or a read-only variable. It does not mandate "annotating every token in the source code," nor does it prohibit editors from retaining syntax highlighting as a fast base layer.
Scheduling: Fast Response and Correct Versions Are Equally Important
Interactive queries such as completion and hover should generally take precedence over full-project indexing. Background analysis can merge nearby edits, reuse snapshots, and cancel outdated tasks after input changes; however, it must not allow old diagnostics to overwrite new results when they arrive late.
$/cancelRequest expresses the cancellation of a specific protocol request, not a unified stop mechanism for arbitrary internal background tasks. Cancellation may race with completion; the server still needs to properly terminate the request; internal analysis also requires its own cooperative cancellation and resource management.
There are trade-offs between debounce waiting and query latency. Some fast local analyses can be executed on every edit; expensive background diagnostics are better suited for merging. There is no conclusion that "parsing on every keypress will definitely cause lag" or that "a unified 50ms debounce guarantees all requests stay below 100ms."
Why Cancellation Still Requires Result Validation
Request 81 analyzes v7; the document then becomes v8 and request 82 returns first. Even if cancellation of 81 has been requested, its computation may already have finished or its response may already be in flight. Cancellation does not promise that no response for that ID will arrive; the request still needs a terminating response. See request cancellation.
An internal task can retain (URI, document version, project configuration generation). The following conservative policy accepts a result only while its full snapshot key still matches. These fields are internal scheduling context, not a claim that LSP completion requests directly carry all of them. Configuration changes can invalidate a result even without a text edit.
current=("file:///demo.lang",8,3)pending={81:("file:///demo.lang",7,3),82:current}shown=[]deffinish(request_id,result):snapshot=pending.pop(request_id)# Complete bookkeeping for this request first
ifsnapshot==current:shown.append(result)finish(82,"new completion")finish(81,"old completion")assertshown==["new completion"]andnotpendingpending[83]=currentcurrent=("file:///demo.lang",8,4)# Text unchanged; compilation configuration changed
finish(83,"stale configuration")assertshown==["new completion"]
This does not require every edit to discard every result. A more precise engine can prove that dependencies did not change or map old positions to a new version. A larger request ID alone proves neither: IDs associate requests with responses, rather than ordering document versions.
Result
Consequence of staleness
Relevant state
Completion
A replacement range overwrites newer input
Request context, cursor and text version
Diagnostics
A fixed error reappears
Diagnostic version or scheduling policy when unversioned
Rename edits
A changed file is edited at the wrong location
Each document's version, conflicts and edit capabilities
Background index
Symbols connect to another compilation context
Project configuration, dependencies and index generation
Finishing a reference search does not prove rename targets have stayed unchanged. Where supported, a versioned TextDocumentEdit records the basis for an edit and the client handles it according to versions and capabilities. Cross-file changes must also account for failure-handling capabilities; clients do not all provide identical atomic application semantics.
Does a larger request ID imply a fresher result?
The second task intentionally has a smaller request ID, preventing numeric order from masquerading as version order. Tasks retain launch-time text/config versions and validate against the current snapshot at delivery; stale results cannot replace accepted fresh results. Cancellation is intent; late delivery models an already-completed response race. Config changes also invalidate unchanged text results.
Preparing the visual
Does a larger request ID imply a fresher result?
Request IDs pair messages, not versions. Text and project configuration determine applicability; cancellation can race with completion, so late results still need validation.
{"id":"analysis-snapshot-validity","title":"Does a larger request ID imply a fresher result?","summary":"Request IDs pair messages, not versions. Text and project configuration determine applicability; cancellation can race with completion, so late results still need validation.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"start\" data-i18n=\"start\"></button><button id=\"edit\" data-i18n=\"edit\"></button><button id=\"config\" data-i18n=\"config\"></button><button id=\"deliverA\" data-i18n=\"deliverA\"></button><button id=\"deliverB\" data-i18n=\"deliverB\"></button><button id=\"cancel\" data-i18n=\"cancel\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"metrics\"><div><span data-i18n=\"current\"></span><strong id=\"current\"></strong></div><div><span data-i18n=\"accepted\"></span><strong id=\"accepted\"></strong></div></div><div id=\"tasks\" class=\"table\"></div><p id=\"history\" class=\"log\" role=\"status\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.metrics strong{font-size:17px;min-height:3.2em}#tasks .tr{grid-template-columns:.6fr 1fr 1fr}#tasks .tr>*{min-height:4.8em}#history{min-height:5.4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let version=7,config=1,tasks=[],accepted=null,history='∅';function draw(){set('current','v'+version+' / c'+config);set('accepted',accepted?'#'+accepted.id+' v'+accepted.v+' c'+accepted.c:'∅');table('tasks',['A','B'].map((n,i)=>{const task=tasks[i];return[n,task?'#'+task.id+' v'+task.v+' c'+task.c:'∅',task?(task.done?'delivered':task.cancelled?'cancel requested':'pending'):'—']}));set('history',history);$('#start').disabled=tasks.length===2;$('#deliverA').disabled=!tasks[0]||tasks[0].done;$('#deliverB').disabled=!tasks[1]||tasks[1].done;$('#cancel').disabled=!tasks[0]||tasks[0].done||tasks[0].cancelled;$('#accepted').dataset.id=accepted?.id||'';}$('#start').onclick=()=>{tasks.push({id:tasks.length?12:81,v:version,c:config,done:false,cancelled:false});draw()};function invalidate(){accepted=null;draw()}$('#edit').onclick=()=>{version++;invalidate()};$('#config').onclick=()=>{config++;invalidate()};for(const [i,id]of ['deliverA','deliverB'].entries())$('#'+id).onclick=()=>{const task=tasks[i];task.done=true;const valid=task.v===version&&task.c===config;if(valid)accepted=task;history='#'+task.id+': (v'+task.v+',c'+task.c+') '+(valid?'=':'≠')+' (v'+version+',c'+config+')\\n'+t(valid?'valid':'stale');draw()};$('#cancel').onclick=()=>{tasks[0].cancelled=true;draw()};$('#reset').onclick=()=>{version=7;config=1;tasks=[];accepted=null;history='∅';draw()};draw();","audio":false,"strings":{"accepted":"Currently accepted result","cancel":"Request cancel A","config":"Change project config","current":"Current snapshot key","deliverA":"Deliver task A","deliverB":"Deliver task B","edit":"Edit text version","heading":"Does a larger request ID imply a fresher result?","history":"Latest delivery check","intro":"Start two analyses, edit text or configuration, then deliver results in any order. Cancellation is recorded; snapshot validation still decides acceptance.","reset":"Reset","scope":"One URI, up to two undelivered tasks. Conservative equality of (version,config), not all protocol messages or fine-grained reuse. Post-cancel delivery models a completed-response race.","scopeLabel":"Model scope and assumptions","stale":"Snapshot changed; discard stale result.","start":"Start analysis","valid":"Snapshot matches; accept result."}}
Learning Path for Minimal Implementation
First, implement initialization, document synchronization, and position conversion, ensuring the server holds the correct text; then provide local syntax diagnostics and simple hover; next, introduce name resolution, project context, and incremental queries; finally, implement cross-file references, renaming, and more complex completions. Each step should check error-prone source code and continuous edits, not just one-time complete files.