Skip to main content

logicaffeine_lsp/
server.rs

1use std::sync::Arc;
2
3use tower_lsp::jsonrpc::Result;
4use tower_lsp::lsp_types::*;
5use tower_lsp::{Client, LanguageServer};
6
7use crate::document::DocumentState;
8use crate::flycheck::{CargoFlycheck, Flycheck, FlycheckRunner};
9use crate::scheduler::{Scheduler, DEBOUNCE};
10use crate::semantic_tokens;
11use crate::state::ServerState;
12use crate::workspace::WorkspaceIndex;
13
14pub struct LogicAffeineServer {
15    client: Client,
16    state: Arc<ServerState>,
17    scheduler: Arc<Scheduler>,
18    workspace_index: Arc<WorkspaceIndex>,
19    workspace_roots: std::sync::Mutex<Vec<std::path::PathBuf>>,
20    flycheck: Arc<Flycheck>,
21    /// `logicaffeine.flycheck.enable` — on-save rustc analysis (default on).
22    flycheck_enabled: std::sync::atomic::AtomicBool,
23}
24
25impl LogicAffeineServer {
26    pub fn new(client: Client) -> Self {
27        Self::with_flycheck(client, Box::new(CargoFlycheck::new()))
28    }
29
30    /// Construct with an injected check engine — the seam mock-runner tests
31    /// use to prove merge/staleness/dedup behavior without a toolchain.
32    pub fn with_flycheck(client: Client, runner: Box<dyn FlycheckRunner>) -> Self {
33        LogicAffeineServer {
34            client,
35            state: Arc::new(ServerState::new()),
36            scheduler: Arc::new(Scheduler::new()),
37            workspace_index: Arc::new(WorkspaceIndex::new()),
38            workspace_roots: std::sync::Mutex::new(Vec::new()),
39            flycheck: Arc::new(Flycheck::new(runner)),
40            flycheck_enabled: std::sync::atomic::AtomicBool::new(true),
41        }
42    }
43
44    /// The cache key for this document's flycheck runs: the workspace root
45    /// when one is open, else the file's own directory.
46    fn workspace_key(&self, uri: &Url) -> String {
47        if let Some(root) = self.workspace_roots.lock().unwrap().first() {
48            return root.display().to_string();
49        }
50        uri.to_file_path()
51            .ok()
52            .and_then(|p| p.parent().map(|d| d.display().to_string()))
53            .unwrap_or_else(|| uri.to_string())
54    }
55
56    /// Analyze `text` off the async runtime, and — if `generation` is still
57    /// current when done — install the snapshot and publish its diagnostics.
58    async fn analyze_and_publish(
59        client: Client,
60        state: Arc<ServerState>,
61        scheduler: Arc<Scheduler>,
62        flycheck: Arc<Flycheck>,
63        uri: Url,
64        text: String,
65        version: i32,
66        generation: u64,
67    ) {
68        let analysis_uri = uri.clone();
69        let document = match tokio::task::spawn_blocking(move || {
70            DocumentState::with_uri(text, version, Some(&analysis_uri))
71        })
72        .await
73        {
74            Ok(document) => document,
75            Err(join_error) => {
76                // Defense in depth: the parser is total by construction, but
77                // an analysis panic must degrade to "stale snapshot", never
78                // take the document (or the server) down.
79                log::error!("analysis panicked for {uri}: {join_error}");
80                return;
81            }
82        };
83
84        if !scheduler.is_current(&uri, generation) {
85            return;
86        }
87        let snapshot = state.install_snapshot(uri.clone(), document);
88        let mut diagnostics = snapshot.diagnostics.clone();
89        diagnostics.extend(flycheck.diagnostics_for(&uri));
90        client
91            .publish_diagnostics(uri, diagnostics, Some(snapshot.version))
92            .await;
93    }
94}
95
96#[tower_lsp::async_trait]
97impl LanguageServer for LogicAffeineServer {
98    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
99        let mut roots = Vec::new();
100        if let Some(folders) = params.workspace_folders {
101            roots.extend(folders.iter().filter_map(|f| f.uri.to_file_path().ok()));
102        }
103        #[allow(deprecated)]
104        if roots.is_empty() {
105            if let Some(root) = params.root_uri.and_then(|u| u.to_file_path().ok()) {
106                roots.push(root);
107            }
108        }
109        *self.workspace_roots.lock().unwrap() = roots;
110
111        Ok(InitializeResult {
112            capabilities: ServerCapabilities {
113                text_document_sync: Some(TextDocumentSyncCapability::Options(
114                    TextDocumentSyncOptions {
115                        open_close: Some(true),
116                        change: Some(TextDocumentSyncKind::INCREMENTAL),
117                        save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
118                            include_text: Some(false),
119                        })),
120                        ..Default::default()
121                    },
122                )),
123                semantic_tokens_provider: Some(
124                    SemanticTokensServerCapabilities::SemanticTokensOptions(
125                        SemanticTokensOptions {
126                            legend: semantic_tokens::legend(),
127                            full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
128                            range: Some(true),
129                            ..Default::default()
130                        },
131                    ),
132                ),
133                document_symbol_provider: Some(OneOf::Left(true)),
134                definition_provider: Some(OneOf::Left(true)),
135                hover_provider: Some(HoverProviderCapability::Simple(true)),
136                completion_provider: Some(CompletionOptions {
137                    trigger_characters: Some(vec![
138                        ".".to_string(),
139                        ":".to_string(),
140                        "'".to_string(),
141                    ]),
142                    ..Default::default()
143                }),
144                references_provider: Some(OneOf::Left(true)),
145                signature_help_provider: Some(SignatureHelpOptions {
146                    trigger_characters: Some(vec![
147                        " ".to_string(),
148                        ",".to_string(),
149                    ]),
150                    ..Default::default()
151                }),
152                code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
153                rename_provider: Some(OneOf::Right(RenameOptions {
154                    prepare_provider: Some(true),
155                    work_done_progress_options: Default::default(),
156                })),
157                folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
158                inlay_hint_provider: Some(OneOf::Left(true)),
159                code_lens_provider: Some(CodeLensOptions {
160                    resolve_provider: Some(false),
161                }),
162                document_formatting_provider: Some(OneOf::Left(true)),
163                document_range_formatting_provider: Some(OneOf::Left(true)),
164                document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
165                    first_trigger_character: ".".to_string(),
166                    more_trigger_character: None,
167                }),
168                document_highlight_provider: Some(OneOf::Left(true)),
169                selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
170                call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
171                diagnostic_provider: Some(DiagnosticServerCapabilities::Options(
172                    DiagnosticOptions {
173                        identifier: None,
174                        inter_file_dependencies: false,
175                        workspace_diagnostics: false,
176                        work_done_progress_options: Default::default(),
177                    },
178                )),
179                workspace_symbol_provider: Some(OneOf::Left(true)),
180                ..Default::default()
181            },
182            server_info: Some(ServerInfo {
183                name: "logicaffeine-lsp".to_string(),
184                version: Some(env!("CARGO_PKG_VERSION").to_string()),
185            }),
186        })
187    }
188
189    async fn initialized(&self, _: InitializedParams) {
190        log::info!("LogicAffeine LSP initialized");
191        // Index the workspace in the background; requests answer from
192        // whatever has landed so far.
193        let roots = self.workspace_roots.lock().unwrap().clone();
194        let index = Arc::clone(&self.workspace_index);
195        tokio::task::spawn_blocking(move || {
196            for root in roots {
197                index.scan_folder(&root);
198            }
199        });
200    }
201
202    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
203        // `logicaffeine.flycheck.enable` — the only server-side setting today.
204        let enabled = params
205            .settings
206            .pointer("/logicaffeine/flycheck/enable")
207            .and_then(|v| v.as_bool())
208            .unwrap_or(true);
209        let was = self
210            .flycheck_enabled
211            .swap(enabled, std::sync::atomic::Ordering::SeqCst);
212        if was && !enabled {
213            // Turning it off retracts published findings: republish every
214            // open document's interactive-only set.
215            let uris = self.scheduler.open_uris();
216            for uri in uris {
217                self.flycheck.forget(&uri);
218                if let Some(doc) = self.state.snapshot(&uri) {
219                    self.client
220                        .publish_diagnostics(uri, doc.diagnostics.clone(), Some(doc.version))
221                        .await;
222                }
223            }
224        }
225    }
226
227    async fn did_save(&self, params: DidSaveTextDocumentParams) {
228        if !self
229            .flycheck_enabled
230            .load(std::sync::atomic::Ordering::SeqCst)
231        {
232            // Workspace re-index still happens; only the rustc pass is off.
233            if let Ok(path) = params.text_document.uri.to_file_path() {
234                let index = Arc::clone(&self.workspace_index);
235                tokio::task::spawn_blocking(move || index.index_file(&path));
236            }
237            return;
238        }
239        let uri = params.text_document.uri;
240        if let Ok(path) = uri.to_file_path() {
241            let index = Arc::clone(&self.workspace_index);
242            tokio::task::spawn_blocking(move || index.index_file(&path));
243        }
244
245        // Flycheck: run rustc's analysis over the saved text in the
246        // background; the generation guard means a newer save always wins.
247        let Some((text, version)) = self.scheduler.current_text(&uri) else {
248            return;
249        };
250        let generation = self.flycheck.begin_save(&uri);
251        let workspace_key = self.workspace_key(&uri);
252        let client = self.client.clone();
253        let state = Arc::clone(&self.state);
254        let flycheck = Arc::clone(&self.flycheck);
255        tokio::spawn(async move {
256            let run_text = text.clone();
257            let run_flycheck = Arc::clone(&flycheck);
258            let findings = tokio::task::spawn_blocking(move || {
259                run_flycheck.run(&run_text, &workspace_key)
260            })
261            .await
262            .expect("flycheck task panicked");
263
264            // Unavailable toolchain: interactive-only diagnostics, silently.
265            let Some(findings) = findings else { return };
266
267            let interactive = state
268                .snapshot(&uri)
269                .map(|doc| doc.diagnostics.clone())
270                .unwrap_or_default();
271            let Some(rustc_diagnostics) =
272                flycheck.complete(&uri, generation, findings, &text, &interactive)
273            else {
274                return; // a newer save or an edit superseded this run
275            };
276
277            let mut diagnostics = interactive;
278            diagnostics.extend(rustc_diagnostics);
279            client
280                .publish_diagnostics(uri, diagnostics, Some(version))
281                .await;
282        });
283    }
284
285    async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
286        let index = Arc::clone(&self.workspace_index);
287        tokio::task::spawn_blocking(move || {
288            for change in params.changes {
289                match change.typ {
290                    FileChangeType::DELETED => index.remove(&change.uri),
291                    _ => {
292                        if let Ok(path) = change.uri.to_file_path() {
293                            index.index_file(&path);
294                        }
295                    }
296                }
297            }
298        });
299    }
300
301    async fn symbol(
302        &self,
303        params: WorkspaceSymbolParams,
304    ) -> Result<Option<Vec<SymbolInformation>>> {
305        let hits = self.workspace_index.query(&params.query, 256);
306        if hits.is_empty() {
307            return Ok(None);
308        }
309        #[allow(deprecated)]
310        Ok(Some(
311            hits.into_iter()
312                .map(|s| SymbolInformation {
313                    name: s.name,
314                    kind: s.kind,
315                    tags: None,
316                    deprecated: None,
317                    location: s.location,
318                    container_name: s.container,
319                })
320                .collect(),
321        ))
322    }
323
324    async fn shutdown(&self) -> Result<()> {
325        Ok(())
326    }
327
328    async fn did_open(&self, params: DidOpenTextDocumentParams) {
329        let uri = params.text_document.uri;
330        let text = params.text_document.text;
331        let version = params.text_document.version;
332
333        let generation = self.scheduler.open(uri.clone(), text.clone(), version);
334        // First analysis runs immediately — no debounce on open.
335        Self::analyze_and_publish(
336            self.client.clone(),
337            Arc::clone(&self.state),
338            Arc::clone(&self.scheduler),
339            Arc::clone(&self.flycheck),
340            uri,
341            text,
342            version,
343            generation,
344        )
345        .await;
346    }
347
348    async fn did_change(&self, params: DidChangeTextDocumentParams) {
349        let uri = params.text_document.uri;
350        let version = params.text_document.version;
351
352        let Some(generation) =
353            self.scheduler
354                .apply_changes(&uri, params.content_changes, version)
355        else {
356            return;
357        };
358
359        // Edits invalidate flycheck findings — their positions would lie.
360        self.flycheck.clear(&uri);
361
362        // Debounce: analysis fires only if no newer edit arrives in the
363        // window; the generation guard drops stale results after it.
364        let client = self.client.clone();
365        let state = Arc::clone(&self.state);
366        let scheduler = Arc::clone(&self.scheduler);
367        let flycheck = Arc::clone(&self.flycheck);
368        tokio::spawn(async move {
369            tokio::time::sleep(DEBOUNCE).await;
370            let Some((text, version)) = scheduler.current_if(&uri, generation) else {
371                return;
372            };
373            Self::analyze_and_publish(
374                client, state, scheduler, flycheck, uri, text, version, generation,
375            )
376            .await;
377        });
378    }
379
380    async fn did_close(&self, params: DidCloseTextDocumentParams) {
381        self.scheduler.close(&params.text_document.uri);
382        self.state.close_document(&params.text_document.uri);
383        self.flycheck.forget(&params.text_document.uri);
384        // Clear diagnostics on close
385        self.client
386            .publish_diagnostics(params.text_document.uri, vec![], None)
387            .await;
388    }
389
390    async fn semantic_tokens_full(
391        &self,
392        params: SemanticTokensParams,
393    ) -> Result<Option<SemanticTokensResult>> {
394        let uri = &params.text_document.uri;
395        let doc = match self.state.snapshot(uri) {
396            Some(doc) => doc,
397            None => return Ok(None),
398        };
399
400        let data = semantic_tokens::encode_document_tokens(&doc);
401        let result_id = self.state.cache_semantic_tokens(uri.clone(), data.clone());
402
403        Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
404            result_id: Some(result_id),
405            data,
406        })))
407    }
408
409    async fn semantic_tokens_full_delta(
410        &self,
411        params: SemanticTokensDeltaParams,
412    ) -> Result<Option<SemanticTokensFullDeltaResult>> {
413        let uri = &params.text_document.uri;
414        let doc = match self.state.snapshot(uri) {
415            Some(doc) => doc,
416            None => return Ok(None),
417        };
418
419        let data = semantic_tokens::encode_document_tokens(&doc);
420        let previous = self.state.cached_semantic_tokens(uri);
421        let result_id = self.state.cache_semantic_tokens(uri.clone(), data.clone());
422
423        match previous {
424            Some((prev_id, prev_data)) if prev_id == params.previous_result_id => {
425                let edits = semantic_tokens::semantic_token_edits(&prev_data, &data);
426                Ok(Some(SemanticTokensFullDeltaResult::TokensDelta(
427                    SemanticTokensDelta {
428                        result_id: Some(result_id),
429                        edits,
430                    },
431                )))
432            }
433            // Unknown or stale id: answer with the full set.
434            _ => Ok(Some(SemanticTokensFullDeltaResult::Tokens(SemanticTokens {
435                result_id: Some(result_id),
436                data,
437            }))),
438        }
439    }
440
441    async fn semantic_tokens_range(
442        &self,
443        params: SemanticTokensRangeParams,
444    ) -> Result<Option<SemanticTokensRangeResult>> {
445        let uri = &params.text_document.uri;
446        let doc = match self.state.snapshot(uri) {
447            Some(doc) => doc,
448            None => return Ok(None),
449        };
450
451        let start = doc.line_index.offset(params.range.start);
452        let end = doc.line_index.offset(params.range.end);
453        let data = semantic_tokens::encode_document_tokens_in_range(&doc, start, end);
454
455        Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
456            result_id: None,
457            data,
458        })))
459    }
460
461    async fn document_symbol(
462        &self,
463        params: DocumentSymbolParams,
464    ) -> Result<Option<DocumentSymbolResponse>> {
465        let uri = &params.text_document.uri;
466        let doc = match self.state.snapshot(uri) {
467            Some(doc) => doc,
468            None => return Ok(None),
469        };
470
471        let symbols = crate::document_symbols::document_symbols(&doc);
472        Ok(Some(DocumentSymbolResponse::Nested(symbols)))
473    }
474
475    async fn goto_definition(
476        &self,
477        params: GotoDefinitionParams,
478    ) -> Result<Option<GotoDefinitionResponse>> {
479        let uri = &params.text_document_position_params.text_document.uri;
480        let position = params.text_document_position_params.position;
481
482        let doc = match self.state.snapshot(uri) {
483            Some(doc) => doc,
484            None => return Ok(None),
485        };
486
487        if let Some(local) = crate::definition::goto_definition(&doc, position, uri) {
488            return Ok(Some(local));
489        }
490
491        // Cross-file: resolve the name under the cursor against the
492        // workspace index when this document doesn't define it.
493        let offset = doc.line_index.offset(position);
494        let name = doc
495            .tokens
496            .iter()
497            .find(|t| t.span.start <= offset && offset < t.span.end)
498            .and_then(|t| crate::index::resolve_token_name(t, &doc.interner))
499            .map(|n| n.to_string());
500        Ok(name
501            .and_then(|n| self.workspace_index.definition_of(&n))
502            .map(GotoDefinitionResponse::Scalar))
503    }
504
505    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
506        let uri = &params.text_document_position_params.text_document.uri;
507        let position = params.text_document_position_params.position;
508
509        let doc = match self.state.snapshot(uri) {
510            Some(doc) => doc,
511            None => return Ok(None),
512        };
513
514        Ok(crate::hover::hover(&doc, position))
515    }
516
517    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
518        let uri = &params.text_document_position.text_document.uri;
519        let position = params.text_document_position.position;
520
521        let doc = match self.state.snapshot(uri) {
522            Some(doc) => doc,
523            None => return Ok(None),
524        };
525
526        Ok(crate::completion::completions(&doc, position))
527    }
528
529    async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
530        let uri = &params.text_document_position.text_document.uri;
531        let position = params.text_document_position.position;
532
533        let doc = match self.state.snapshot(uri) {
534            Some(doc) => doc,
535            None => return Ok(None),
536        };
537
538        let include_declaration = params.context.include_declaration;
539        let mut locations =
540            crate::references::find_references(&doc, position, uri, include_declaration);
541
542        // Cross-file: LIVE buffers answer for themselves; the workspace
543        // index answers for everything on disk that isn't open.
544        if let Some(name) = crate::references::name_at(&doc, position) {
545            if crate::references::is_cross_file_symbol(&doc, &name) {
546                let open_docs = self.state.open_documents();
547                for (other_uri, other_doc) in &open_docs {
548                    if other_uri == uri {
549                        continue;
550                    }
551                    for range in crate::references::cross_file_candidates(
552                        other_doc,
553                        &name,
554                        include_declaration,
555                    ) {
556                        locations.push(Location { uri: other_uri.clone(), range });
557                    }
558                }
559                let skip: Vec<&Url> = open_docs.iter().map(|(u, _)| u).collect();
560                locations.extend(self.workspace_index.references_of(&name, &skip));
561            }
562        }
563
564        if locations.is_empty() {
565            Ok(None)
566        } else {
567            Ok(Some(locations))
568        }
569    }
570
571    async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
572        let uri = &params.text_document_position_params.text_document.uri;
573        let position = params.text_document_position_params.position;
574
575        let doc = match self.state.snapshot(uri) {
576            Some(doc) => doc,
577            None => return Ok(None),
578        };
579
580        Ok(crate::signature_help::signature_help(&doc, position))
581    }
582
583    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
584        let uri = &params.text_document.uri;
585        let range = params.range;
586
587        let doc = match self.state.snapshot(uri) {
588            Some(doc) => doc,
589            None => return Ok(None),
590        };
591
592        let actions = crate::code_actions::code_actions(&doc, range, uri);
593        if actions.is_empty() {
594            Ok(None)
595        } else {
596            Ok(Some(actions))
597        }
598    }
599
600    async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
601        let uri = &params.text_document_position.text_document.uri;
602        let position = params.text_document_position.position;
603
604        let doc = match self.state.snapshot(uri) {
605            Some(doc) => doc,
606            None => return Ok(None),
607        };
608
609        let new_name = params.new_name;
610        let mut edit = match crate::rename::rename(&doc, position, new_name.clone(), uri) {
611            Some(edit) => edit,
612            None => return Ok(None),
613        };
614
615        // Cross-file: LIVE buffers answer for themselves; the workspace
616        // index answers for everything on disk that isn't open.
617        if let Some(name) = crate::references::name_at(&doc, position) {
618            if crate::references::is_cross_file_symbol(&doc, &name) {
619                let changes = edit.changes.get_or_insert_with(Default::default);
620                let open_docs = self.state.open_documents();
621                for (other_uri, other_doc) in &open_docs {
622                    if other_uri == uri {
623                        continue;
624                    }
625                    let ranges =
626                        crate::references::cross_file_candidates(other_doc, &name, true);
627                    if !ranges.is_empty() {
628                        changes.entry(other_uri.clone()).or_default().extend(
629                            ranges.into_iter().map(|range| TextEdit {
630                                range,
631                                new_text: new_name.clone(),
632                            }),
633                        );
634                    }
635                }
636                let skip: Vec<&Url> = open_docs.iter().map(|(u, _)| u).collect();
637                for location in self.workspace_index.references_of(&name, &skip) {
638                    changes.entry(location.uri).or_default().push(TextEdit {
639                        range: location.range,
640                        new_text: new_name.clone(),
641                    });
642                }
643            }
644        }
645
646        Ok(Some(edit))
647    }
648
649    async fn prepare_rename(
650        &self,
651        params: TextDocumentPositionParams,
652    ) -> Result<Option<PrepareRenameResponse>> {
653        let uri = &params.text_document.uri;
654        let position = params.position;
655
656        let doc = match self.state.snapshot(uri) {
657            Some(doc) => doc,
658            None => return Ok(None),
659        };
660
661        Ok(crate::rename::prepare_rename(&doc, position).map(|(range, text)| {
662            PrepareRenameResponse::RangeWithPlaceholder {
663                range,
664                placeholder: text,
665            }
666        }))
667    }
668
669    async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
670        let uri = &params.text_document.uri;
671
672        let doc = match self.state.snapshot(uri) {
673            Some(doc) => doc,
674            None => return Ok(None),
675        };
676
677        let ranges = crate::folding::folding_ranges(&doc);
678        if ranges.is_empty() {
679            Ok(None)
680        } else {
681            Ok(Some(ranges))
682        }
683    }
684
685    async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
686        let uri = &params.text_document.uri;
687
688        let doc = match self.state.snapshot(uri) {
689            Some(doc) => doc,
690            None => return Ok(None),
691        };
692
693        let hints = crate::inlay_hints::inlay_hints(&doc, params.range);
694        if hints.is_empty() {
695            Ok(None)
696        } else {
697            Ok(Some(hints))
698        }
699    }
700
701    async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
702        let uri = &params.text_document.uri;
703
704        let doc = match self.state.snapshot(uri) {
705            Some(doc) => doc,
706            None => return Ok(None),
707        };
708
709        let lenses = crate::code_lens::code_lenses(&doc, uri);
710        if lenses.is_empty() {
711            Ok(None)
712        } else {
713            Ok(Some(lenses))
714        }
715    }
716
717    async fn document_highlight(
718        &self,
719        params: DocumentHighlightParams,
720    ) -> Result<Option<Vec<DocumentHighlight>>> {
721        let uri = &params.text_document_position_params.text_document.uri;
722        let doc = match self.state.snapshot(uri) {
723            Some(doc) => doc,
724            None => return Ok(None),
725        };
726        Ok(crate::document_highlights::document_highlights(
727            &doc,
728            params.text_document_position_params.position,
729        ))
730    }
731
732    async fn selection_range(
733        &self,
734        params: SelectionRangeParams,
735    ) -> Result<Option<Vec<SelectionRange>>> {
736        let doc = match self.state.snapshot(&params.text_document.uri) {
737            Some(doc) => doc,
738            None => return Ok(None),
739        };
740        Ok(Some(crate::selection_ranges::selection_ranges(
741            &doc,
742            &params.positions,
743        )))
744    }
745
746    async fn on_type_formatting(
747        &self,
748        params: DocumentOnTypeFormattingParams,
749    ) -> Result<Option<Vec<TextEdit>>> {
750        let uri = &params.text_document_position.text_document.uri;
751        let doc = match self.state.snapshot(uri) {
752            Some(doc) => doc,
753            None => return Ok(None),
754        };
755
756        // Normalize the line the sentence just closed on — the same rule set
757        // `largo fmt` applies, one sentence at a time.
758        let line_number = params.text_document_position.position.line as usize;
759        let Some(line) = doc.source.split('\n').nth(line_number) else {
760            return Ok(None);
761        };
762        // Inside a multiline string every byte is content: an odd number of
763        // """ delimiters before this line means the sentence-closing `.`
764        // was typed inside one — never touch it. The LIVE text decides (the
765        // snapshot can lag a keystroke behind).
766        let live = self
767            .scheduler
768            .current_text(uri)
769            .map(|(text, _)| text)
770            .unwrap_or_else(|| doc.source.clone());
771        let line_start_offset: usize = live
772            .split('\n')
773            .take(line_number)
774            .map(|l| l.len() + 1)
775            .sum();
776        let delimiters_before = live[..line_start_offset.min(live.len())]
777            .matches("\"\"\"")
778            .count();
779        if delimiters_before % 2 == 1 {
780            return Ok(None);
781        }
782        let formatted = logicaffeine_language::source_format::format_line(line);
783        if formatted == line {
784            return Ok(None);
785        }
786        let end_character = line.encode_utf16().count() as u32;
787        Ok(Some(vec![TextEdit {
788            range: Range {
789                start: Position { line: line_number as u32, character: 0 },
790                end: Position { line: line_number as u32, character: end_character },
791            },
792            new_text: formatted,
793        }]))
794    }
795
796    async fn prepare_call_hierarchy(
797        &self,
798        params: CallHierarchyPrepareParams,
799    ) -> Result<Option<Vec<CallHierarchyItem>>> {
800        let uri = &params.text_document_position_params.text_document.uri;
801        let doc = match self.state.snapshot(uri) {
802            Some(doc) => doc,
803            None => return Ok(None),
804        };
805        Ok(crate::call_hierarchy::prepare(
806            &doc,
807            params.text_document_position_params.position,
808            uri,
809        ))
810    }
811
812    async fn incoming_calls(
813        &self,
814        params: CallHierarchyIncomingCallsParams,
815    ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
816        let uri = &params.item.uri;
817        let doc = match self.state.snapshot(uri) {
818            Some(doc) => doc,
819            None => return Ok(None),
820        };
821        Ok(Some(crate::call_hierarchy::incoming_calls(
822            &doc,
823            &params.item,
824            uri,
825        )))
826    }
827
828    async fn outgoing_calls(
829        &self,
830        params: CallHierarchyOutgoingCallsParams,
831    ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
832        let uri = &params.item.uri;
833        let doc = match self.state.snapshot(uri) {
834            Some(doc) => doc,
835            None => return Ok(None),
836        };
837        Ok(Some(crate::call_hierarchy::outgoing_calls(
838            &doc,
839            &params.item,
840            uri,
841        )))
842    }
843
844    async fn diagnostic(
845        &self,
846        params: DocumentDiagnosticParams,
847    ) -> Result<DocumentDiagnosticReportResult> {
848        let uri = &params.text_document.uri;
849        let (items, result_id) = match self.state.snapshot(uri) {
850            Some(doc) => {
851                let mut items = doc.diagnostics.clone();
852                let rustc = self.flycheck.diagnostics_for(uri);
853                // The id covers BOTH engines: a flycheck completion changes
854                // the report without a version bump.
855                let result_id = format!("{}:{}", doc.version, rustc.len());
856                items.extend(rustc);
857                (items, result_id)
858            }
859            None => (Vec::new(), "closed".to_string()),
860        };
861
862        if params.previous_result_id.as_deref() == Some(result_id.as_str()) {
863            return Ok(DocumentDiagnosticReportResult::Report(
864                DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport {
865                    related_documents: None,
866                    unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
867                        result_id,
868                    },
869                }),
870            ));
871        }
872
873        Ok(DocumentDiagnosticReportResult::Report(
874            DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
875                related_documents: None,
876                full_document_diagnostic_report: FullDocumentDiagnosticReport {
877                    result_id: Some(result_id),
878                    items,
879                },
880            }),
881        ))
882    }
883
884    async fn range_formatting(
885        &self,
886        params: DocumentRangeFormattingParams,
887    ) -> Result<Option<Vec<TextEdit>>> {
888        let uri = &params.text_document.uri;
889        let doc = match self.state.snapshot(uri) {
890            Some(doc) => doc,
891            None => return Ok(None),
892        };
893        let edits = crate::formatting::format_range(&doc, params.range);
894        if edits.is_empty() {
895            Ok(None)
896        } else {
897            Ok(Some(edits))
898        }
899    }
900
901    async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
902        let uri = &params.text_document.uri;
903
904        let doc = match self.state.snapshot(uri) {
905            Some(doc) => doc,
906            None => return Ok(None),
907        };
908
909        let edits = crate::formatting::format_document(&doc);
910        if edits.is_empty() {
911            Ok(None)
912        } else {
913            Ok(Some(edits))
914        }
915    }
916}