1- use std :: collections :: HashMap ;
1+ use ahash :: AHashMap ;
22
33use ndc_core:: FunctionRegistry ;
44use ndc_interpreter:: { Interpreter , NativeFunction } ;
@@ -7,28 +7,41 @@ use tokio::sync::RwLock;
77use tower_lsp:: jsonrpc:: Result as JsonRPCResult ;
88use tower_lsp:: lsp_types:: {
99 CompletionItem , CompletionOptions , CompletionParams , CompletionResponse ,
10- DidChangeTextDocumentParams , DidOpenTextDocumentParams , InitializeParams , InitializeResult ,
10+ DidChangeTextDocumentParams , DidCloseTextDocumentParams , DidOpenTextDocumentParams ,
11+ DocumentSymbolParams , DocumentSymbolResponse , GotoDefinitionParams , GotoDefinitionResponse ,
12+ Hover , HoverParams , HoverProviderCapability , InitializeParams , InitializeResult ,
1113 InitializedParams , InlayHint , InlayHintParams , MessageType , OneOf , ServerCapabilities ,
1214 TextDocumentSyncCapability , TextDocumentSyncKind , Url , WorkDoneProgressOptions ,
1315} ;
1416use tower_lsp:: { Client , LanguageServer } ;
1517
1618use crate :: diagnostics;
17- use crate :: features:: { completion, inlay_hints} ;
19+ use crate :: features:: completion:: FunctionInfo ;
20+ use crate :: features:: { completion, definition, hover, inlay_hints, symbols} ;
1821use crate :: state:: DocumentState ;
1922
2023pub struct Backend {
2124 pub client : Client ,
22- documents : RwLock < HashMap < Url , DocumentState > > ,
25+ documents : RwLock < AHashMap < Url , DocumentState > > ,
2326 configure : fn ( & mut FunctionRegistry < Rc < NativeFunction > > ) ,
27+ /// Native-function metadata, snapshotted once at startup. The set of native
28+ /// functions never changes, so completion and hover read this instead of
29+ /// rebuilding an interpreter per request.
30+ functions : Vec < FunctionInfo > ,
2431}
2532
2633impl Backend {
2734 pub fn new ( client : Client , configure : fn ( & mut FunctionRegistry < Rc < NativeFunction > > ) ) -> Self {
35+ let functions = {
36+ let mut interpreter = Interpreter :: capturing ( ) ;
37+ interpreter. configure ( configure) ;
38+ FunctionInfo :: collect ( & interpreter)
39+ } ;
2840 Self {
2941 client,
30- documents : RwLock :: new ( HashMap :: new ( ) ) ,
42+ documents : RwLock :: new ( AHashMap :: new ( ) ) ,
3143 configure,
44+ functions,
3245 }
3346 }
3447
@@ -38,63 +51,88 @@ impl Backend {
3851 interpreter
3952 }
4053
41- /// Update the source text immediately so concurrent requests (e.g. completion
42- /// triggered by `.`) always see the latest document content.
43- async fn update_source ( & self , uri : & Url , text : & str ) {
54+ // Staleness & concurrency model
55+ // ------------------------------
56+ // `didChange` notifications can interleave at await points, so any cached
57+ // state is tagged with the client's monotonic document `version`. Two rules
58+ // keep async edits sound:
59+ // 1. `update_source` runs synchronously under the lock and bumps `version`,
60+ // so completion (triggered by `.`) always sees the latest text.
61+ // 2. `validate` runs analysis off the lock, then commits/publishes only if
62+ // the stored `version` still equals the one it analysed — a slow run
63+ // can't roll the buffer back to older text.
64+ // AST-backed features (hover, go-to-def, symbols, inlay hints) additionally
65+ // gate on `analysis_matches_source`, so they never map stale spans onto
66+ // edited text. The completion caches (`variable_types` by name,
67+ // `expression_types` by end-offset) are intentionally resilient: dot
68+ // completion fires on a buffer that doesn't parse (`x.`), and the data it
69+ // reads sits at the cursor where offsets are stable for the appended `.`.
70+
71+ /// Update the cached source text immediately and bump the document version.
72+ async fn update_source ( & self , uri : & Url , text : & str , version : i32 ) {
4473 let mut docs = self . documents . write ( ) . await ;
45- match docs. get_mut ( uri) {
46- Some ( state) => state. source = text. to_string ( ) ,
47- None => {
48- docs. insert (
49- uri. clone ( ) ,
50- DocumentState {
51- hints : Vec :: new ( ) ,
52- source : text. to_string ( ) ,
53- variable_types : HashMap :: new ( ) ,
54- expression_types : HashMap :: new ( ) ,
55- } ,
56- ) ;
57- }
74+ if let Some ( state) = docs. get_mut ( uri) {
75+ state. source = text. to_string ( ) ;
76+ state. line_index = crate :: util:: LineIndex :: new ( text) ;
77+ state. version = version;
78+ // The AST now predates this edit; `validate` re-sets this to true
79+ // if the new source parses.
80+ state. analysis_matches_source = false ;
81+ } else {
82+ let mut state = DocumentState :: from_source ( text. to_string ( ) ) ;
83+ state. version = version;
84+ docs. insert ( uri. clone ( ) , state) ;
5885 }
5986 }
6087
61- /// Run diagnostics and semantic analysis, updating cached hints and types.
62- /// The source text must already be updated via `update_source` before calling this.
63- async fn validate ( & self , uri : & Url , text : & str ) {
88+ /// Run diagnostics and semantic analysis for `version` of the document, then
89+ /// commit the analysis and publish diagnostics only if no later edit has
90+ /// superseded it. `update_source` must have run for this `version` first.
91+ async fn validate ( & self , uri : & Url , text : & str , version : i32 ) {
6492 let ( mut diagnostics, _ast) = diagnostics:: lex_and_parse ( text) ;
6593
66- // Run full semantic analysis and collect inlay hints + variable types.
67- // The interpreter uses Rc internally (non-Send), so it must be fully
68- // dropped before the next await point.
69- let analysis = {
94+ // Run full semantic analysis. The interpreter uses Rc internally
95+ // (non-Send), so it must be fully dropped before the next await point.
96+ let analysed = {
7097 let mut interpreter = self . make_interpreter ( ) ;
7198 interpreter
7299 . analyse_str ( text)
73100 . ok ( )
74101 . map ( |( expressions, analysis_result) | {
75- // Convert analysis errors to LSP diagnostics.
76102 for err in & analysis_result. errors {
77103 diagnostics. push ( diagnostics:: analysis_error_to_diagnostic ( text, err) ) ;
78104 }
79- inlay_hints :: collect ( & expressions, & analysis_result, text )
105+ ( expressions, analysis_result)
80106 } )
81107 } ;
82108
109+ // Hold the write lock across the commit AND the publish. `did_close`
110+ // also takes this lock to remove state + clear diagnostics, so holding
111+ // it here serializes the two: either close runs first (and we then see
112+ // the document gone and skip), or we publish first and close clears it
113+ // afterwards. Without this, a close could slip between an unlocked
114+ // version check and the publish, leaving stale diagnostics on a closed
115+ // file. The publish is a fire-and-forget notification, so the lock is
116+ // held only briefly.
117+ let mut docs = self . documents . write ( ) . await ;
118+ let Some ( state) = docs. get_mut ( uri) else {
119+ return ;
120+ } ;
121+ // A later edit already moved the buffer on — drop this stale run
122+ // rather than committing old AST or publishing old diagnostics.
123+ if state. version != version {
124+ return ;
125+ }
126+ // On success, commit in place (keeps the current source/line_index).
127+ // On parse failure, keep the last good AST; `analysis_matches_source`
128+ // is already false, so AST-backed features stay disabled.
129+ if let Some ( ( ast, analysis) ) = analysed {
130+ state. set_analysis ( ast, analysis) ;
131+ }
132+
83133 self . client
84- . publish_diagnostics ( uri. clone ( ) , diagnostics, None )
134+ . publish_diagnostics ( uri. clone ( ) , diagnostics, Some ( version ) )
85135 . await ;
86-
87- // Only update document state when analysis succeeds. On failure (e.g.
88- // incomplete syntax while typing `x.`), keep the last good hints and
89- // variable types so inlay hints stay visible and dot-completion works.
90- if let Some ( info) = analysis {
91- let mut docs = self . documents . write ( ) . await ;
92- if let Some ( state) = docs. get_mut ( uri) {
93- state. hints = info. hints ;
94- state. variable_types = info. variable_types ;
95- state. expression_types = info. expression_types ;
96- }
97- }
98136 }
99137}
100138
@@ -116,6 +154,9 @@ impl LanguageServer for Backend {
116154 completion_item : None ,
117155 } ) ,
118156 inlay_hint_provider : Some ( OneOf :: Left ( true ) ) ,
157+ hover_provider : Some ( HoverProviderCapability :: Simple ( true ) ) ,
158+ document_symbol_provider : Some ( OneOf :: Left ( true ) ) ,
159+ definition_provider : Some ( OneOf :: Left ( true ) ) ,
119160 ..Default :: default ( )
120161 } ,
121162 ..Default :: default ( )
@@ -135,46 +176,100 @@ impl LanguageServer for Backend {
135176 async fn did_open ( & self , params : DidOpenTextDocumentParams ) {
136177 let uri = params. text_document . uri ;
137178 let text = params. text_document . text ;
138- self . update_source ( & uri, & text) . await ;
139- self . validate ( & uri, & text) . await ;
179+ let version = params. text_document . version ;
180+ self . update_source ( & uri, & text, version) . await ;
181+ self . validate ( & uri, & text, version) . await ;
140182 }
141183
142184 async fn did_change ( & self , params : DidChangeTextDocumentParams ) {
143185 let uri = params. text_document . uri ;
144- for change in params. content_changes {
145- self . update_source ( & uri, & change. text ) . await ;
146- self . validate ( & uri, & change. text ) . await ;
186+ let version = params. text_document . version ;
187+ // Full-document sync: the last change carries the whole buffer, so only
188+ // the final one matters.
189+ if let Some ( change) = params. content_changes . into_iter ( ) . next_back ( ) {
190+ self . update_source ( & uri, & change. text , version) . await ;
191+ self . validate ( & uri, & change. text , version) . await ;
147192 }
148193 }
149194
195+ async fn did_close ( & self , params : DidCloseTextDocumentParams ) {
196+ let uri = params. text_document . uri ;
197+ // Drop cached state so the map doesn't grow unbounded, and clear the
198+ // document's diagnostics. Hold the lock across the publish so it can't
199+ // interleave with a `validate` publish (see the note in `validate`).
200+ let mut docs = self . documents . write ( ) . await ;
201+ docs. remove ( & uri) ;
202+ self . client . publish_diagnostics ( uri, Vec :: new ( ) , None ) . await ;
203+ }
204+
150205 async fn inlay_hint ( & self , params : InlayHintParams ) -> JsonRPCResult < Option < Vec < InlayHint > > > {
206+ let docs = self . documents . read ( ) . await ;
207+ Ok ( docs. get ( & params. text_document . uri ) . map ( |state| {
208+ if !state. analysis_matches_source {
209+ return Vec :: new ( ) ;
210+ }
211+ inlay_hints:: collect (
212+ & state. ast ,
213+ & state. analysis ,
214+ & state. source ,
215+ & state. line_index ,
216+ )
217+ } ) )
218+ }
219+
220+ async fn hover ( & self , params : HoverParams ) -> JsonRPCResult < Option < Hover > > {
221+ let docs = self . documents . read ( ) . await ;
222+ let uri = & params. text_document_position_params . text_document . uri ;
223+ let position = params. text_document_position_params . position ;
224+ Ok ( docs
225+ . get ( uri)
226+ . filter ( |state| state. analysis_matches_source )
227+ . and_then ( |state| hover:: hover ( state, position, & self . functions ) ) )
228+ }
229+
230+ async fn goto_definition (
231+ & self ,
232+ params : GotoDefinitionParams ,
233+ ) -> JsonRPCResult < Option < GotoDefinitionResponse > > {
234+ let docs = self . documents . read ( ) . await ;
235+ let uri = & params. text_document_position_params . text_document . uri ;
236+ let position = params. text_document_position_params . position ;
237+ Ok ( docs
238+ . get ( uri)
239+ . filter ( |state| state. analysis_matches_source )
240+ . and_then ( |state| {
241+ definition:: goto_definition ( state, position, uri. clone ( ) )
242+ . map ( GotoDefinitionResponse :: Scalar )
243+ } ) )
244+ }
245+
246+ async fn document_symbol (
247+ & self ,
248+ params : DocumentSymbolParams ,
249+ ) -> JsonRPCResult < Option < DocumentSymbolResponse > > {
151250 let docs = self . documents . read ( ) . await ;
152251 Ok ( docs
153252 . get ( & params. text_document . uri )
154- . map ( |state| state. hints . clone ( ) ) )
253+ . filter ( |state| state. analysis_matches_source )
254+ . map ( |state| {
255+ DocumentSymbolResponse :: Nested ( symbols:: document_symbols (
256+ & state. ast ,
257+ & state. source ,
258+ & state. line_index ,
259+ ) )
260+ } ) )
155261 }
156262
157263 async fn completion (
158264 & self ,
159265 params : CompletionParams ,
160266 ) -> JsonRPCResult < Option < CompletionResponse > > {
161- let state = {
162- let docs = self . documents . read ( ) . await ;
163- let uri = & params. text_document_position . text_document . uri ;
164- docs. get ( uri) . map ( |s| {
165- // Clone what completion needs so we can drop the lock.
166- DocumentState {
167- hints : Vec :: new ( ) , // not needed for completion
168- source : s. source . clone ( ) ,
169- variable_types : s. variable_types . clone ( ) ,
170- expression_types : s. expression_types . clone ( ) ,
171- }
172- } )
173- } ;
174-
175- let interpreter = self . make_interpreter ( ) ;
267+ let docs = self . documents . read ( ) . await ;
268+ let uri = & params. text_document_position . text_document . uri ;
176269 let position = params. text_document_position . position ;
177- let response = completion:: complete ( state. as_ref ( ) , position, & interpreter) ;
270+ // Completion is synchronous and never awaits, so we can hold the read
271+ // lock and borrow the state directly (no cloning).
272+ let response = completion:: complete ( docs. get ( uri) , position, & self . functions ) ;
178273 Ok ( Some ( response) )
179274 }
180275
0 commit comments