Skip to main content

brrr_lint/
server.rs

1//! LSP server implementation.
2
3use crate::config::{FstarConfig, LspSettings};
4use crate::document::{DocumentState, FragmentStatus, StatusUpdate};
5use crate::error::Result;
6use dashmap::DashMap;
7use std::sync::Arc;
8use tokio::sync::{mpsc, RwLock};
9use tower_lsp::jsonrpc::Result as RpcResult;
10use tower_lsp::lsp_types::{
11    CompletionOptions, CompletionParams, CompletionResponse, DidChangeConfigurationParams,
12    DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
13    DidSaveTextDocumentParams, DocumentFormattingParams, DocumentRangeFormattingParams,
14    GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
15    HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, MarkupContent,
16    MarkupKind, MessageType, OneOf, Position, Range, SaveOptions, ServerCapabilities, ServerInfo,
17    TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
18    TextDocumentSyncSaveOptions, TextEdit, Url, WorkspaceFolder,
19};
20use tower_lsp::{Client, LanguageServer};
21use tracing::{debug, error, info};
22
23/// Custom LSP requests for F*.
24///
25/// These requests allow clients to query the current verification state
26/// without relying on notifications. Useful for custom UI, debugging tools,
27/// or integration with other extensions.
28pub mod requests {
29    use serde::{Deserialize, Serialize};
30    use tower_lsp::lsp_types::{Range, TextDocumentIdentifier};
31
32    /// Parameters for getDiagnostics request.
33    pub type GetDiagnosticsParams = TextDocumentIdentifier;
34
35    /// Parameters for getFragments request.
36    pub type GetFragmentsParams = TextDocumentIdentifier;
37
38    /// Information about a verified code fragment.
39    ///
40    /// Provides the verification status and range of each fragment
41    /// in the document, enabling clients to display custom verification
42    /// progress indicators.
43    #[derive(Debug, Clone, Serialize, Deserialize)]
44    pub struct FragmentInfo {
45        /// The range of the fragment in the document.
46        pub range: Range,
47        /// Verification status: "ok", "lax-ok", "in-progress", "started", or "failed".
48        pub status: String,
49        /// True if the fragment was invalidated by an edit (stale result).
50        #[serde(skip_serializing_if = "std::ops::Not::not")]
51        pub stale: bool,
52    }
53}
54
55/// Custom LSP notifications for F*.
56pub mod notifications {
57    use serde::{Deserialize, Serialize};
58    use tower_lsp::lsp_types::notification::Notification;
59
60    /// Status notification: server -> client.
61    #[derive(Debug)]
62    pub enum StatusNotification {}
63
64    impl Notification for StatusNotification {
65        type Params = StatusParams;
66        const METHOD: &'static str = "$/fstar/status";
67    }
68
69    #[derive(Debug, Clone, Serialize, Deserialize)]
70    pub struct StatusParams {
71        pub uri: String,
72        pub fragments: Vec<FragmentStatus>,
73    }
74
75    #[derive(Debug, Clone, Serialize, Deserialize)]
76    pub struct FragmentStatus {
77        pub kind: String,
78        pub range: tower_lsp::lsp_types::Range,
79        #[serde(skip_serializing_if = "std::ops::Not::not")]
80        pub stale: bool,
81    }
82
83    /// Verify to position: client -> server.
84    ///
85    /// The enum type is never directly referenced - handlers are registered
86    /// with string method names. However, this enum defines the protocol
87    /// contract (METHOD constant and Params type) for documentation and
88    /// type safety when tower-lsp deserializes incoming notifications.
89    #[allow(dead_code)]
90    #[derive(Debug)]
91    pub enum VerifyToPosition {}
92
93    impl Notification for VerifyToPosition {
94        type Params = VerifyToPositionParams;
95        const METHOD: &'static str = "$/fstar/verifyToPosition";
96    }
97
98    #[derive(Debug, Clone, Serialize, Deserialize)]
99    pub struct VerifyToPositionParams {
100        pub uri: String,
101        pub position: tower_lsp::lsp_types::Position,
102        pub lax: bool,
103    }
104
105    /// Restart: client -> server.
106    ///
107    /// Protocol marker enum - see VerifyToPosition for explanation.
108    #[allow(dead_code)]
109    #[derive(Debug)]
110    pub enum Restart {}
111
112    impl Notification for Restart {
113        type Params = RestartParams;
114        const METHOD: &'static str = "$/fstar/restart";
115    }
116
117    #[derive(Debug, Clone, Serialize, Deserialize)]
118    pub struct RestartParams {
119        pub uri: String,
120    }
121
122    /// Kill and restart solver: client -> server.
123    ///
124    /// Protocol marker enum - see VerifyToPosition for explanation.
125    #[allow(dead_code)]
126    #[derive(Debug)]
127    pub enum KillAndRestartSolver {}
128
129    impl Notification for KillAndRestartSolver {
130        type Params = KillAndRestartSolverParams;
131        const METHOD: &'static str = "$/fstar/killAndRestartSolver";
132    }
133
134    #[derive(Debug, Clone, Serialize, Deserialize)]
135    pub struct KillAndRestartSolverParams {
136        pub uri: String,
137    }
138
139    /// Kill all: client -> server.
140    ///
141    /// Protocol marker enum - see VerifyToPosition for explanation.
142    #[allow(dead_code)]
143    #[derive(Debug)]
144    pub enum KillAll {}
145
146    impl Notification for KillAll {
147        type Params = KillAllParams;
148        const METHOD: &'static str = "$/fstar/killAll";
149    }
150
151    /// Parameters for kill all notification (empty object).
152    #[derive(Debug, Clone, Serialize, Deserialize)]
153    pub struct KillAllParams {}
154
155    /// Get translated F* position (C2Pulse): client -> server request.
156    #[derive(Debug, Clone, Serialize, Deserialize)]
157    pub struct GetTranslatedFstParams {
158        pub uri: String,
159        pub position: tower_lsp::lsp_types::Position,
160    }
161
162    #[derive(Debug, Clone, Serialize, Deserialize)]
163    pub struct GetTranslatedFstResponse {
164        pub uri: String,
165        pub position: tower_lsp::lsp_types::Position,
166    }
167}
168
169/// F* Language Server.
170pub struct FstarServer {
171    /// LSP client for sending notifications.
172    client: Client,
173
174    /// Document states indexed by URI.
175    documents: DashMap<String, Arc<DocumentState>>,
176
177    /// LSP settings.
178    settings: RwLock<LspSettings>,
179
180    /// Status update sender.
181    status_tx: mpsc::Sender<StatusUpdate>,
182
183    /// Workspace folders.
184    workspace_folders: RwLock<Vec<WorkspaceFolder>>,
185
186    /// Per-document flycheck debounce handles.
187    /// Stores an abort handle that cancels the previous flycheck task.
188    flycheck_handles: DashMap<String, tokio::task::AbortHandle>,
189}
190
191impl FstarServer {
192    /// Create a new F* server with CLI-derived settings.
193    pub fn new(client: Client, cli_settings: LspSettings) -> Self {
194        let (status_tx, status_rx) = mpsc::channel(100);
195
196        let server = Self {
197            client: client.clone(),
198            documents: DashMap::new(),
199            settings: RwLock::new(cli_settings),
200            status_tx,
201            workspace_folders: RwLock::new(Vec::new()),
202            flycheck_handles: DashMap::new(),
203        };
204
205        // Spawn status update handler
206        tokio::spawn(Self::status_update_handler(client, status_rx));
207
208        server
209    }
210
211    /// Handle status updates and forward to client.
212    async fn status_update_handler(client: Client, mut rx: mpsc::Receiver<StatusUpdate>) {
213        while let Some(update) = rx.recv().await {
214            let params = notifications::StatusParams {
215                uri: update.uri.to_string(),
216                fragments: update
217                    .fragments
218                    .iter()
219                    .map(|f| notifications::FragmentStatus {
220                        kind: match f.status {
221                            FragmentStatus::Ok => "ok",
222                            FragmentStatus::LaxOk => "lax-ok",
223                            FragmentStatus::Started => "started",
224                            FragmentStatus::Failed => "failed",
225                        }
226                        .to_string(),
227                        range: f.range,
228                        stale: f.stale,
229                    })
230                    .collect(),
231            };
232
233            client
234                .send_notification::<notifications::StatusNotification>(params)
235                .await;
236
237            // Also publish diagnostics
238            client
239                .publish_diagnostics(update.uri, update.diagnostics, None)
240                .await;
241        }
242    }
243
244    /// Get or create document state.
245    async fn get_or_create_document(
246        &self,
247        uri: &Url,
248        text: Option<String>,
249        version: i32,
250    ) -> Result<Arc<DocumentState>> {
251        let key = uri.to_string();
252
253        if let Some(doc) = self.documents.get(&key) {
254            return Ok(Arc::clone(&doc));
255        }
256
257        // Find config for this file
258        let file_path = uri
259            .to_file_path()
260            .map_err(|_| crate::error::FstarError::Config("Invalid URI".to_string()))?;
261
262        // Collect workspace folder paths for config boundary check
263        let ws_folders: Vec<std::path::PathBuf> = self
264            .workspace_folders
265            .read()
266            .await
267            .iter()
268            .filter_map(|f| Url::parse(&f.uri.to_string()).ok()?.to_file_path().ok())
269            .collect();
270
271        let mut config = match FstarConfig::find_and_load(&file_path, &ws_folders).await? {
272            Some((config, _)) => config,
273            None => FstarConfig::default(),
274        };
275
276        // Apply CLI fstar_exe override if config doesn't specify one
277        let settings = self.settings.read().await;
278        if config.fstar_exe.is_none() {
279            if let Some(ref exe) = settings.fstar_exe {
280                config.fstar_exe = Some(exe.clone());
281            }
282        }
283        drop(settings);
284
285        let settings = self.settings.read().await.clone();
286        let doc = Arc::new(
287            DocumentState::new(
288                uri.clone(),
289                text.unwrap_or_default(),
290                version,
291                config,
292                settings,
293                self.status_tx.clone(),
294            )
295            .await?,
296        );
297
298        // Initialize F* connections - if this fails, don't store the broken document
299        if let Err(e) = doc.initialize().await {
300            error!("Failed to initialize F* for {}: {}", uri, e);
301            self.client
302                .show_message(MessageType::ERROR, format!("Failed to start F*: {}", e))
303                .await;
304            return Err(e);
305        }
306
307        self.documents.insert(key, Arc::clone(&doc));
308        Ok(doc)
309    }
310
311    /// Get document state.
312    fn get_document(&self, uri: &Url) -> Option<Arc<DocumentState>> {
313        self.documents.get(&uri.to_string()).map(|r| Arc::clone(&r))
314    }
315
316    /// Remove document state.
317    async fn remove_document(&self, uri: &Url) {
318        if let Some((_, doc)) = self.documents.remove(&uri.to_string()) {
319            doc.dispose().await;
320        }
321    }
322}
323
324#[tower_lsp::async_trait]
325impl LanguageServer for FstarServer {
326    async fn initialize(&self, params: InitializeParams) -> RpcResult<InitializeResult> {
327        info!("F* LSP server initializing");
328
329        // Store workspace folders
330        if let Some(folders) = params.workspace_folders {
331            *self.workspace_folders.write().await = folders;
332        }
333
334        // Parse initialization options if provided, merging with CLI settings.
335        // CLI settings (fstar_exe, debug) take precedence over client settings.
336        if let Some(options) = params.initialization_options {
337            if let Ok(client_settings) = serde_json::from_value::<LspSettings>(options) {
338                let mut settings = self.settings.write().await;
339
340                // Preserve CLI fstar_exe if set, otherwise use client value
341                let cli_fstar_exe = settings.fstar_exe.clone();
342
343                // Merge client settings
344                settings.verify_on_open = client_settings.verify_on_open;
345                settings.verify_on_save = client_settings.verify_on_save;
346                settings.fly_check = client_settings.fly_check;
347                settings.timeout_ms = client_settings.timeout_ms;
348                settings.max_processes = client_settings.max_processes;
349
350                // CLI --debug wins (OR with client setting)
351                settings.debug = settings.debug || client_settings.debug;
352
353                // CLI fstar_exe takes precedence over client setting
354                if cli_fstar_exe.is_some() {
355                    settings.fstar_exe = cli_fstar_exe;
356                } else {
357                    settings.fstar_exe = client_settings.fstar_exe;
358                }
359            }
360        }
361
362        Ok(InitializeResult {
363            capabilities: ServerCapabilities {
364                text_document_sync: Some(TextDocumentSyncCapability::Options(
365                    TextDocumentSyncOptions {
366                        open_close: Some(true),
367                        change: Some(TextDocumentSyncKind::INCREMENTAL),
368                        save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
369                            include_text: Some(false),
370                        })),
371                        ..Default::default()
372                    },
373                )),
374                hover_provider: Some(HoverProviderCapability::Simple(true)),
375                completion_provider: Some(CompletionOptions {
376                    trigger_characters: Some(vec![".".to_string()]),
377                    ..Default::default()
378                }),
379                definition_provider: Some(OneOf::Left(true)),
380                document_formatting_provider: Some(OneOf::Left(true)),
381                document_range_formatting_provider: Some(OneOf::Left(true)),
382                ..Default::default()
383            },
384            server_info: Some(ServerInfo {
385                name: "fstar-lsp".to_string(),
386                version: Some(env!("CARGO_PKG_VERSION").to_string()),
387            }),
388        })
389    }
390
391    async fn initialized(&self, _: InitializedParams) {
392        info!("F* LSP server initialized");
393    }
394
395    async fn shutdown(&self) -> RpcResult<()> {
396        info!("F* LSP server shutting down");
397
398        // Collect all documents first to release DashMap locks before awaiting.
399        // DashMap::iter() holds shard read locks - calling .await while holding
400        // these locks would cause deadlocks if other tasks try to access documents.
401        let docs: Vec<Arc<DocumentState>> = self
402            .documents
403            .iter()
404            .map(|entry| Arc::clone(entry.value()))
405            .collect();
406
407        // Now dispose without holding any DashMap locks
408        for doc in docs {
409            doc.dispose().await;
410        }
411        self.documents.clear();
412
413        Ok(())
414    }
415
416    async fn did_open(&self, params: DidOpenTextDocumentParams) {
417        let uri = params.text_document.uri;
418        let text = params.text_document.text;
419        let version = params.text_document.version;
420
421        debug!("Document opened: {}", uri);
422
423        match self.get_or_create_document(&uri, Some(text), version).await {
424            Ok(doc) => {
425                let settings = self.settings.read().await;
426                if settings.verify_on_open {
427                    drop(settings);
428                    if let Err(e) = doc.verify_full().await {
429                        error!("Verification failed: {}", e);
430                    }
431                } else if settings.fly_check {
432                    drop(settings);
433                    if let Err(e) = doc.verify_lax().await {
434                        error!("Lax check failed: {}", e);
435                    }
436                }
437            }
438            Err(e) => {
439                error!("Failed to open document: {}", e);
440            }
441        }
442    }
443
444    async fn did_change(&self, params: DidChangeTextDocumentParams) {
445        let uri = params.text_document.uri;
446        let version = params.text_document.version;
447        let key = uri.to_string();
448
449        if let Some(doc) = self.get_document(&uri) {
450            doc.update(params.content_changes, version).await;
451
452            // Trigger flycheck after debounce, cancelling any previous pending flycheck
453            let settings = self.settings.read().await;
454            if settings.fly_check {
455                drop(settings);
456
457                // Cancel previous flycheck for this document
458                if let Some((_, prev)) = self.flycheck_handles.remove(&key) {
459                    prev.abort();
460                }
461
462                let doc = Arc::clone(&doc);
463                let handle_key = key.clone();
464                let handles = self.flycheck_handles.clone();
465                let task = tokio::spawn(async move {
466                    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
467                    // Send lax query to lax checker for flycheck diagnostics
468                    let _ = doc.verify_lax().await;
469                    // Send cache query to main checker so it knows about buffer changes.
470                    // This enables incremental verification: when verify_full() is later
471                    // called (e.g., on save), F* only re-verifies changed portions.
472                    let _ = doc.verify_cache().await;
473                    handles.remove(&handle_key);
474                });
475
476                self.flycheck_handles.insert(key, task.abort_handle());
477            }
478        }
479    }
480
481    async fn did_save(&self, params: DidSaveTextDocumentParams) {
482        let uri = params.text_document.uri;
483
484        if let Some(doc) = self.get_document(&uri) {
485            let settings = self.settings.read().await;
486            if settings.verify_on_save {
487                drop(settings);
488                if let Err(e) = doc.verify_full().await {
489                    error!("Verification on save failed: {}", e);
490                }
491            }
492        }
493    }
494
495    async fn did_close(&self, params: DidCloseTextDocumentParams) {
496        let uri = params.text_document.uri;
497        let key = uri.to_string();
498        debug!("Document closed: {}", uri);
499
500        // Abort any pending flycheck task before removing the document.
501        // This prevents the task from holding Arc<DocumentState> after close,
502        // ensuring resources are released immediately rather than after the
503        // 200ms debounce delay.
504        if let Some((_, handle)) = self.flycheck_handles.remove(&key) {
505            handle.abort();
506        }
507
508        self.remove_document(&uri).await;
509    }
510
511    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
512        if let Ok(client_settings) = serde_json::from_value::<LspSettings>(params.settings) {
513            info!("Configuration updated");
514            let mut settings = self.settings.write().await;
515
516            // Preserve CLI fstar_exe if set
517            let cli_fstar_exe = settings.fstar_exe.clone();
518
519            // Merge client settings
520            settings.verify_on_open = client_settings.verify_on_open;
521            settings.verify_on_save = client_settings.verify_on_save;
522            settings.fly_check = client_settings.fly_check;
523            settings.timeout_ms = client_settings.timeout_ms;
524            settings.max_processes = client_settings.max_processes;
525
526            // CLI --debug wins (OR with client setting)
527            settings.debug = settings.debug || client_settings.debug;
528
529            // CLI fstar_exe takes precedence over client setting
530            if cli_fstar_exe.is_some() {
531                settings.fstar_exe = cli_fstar_exe;
532            } else {
533                settings.fstar_exe = client_settings.fstar_exe;
534            }
535        }
536    }
537
538    async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {
539        let uri = params.text_document_position_params.text_document.uri;
540        let position = params.text_document_position_params.position;
541
542        if let Some(doc) = self.get_document(&uri) {
543            if let Some((contents, range)) = doc.hover(position).await {
544                return Ok(Some(Hover {
545                    contents: HoverContents::Markup(MarkupContent {
546                        kind: MarkupKind::Markdown,
547                        value: contents,
548                    }),
549                    range,
550                }));
551            }
552        }
553
554        Ok(None)
555    }
556
557    async fn goto_definition(
558        &self,
559        params: GotoDefinitionParams,
560    ) -> RpcResult<Option<GotoDefinitionResponse>> {
561        let uri = params.text_document_position_params.text_document.uri;
562        let position = params.text_document_position_params.position;
563
564        if let Some(doc) = self.get_document(&uri) {
565            if let Some(location) = doc.definition(position).await {
566                return Ok(Some(GotoDefinitionResponse::Scalar(location)));
567            }
568        }
569
570        Ok(None)
571    }
572
573    async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
574        let uri = params.text_document_position.text_document.uri;
575        let position = params.text_document_position.position;
576
577        if let Some(doc) = self.get_document(&uri) {
578            let items = doc.completions(position).await;
579            if !items.is_empty() {
580                return Ok(Some(CompletionResponse::Array(items)));
581            }
582        }
583
584        Ok(None)
585    }
586
587    async fn formatting(
588        &self,
589        params: DocumentFormattingParams,
590    ) -> RpcResult<Option<Vec<TextEdit>>> {
591        let uri = params.text_document.uri;
592
593        if let Some(doc) = self.get_document(&uri) {
594            if let Some(formatted) = doc.format().await {
595                let end_line = doc.line_count().await;
596
597                return Ok(Some(vec![TextEdit {
598                    range: Range {
599                        start: Position {
600                            line: 0,
601                            character: 0,
602                        },
603                        end: Position {
604                            line: end_line,
605                            character: 0,
606                        },
607                    },
608                    new_text: formatted,
609                }]));
610            }
611        }
612
613        Ok(None)
614    }
615
616    async fn range_formatting(
617        &self,
618        params: DocumentRangeFormattingParams,
619    ) -> RpcResult<Option<Vec<TextEdit>>> {
620        let uri = params.text_document.uri;
621        let range = params.range;
622
623        // FIX: Extract just the selected text and format that portion.
624        // The previous implementation formatted the entire document and then extracted
625        // lines using the original document's line numbers. This was broken because
626        // if formatting changes the number of lines (adding/removing blank lines,
627        // reformatting multi-line expressions), the extracted range would correspond
628        // to wrong lines, producing mangled text.
629        if let Some(doc) = self.get_document(&uri) {
630            // Extract the text within the requested range
631            if let Some(selected_text) = doc.get_text_in_range(range).await {
632                // Format just the selected text
633                if let Some(formatted) = doc.format_text(&selected_text).await {
634                    return Ok(Some(vec![TextEdit {
635                        range,
636                        new_text: formatted,
637                    }]));
638                }
639            }
640        }
641
642        Ok(None)
643    }
644}
645
646/// Custom notification handlers wired via LspService::build().custom_method().
647impl FstarServer {
648    /// Handle verify to position notification.
649    pub async fn handle_verify_to_position(&self, params: notifications::VerifyToPositionParams) {
650        if let Ok(uri) = Url::parse(&params.uri) {
651            if let Some(doc) = self.get_document(&uri) {
652                if let Err(e) = doc.verify_to_position(params.position, params.lax).await {
653                    error!("Verify to position failed: {}", e);
654                }
655            }
656        }
657    }
658
659    /// Handle restart notification.
660    pub async fn handle_restart(&self, params: notifications::RestartParams) {
661        if let Ok(uri) = Url::parse(&params.uri) {
662            if let Some(doc) = self.get_document(&uri) {
663                if let Err(e) = doc.restart().await {
664                    error!("Restart failed: {}", e);
665                    self.client
666                        .show_message(MessageType::ERROR, format!("Failed to restart F*: {}", e))
667                        .await;
668                }
669            }
670        }
671    }
672
673    /// Handle kill and restart solver notification.
674    pub async fn handle_kill_and_restart_solver(
675        &self,
676        params: notifications::KillAndRestartSolverParams,
677    ) {
678        if let Ok(uri) = Url::parse(&params.uri) {
679            if let Some(doc) = self.get_document(&uri) {
680                if let Err(e) = doc.restart_solver().await {
681                    error!("Restart solver failed: {}", e);
682                }
683            }
684        }
685    }
686
687    /// Handle kill all notification.
688    pub async fn handle_kill_all(&self, _params: notifications::KillAllParams) {
689        // Collect all documents first to release DashMap locks before awaiting.
690        // DashMap::iter() holds shard read locks - calling .await while holding
691        // these locks would cause deadlocks if other tasks try to access documents.
692        let docs: Vec<Arc<DocumentState>> = self
693            .documents
694            .iter()
695            .map(|entry| Arc::clone(entry.value()))
696            .collect();
697
698        // Now dispose without holding any DashMap locks
699        for doc in docs {
700            doc.dispose().await;
701        }
702        self.documents.clear();
703    }
704
705    /// Handle getTranslatedFst request (C2Pulse feature).
706    /// Returns None for F* files — only meaningful for C files with source maps.
707    pub async fn handle_get_translated_fst(
708        &self,
709        _params: notifications::GetTranslatedFstParams,
710    ) -> tower_lsp::jsonrpc::Result<Option<notifications::GetTranslatedFstResponse>> {
711        Ok(None)
712    }
713
714    /// Handle getDiagnostics request.
715    ///
716    /// Returns the current diagnostics for the specified document, merging
717    /// full verification diagnostics with flycheck (lax) diagnostics.
718    /// Flycheck diagnostics are filtered to only show those after the last
719    /// verified fragment to avoid duplicates.
720    pub async fn handle_get_diagnostics(
721        &self,
722        params: requests::GetDiagnosticsParams,
723    ) -> RpcResult<Vec<tower_lsp::lsp_types::Diagnostic>> {
724        if let Some(doc) = self.get_document(&params.uri) {
725            Ok(doc.get_diagnostics().await)
726        } else {
727            Ok(vec![])
728        }
729    }
730
731    /// Handle getFragments request.
732    ///
733    /// Returns information about all verified code fragments in the document,
734    /// including their ranges, verification status, and staleness.
735    /// Useful for custom UI that shows verification progress.
736    pub async fn handle_get_fragments(
737        &self,
738        params: requests::GetFragmentsParams,
739    ) -> RpcResult<Vec<requests::FragmentInfo>> {
740        if let Some(doc) = self.get_document(&params.uri) {
741            let fragments = doc.get_fragments().await;
742            Ok(fragments
743                .into_iter()
744                .map(|f| requests::FragmentInfo {
745                    range: f.range,
746                    status: fragment_status_to_string(f.status),
747                    stale: f.stale,
748                })
749                .collect())
750        } else {
751            Ok(vec![])
752        }
753    }
754}
755
756/// Converts a FragmentStatus enum to its string representation for the LSP response.
757fn fragment_status_to_string(status: FragmentStatus) -> String {
758    match status {
759        FragmentStatus::Ok => "ok",
760        FragmentStatus::LaxOk => "lax-ok",
761        FragmentStatus::Started => "started",
762        FragmentStatus::Failed => "failed",
763    }
764    .to_string()
765}