Skip to main content

eure_ls/
lib.rs

1//! Eure Language Server - LSP implementation for the Eure data format.
2//!
3//! This crate provides both a native binary (`eurels`) and a WASM module
4//! for use in VS Code web extensions.
5
6mod capabilities;
7pub mod queries;
8pub mod types;
9mod uri_utils;
10
11// Native-specific module (non-WASM)
12#[cfg(not(target_arch = "wasm32"))]
13pub mod native;
14
15// WASM-specific module
16#[cfg(target_arch = "wasm32")]
17mod wasm;
18#[cfg(target_arch = "wasm32")]
19pub use wasm::WasmCore;
20
21// Public exports for shared functionality
22pub use capabilities::server_capabilities;
23pub use queries::{LspDiagnostics, LspFileDiagnostics, LspSemanticTokens};
24pub use types::{CoreRequestId, Effect, LspError, LspOutput};
25
26use std::collections::{HashMap, HashSet};
27use std::path::PathBuf;
28
29use eure::query::{
30    CollectDiagnosticTargets, Glob, GlobResult, OpenDocuments, OpenDocumentsList, TextFile,
31    TextFileContent, Workspace, WorkspaceId, build_runtime,
32};
33use lsp_types::InitializeParams;
34use query_flow::{DurabilityLevel, QueryRuntime};
35
36use crate::types::{CommandQuery, CommandResult, FileDiagnosticsSubscription, PendingRequest};
37use crate::uri_utils::uri_to_text_file;
38
39use lsp_types::{
40    DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
41    InitializeResult, PublishDiagnosticsParams, SemanticTokensParams,
42    notification::{
43        DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument,
44        Notification as LspNotification, PublishDiagnostics,
45    },
46    request::{Initialize, Request as LspRequest, SemanticTokensFullRequest, Shutdown},
47};
48
49use crate::uri_utils::text_file_to_uri;
50use query_flow::QueryError;
51use serde_json::Value;
52
53// Cross-platform logging
54#[cfg(not(target_arch = "wasm32"))]
55use tracing::{debug, error};
56
57#[cfg(target_arch = "wasm32")]
58macro_rules! debug {
59    ($($arg:tt)*) => { web_sys::console::debug_1(&format!($($arg)*).into()) };
60}
61#[cfg(target_arch = "wasm32")]
62macro_rules! error {
63    ($($arg:tt)*) => { web_sys::console::error_1(&format!($($arg)*).into()) };
64}
65
66/// Register workspaces from LSP initialization parameters.
67pub fn register_workspaces_from_init(runtime: &mut QueryRuntime, params: &InitializeParams) {
68    if let Some(folders) = &params.workspace_folders {
69        for folder in folders {
70            let workspace_path = PathBuf::from(folder.uri.path().as_str());
71            let config_path = workspace_path.join("Eure.eure");
72
73            runtime.resolve_asset(
74                WorkspaceId(workspace_path.to_string_lossy().into_owned()),
75                Workspace {
76                    path: workspace_path,
77                    config_path,
78                },
79                DurabilityLevel::Static,
80            );
81        }
82    } else if let Some(root_uri) = {
83        #[allow(
84            deprecated,
85            reason = "fallback for clients without workspace_folders support"
86        )]
87        &params.root_uri
88    } {
89        let workspace_path = PathBuf::from(root_uri.path().as_str());
90        let config_path = workspace_path.join("Eure.eure");
91
92        runtime.resolve_asset(
93            WorkspaceId(workspace_path.to_string_lossy().into_owned()),
94            Workspace {
95                path: workspace_path,
96                config_path,
97            },
98            DurabilityLevel::Static,
99        );
100    }
101}
102
103/// The headless LSP core state machine.
104///
105/// This struct contains all the state and logic for the language server,
106/// independent of the platform-specific event loop. Both native and WASM
107/// implementations use this core.
108pub struct LspCore {
109    /// The query runtime for executing LSP queries.
110    runtime: QueryRuntime,
111    /// Pending requests waiting for assets to be resolved.
112    pending_requests: HashMap<CoreRequestId, PendingRequest>,
113    /// Files that have been requested but not yet resolved.
114    pending_assets: HashSet<TextFile>,
115    /// Glob patterns that have been requested but not yet resolved.
116    pending_globs: HashMap<String, Glob>,
117    /// Per-file diagnostics subscriptions with revision tracking.
118    diagnostics_subscriptions: HashMap<TextFile, FileDiagnosticsSubscription>,
119    /// URIs we've published diagnostics to (for stale clearing).
120    published_uris: HashSet<String>,
121    /// Cached content of open documents (keyed by URI string).
122    documents: HashMap<String, String>,
123    /// Whether the server has been initialized.
124    initialized: bool,
125}
126
127impl LspCore {
128    /// Create a new LspCore instance.
129    pub fn new() -> Self {
130        let runtime = build_runtime();
131
132        Self {
133            runtime,
134            pending_requests: HashMap::new(),
135            pending_assets: HashSet::new(),
136            pending_globs: HashMap::new(),
137            diagnostics_subscriptions: HashMap::new(),
138            published_uris: HashSet::new(),
139            documents: HashMap::new(),
140            initialized: false,
141        }
142    }
143
144    /// Get a mutable reference to the query runtime.
145    ///
146    /// This is useful for registering workspaces during initialization.
147    pub fn runtime_mut(&mut self) -> &mut QueryRuntime {
148        &mut self.runtime
149    }
150
151    /// Check if the server has been initialized.
152    pub fn is_initialized(&self) -> bool {
153        self.initialized
154    }
155
156    /// Mark the server as initialized.
157    pub fn set_initialized(&mut self) {
158        self.initialized = true;
159    }
160
161    /// Get pending files that need to be fetched.
162    pub fn pending_files(&self) -> impl Iterator<Item = &TextFile> {
163        self.pending_assets.iter()
164    }
165
166    /// Get pending glob patterns that need to be expanded.
167    pub fn pending_globs(&self) -> impl Iterator<Item = (&str, &Glob)> {
168        self.pending_globs.iter().map(|(k, v)| (k.as_str(), v))
169    }
170
171    // === Document Management ===
172
173    /// Update the OpenDocuments asset with current open documents.
174    ///
175    /// This should be called whenever documents are opened or closed to ensure
176    /// collection queries (`CollectDiagnosticTargets`, `CollectSchemaFiles`) are invalidated.
177    fn update_open_documents(&mut self) {
178        let files: Vec<TextFile> = self
179            .documents
180            .keys()
181            .filter_map(|uri| uri_to_text_file(uri).ok())
182            .collect();
183
184        self.runtime.resolve_asset(
185            OpenDocuments,
186            OpenDocumentsList(files),
187            DurabilityLevel::Volatile,
188        );
189    }
190
191    /// Open a document and cache its content.
192    ///
193    /// This should be called when a `textDocument/didOpen` notification is received.
194    pub fn open_document(&mut self, uri: &str, content: String) {
195        // Update document cache
196        self.documents.insert(uri.to_string(), content.clone());
197
198        // Resolve in query runtime
199        let Ok(file) = uri_to_text_file(uri) else {
200            return; // Invalid URI - skip
201        };
202        self.runtime
203            .resolve_asset(file, TextFileContent(content), DurabilityLevel::Volatile);
204
205        // Update open documents asset
206        self.update_open_documents();
207    }
208
209    /// Update a document's content.
210    ///
211    /// This should be called when a `textDocument/didChange` notification is received.
212    pub fn change_document(&mut self, uri: &str, content: String) {
213        // Same as open - we use full sync mode
214        self.open_document(uri, content);
215    }
216
217    /// Close a document and clear its cached content.
218    ///
219    /// This should be called when a `textDocument/didClose` notification is received.
220    pub fn close_document(&mut self, uri: &str) {
221        // Remove from document cache
222        self.documents.remove(uri);
223
224        // Invalidate in query runtime
225        if let Ok(file) = uri_to_text_file(uri) {
226            self.runtime.invalidate_asset(&file);
227        }
228
229        // Update open documents asset - this triggers re-evaluation of diagnostic targets
230        self.update_open_documents();
231    }
232
233    /// Get the cached content of a document.
234    pub fn get_document(&self, uri: &str) -> Option<&String> {
235        self.documents.get(uri)
236    }
237
238    // === Request Handling ===
239
240    /// Handle an LSP request.
241    ///
242    /// Returns outputs to send to the client and effects for the platform to perform.
243    pub fn handle_request(
244        &mut self,
245        id: CoreRequestId,
246        method: &str,
247        params: Value,
248    ) -> (Vec<LspOutput>, Vec<Effect>) {
249        let mut outputs = Vec::new();
250        let mut effects = Vec::new();
251
252        match method {
253            Initialize::METHOD => {
254                let init_params: InitializeParams = match serde_json::from_value(params) {
255                    Ok(p) => p,
256                    Err(e) => {
257                        outputs.push(LspOutput::Response {
258                            id,
259                            result: Err(LspError::invalid_params(format!("Invalid params: {}", e))),
260                        });
261                        return (outputs, effects);
262                    }
263                };
264
265                // Register workspaces from initialization
266                register_workspaces_from_init(&mut self.runtime, &init_params);
267
268                let result = InitializeResult {
269                    capabilities: server_capabilities(),
270                    server_info: Some(lsp_types::ServerInfo {
271                        name: "eure-ls".to_string(),
272                        version: Some(env!("CARGO_PKG_VERSION").to_string()),
273                    }),
274                };
275
276                self.initialized = true;
277                outputs.push(LspOutput::Response {
278                    id,
279                    result: Ok(serde_json::to_value(result).unwrap()),
280                });
281            }
282            Shutdown::METHOD => {
283                outputs.push(LspOutput::Response {
284                    id,
285                    result: Ok(Value::Null),
286                });
287            }
288            SemanticTokensFullRequest::METHOD => {
289                let params: SemanticTokensParams = match serde_json::from_value(params) {
290                    Ok(p) => p,
291                    Err(e) => {
292                        outputs.push(LspOutput::Response {
293                            id,
294                            result: Err(LspError::invalid_params(format!("Invalid params: {}", e))),
295                        });
296                        return (outputs, effects);
297                    }
298                };
299
300                let uri = params.text_document.uri;
301                let uri_str = uri.as_str();
302                let file = match uri_to_text_file(uri_str) {
303                    Ok(f) => f,
304                    Err(e) => {
305                        outputs.push(LspOutput::Response {
306                            id,
307                            result: Err(LspError::invalid_params(format!("Invalid URI: {}", e))),
308                        });
309                        return (outputs, effects);
310                    }
311                };
312                let source = self.documents.get(uri_str).cloned().unwrap_or_default();
313
314                let query = LspSemanticTokens::new(file, source.clone());
315                let command = CommandQuery::SemanticTokensFull(query);
316
317                match self.try_execute(&command) {
318                    Ok(result) => {
319                        let json = self.result_to_value(result);
320                        outputs.push(LspOutput::Response {
321                            id,
322                            result: Ok(json),
323                        });
324                    }
325                    Err(QueryError::Suspend { .. }) => {
326                        // Query is pending - collect effects and store request
327                        let (new_effects, waiting_for) = self.collect_pending_assets();
328                        effects.extend(new_effects);
329
330                        self.pending_requests.insert(
331                            id.clone(),
332                            PendingRequest {
333                                id,
334                                command,
335                                waiting_for,
336                            },
337                        );
338                    }
339                    Err(e) => {
340                        if let Some(lsp_err) = Self::handle_query_error("SemanticTokens", e) {
341                            outputs.push(LspOutput::Response {
342                                id,
343                                result: Err(lsp_err),
344                            });
345                        }
346                    }
347                }
348            }
349            _ => {
350                outputs.push(LspOutput::Response {
351                    id,
352                    result: Err(LspError::method_not_found(method)),
353                });
354            }
355        }
356
357        (outputs, effects)
358    }
359
360    /// Cancel a pending request.
361    pub fn cancel_request(&mut self, id: &CoreRequestId) {
362        self.pending_requests.remove(id);
363    }
364
365    // === Notification Handling ===
366
367    /// Handle an LSP notification.
368    ///
369    /// Returns outputs to send to the client and effects for the platform to perform.
370    pub fn handle_notification(
371        &mut self,
372        method: &str,
373        params: Value,
374    ) -> (Vec<LspOutput>, Vec<Effect>) {
375        let mut outputs = Vec::new();
376        let mut effects = Vec::new();
377
378        match method {
379            DidOpenTextDocument::METHOD => {
380                if let Ok(params) = serde_json::from_value::<DidOpenTextDocumentParams>(params) {
381                    let uri = params.text_document.uri;
382                    let content = params.text_document.text;
383
384                    // Open document in core
385                    self.open_document(uri.as_str(), content);
386
387                    // Refresh diagnostics for all targets
388                    let (diag_outputs, diag_effects) = self.refresh_diagnostics();
389                    outputs.extend(diag_outputs);
390                    effects.extend(diag_effects);
391                }
392            }
393            DidChangeTextDocument::METHOD => {
394                if let Ok(params) = serde_json::from_value::<DidChangeTextDocumentParams>(params) {
395                    let uri = params.text_document.uri;
396                    // We use FULL sync, so there's only one change with the full content
397                    if let Some(change) = params.content_changes.into_iter().next() {
398                        let content = change.text;
399
400                        // Change document in core
401                        self.change_document(uri.as_str(), content);
402
403                        // Refresh diagnostics for all targets
404                        let (diag_outputs, diag_effects) = self.refresh_diagnostics();
405                        outputs.extend(diag_outputs);
406                        effects.extend(diag_effects);
407                    }
408                }
409            }
410            DidCloseTextDocument::METHOD => {
411                if let Ok(params) = serde_json::from_value::<DidCloseTextDocumentParams>(params) {
412                    let uri = params.text_document.uri;
413                    let uri_str = uri.as_str();
414
415                    // Close document in core
416                    self.close_document(uri_str);
417
418                    // Also remove any pending requests for this document
419                    self.pending_requests
420                        .retain(|_, pending| match &pending.command {
421                            CommandQuery::SemanticTokensFull(q) => {
422                                let pending_uri = text_file_to_uri(&q.file);
423                                pending_uri != uri_str
424                            }
425                        });
426
427                    // Refresh diagnostics - stale files will be cleared automatically
428                    let (diag_outputs, diag_effects) = self.refresh_diagnostics();
429                    outputs.extend(diag_outputs);
430                    effects.extend(diag_effects);
431                }
432            }
433            "$/cancelRequest" => {
434                if let Some(id) = params.get("id") {
435                    let core_id = CoreRequestId::from(id);
436                    self.cancel_request(&core_id);
437                }
438            }
439            "initialized" | "exit" => {
440                // Ignore
441            }
442            _ => {
443                // Unknown notification - ignore
444            }
445        }
446
447        (outputs, effects)
448    }
449
450    /// Refresh diagnostics for all diagnostic targets.
451    ///
452    /// Uses `CollectDiagnosticTargets` to discover all files needing diagnostics,
453    /// then polls `LspFileDiagnostics` for each file with per-file revision tracking.
454    ///
455    /// Returns notifications for all changed files and any effects needed.
456    fn refresh_diagnostics(&mut self) -> (Vec<LspOutput>, Vec<Effect>) {
457        let mut outputs = Vec::new();
458        let mut effects = Vec::new();
459
460        debug!("[LspCore] refresh_diagnostics");
461
462        // 1. Collect all files to diagnose (includes open docs + schema files)
463        let all_files = match self.runtime.poll(CollectDiagnosticTargets::new()) {
464            Ok(polled) => match polled.value {
465                Ok(files) => files,
466                Err(e) => {
467                    error!("CollectDiagnosticTargets error: {}", e);
468                    return (outputs, effects);
469                }
470            },
471            Err(QueryError::Suspend { .. }) => {
472                debug!("[LspCore] CollectDiagnosticTargets suspended");
473                let (new_effects, _) = self.collect_pending_assets();
474                effects.extend(new_effects);
475                return (outputs, effects);
476            }
477            Err(e) => {
478                Self::handle_query_error("CollectDiagnosticTargets", e);
479                return (outputs, effects);
480            }
481        };
482
483        debug!("[LspCore] diagnostic targets: {} files", all_files.len());
484
485        // 2. Poll LspFileDiagnostics for each file
486        let mut current_uris = HashSet::new();
487        for file in all_files.iter() {
488            let query = LspFileDiagnostics::new(file.clone());
489
490            // Get or create subscription
491            let last_revision = self
492                .diagnostics_subscriptions
493                .get(file)
494                .map(|s| s.last_revision)
495                .unwrap_or_default();
496
497            match self.runtime.poll(query.clone()) {
498                Ok(polled) => {
499                    let uri = text_file_to_uri(file);
500                    current_uris.insert(uri.clone());
501
502                    // Only publish if revision changed
503                    if polled.revision != last_revision {
504                        // Update subscription
505                        self.diagnostics_subscriptions.insert(
506                            file.clone(),
507                            FileDiagnosticsSubscription {
508                                file: file.clone(),
509                                query,
510                                last_revision: polled.revision,
511                            },
512                        );
513
514                        match polled.value {
515                            Ok(diagnostics) => {
516                                debug!(
517                                    "[LspCore] sending {} diagnostics for {}",
518                                    diagnostics.len(),
519                                    uri
520                                );
521                                if let Ok(parsed_uri) = uri.parse::<lsp_types::Uri>() {
522                                    let params = PublishDiagnosticsParams {
523                                        uri: parsed_uri,
524                                        diagnostics: diagnostics.as_ref().clone(),
525                                        version: None,
526                                    };
527                                    outputs.push(LspOutput::Notification {
528                                        method: PublishDiagnostics::METHOD.to_string(),
529                                        params: serde_json::to_value(params).unwrap(),
530                                    });
531                                }
532                            }
533                            Err(e) => {
534                                error!("Diagnostics query error for {}: {}", uri, e);
535                                if let Ok(parsed_uri) = uri.parse::<lsp_types::Uri>() {
536                                    let params = PublishDiagnosticsParams {
537                                        uri: parsed_uri,
538                                        diagnostics: vec![],
539                                        version: None,
540                                    };
541                                    outputs.push(LspOutput::Notification {
542                                        method: PublishDiagnostics::METHOD.to_string(),
543                                        params: serde_json::to_value(params).unwrap(),
544                                    });
545                                }
546                            }
547                        }
548                    }
549                }
550                Err(QueryError::Suspend { .. }) => {
551                    debug!("[LspCore] diagnostics for {:?} suspended", file);
552                    // Store subscription for retry
553                    self.diagnostics_subscriptions.insert(
554                        file.clone(),
555                        FileDiagnosticsSubscription {
556                            file: file.clone(),
557                            query,
558                            last_revision,
559                        },
560                    );
561                    let (new_effects, _) = self.collect_pending_assets();
562                    effects.extend(new_effects);
563                }
564                Err(e) => {
565                    Self::handle_query_error(&format!("LspFileDiagnostics({:?})", file), e);
566                }
567            }
568        }
569
570        // 3. Clear stale diagnostics for files no longer in target set
571        let stale: Vec<_> = self
572            .published_uris
573            .difference(&current_uris)
574            .cloned()
575            .collect();
576        for uri in stale {
577            debug!("[LspCore] clearing stale diagnostics for {}", uri);
578            if let Ok(parsed_uri) = uri.parse::<lsp_types::Uri>() {
579                let params = PublishDiagnosticsParams {
580                    uri: parsed_uri,
581                    diagnostics: vec![],
582                    version: None,
583                };
584                outputs.push(LspOutput::Notification {
585                    method: PublishDiagnostics::METHOD.to_string(),
586                    params: serde_json::to_value(params).unwrap(),
587                });
588            }
589        }
590        self.published_uris = current_uris;
591
592        // 4. Remove subscriptions for files no longer tracked
593        self.diagnostics_subscriptions
594            .retain(|f, _| all_files.contains(f));
595
596        (outputs, effects)
597    }
598
599    // === Asset Resolution ===
600
601    /// Resolve a file asset with its content.
602    ///
603    /// Returns outputs (responses, notifications) and effects for any newly pending assets.
604    pub fn resolve_file(
605        &mut self,
606        file: TextFile,
607        content: Result<String, String>,
608    ) -> (Vec<LspOutput>, Vec<Effect>) {
609        // Resolve in runtime
610        match content {
611            Ok(text) => {
612                self.runtime.resolve_asset(
613                    file.clone(),
614                    TextFileContent(text),
615                    DurabilityLevel::Volatile,
616                );
617            }
618            Err(error) => {
619                self.runtime.resolve_asset_error::<TextFile>(
620                    file.clone(),
621                    anyhow::anyhow!("{}", error),
622                    DurabilityLevel::Volatile,
623                );
624            }
625        }
626        self.pending_assets.remove(&file);
627
628        // Process pending requests and diagnostics
629        self.process_after_asset_change()
630    }
631
632    /// Resolve a glob pattern with matching files.
633    ///
634    /// Returns outputs (responses, notifications) and effects for any newly pending assets.
635    pub fn resolve_glob(
636        &mut self,
637        id: &str,
638        files: Vec<TextFile>,
639    ) -> (Vec<LspOutput>, Vec<Effect>) {
640        if let Some(glob_key) = self.pending_globs.remove(id) {
641            self.runtime
642                .resolve_asset(glob_key, GlobResult(files), DurabilityLevel::Volatile);
643        }
644
645        // Process pending requests and diagnostics
646        self.process_after_asset_change()
647    }
648
649    /// Process pending requests and diagnostics after an asset is resolved.
650    fn process_after_asset_change(&mut self) -> (Vec<LspOutput>, Vec<Effect>) {
651        let mut outputs = Vec::new();
652        let mut effects = Vec::new();
653
654        // Retry pending requests
655        let (req_outputs, req_effects) = self.retry_pending_requests();
656        outputs.extend(req_outputs);
657        effects.extend(req_effects);
658
659        // Check diagnostics subscriptions
660        let (diag_outputs, diag_effects) = self.check_diagnostics_subscriptions();
661        outputs.extend(diag_outputs);
662        effects.extend(diag_effects);
663
664        (outputs, effects)
665    }
666
667    /// Retry pending requests after an asset was resolved.
668    fn retry_pending_requests(&mut self) -> (Vec<LspOutput>, Vec<Effect>) {
669        let mut outputs = Vec::new();
670        let mut effects = Vec::new();
671
672        let request_ids: Vec<CoreRequestId> = self.pending_requests.keys().cloned().collect();
673        let mut completed_ids = Vec::new();
674
675        for id in request_ids {
676            if let Some(pending) = self.pending_requests.get(&id) {
677                let command = pending.command.clone();
678
679                match self.try_execute(&command) {
680                    Ok(result) => {
681                        let json = self.result_to_value(result);
682                        outputs.push(LspOutput::Response {
683                            id: id.clone(),
684                            result: Ok(json),
685                        });
686                        completed_ids.push(id);
687                    }
688                    Err(QueryError::Suspend { .. }) => {
689                        // Still waiting - collect more effects
690                        let (new_effects, _) = self.collect_pending_assets();
691                        effects.extend(new_effects);
692                    }
693                    Err(e) => {
694                        if let Some(lsp_err) = Self::handle_query_error("RetryQuery", e) {
695                            outputs.push(LspOutput::Response {
696                                id: id.clone(),
697                                result: Err(lsp_err),
698                            });
699                            completed_ids.push(id);
700                        }
701                    }
702                }
703            }
704        }
705
706        for id in completed_ids {
707            self.pending_requests.remove(&id);
708        }
709
710        (outputs, effects)
711    }
712
713    /// Check diagnostics subscriptions and send updates.
714    ///
715    /// This simply calls `refresh_diagnostics` to re-poll all targets.
716    fn check_diagnostics_subscriptions(&mut self) -> (Vec<LspOutput>, Vec<Effect>) {
717        self.refresh_diagnostics()
718    }
719
720    // === Internal Helpers ===
721
722    /// Log a QueryError and convert it to an LspError.
723    /// Returns None for Suspend (should be handled separately).
724    fn handle_query_error(context: &str, err: QueryError) -> Option<LspError> {
725        match err {
726            QueryError::Suspend { .. } => None,
727            QueryError::Cancelled => {
728                error!("{}: query unexpectedly cancelled", context);
729                Some(LspError::internal_error("Query cancelled"))
730            }
731            QueryError::DependenciesRemoved { missing_keys } => {
732                error!("{}: dependencies removed: {:?}", context, missing_keys);
733                Some(LspError::internal_error("Dependencies removed"))
734            }
735            QueryError::Cycle { path } => {
736                error!("{}: query cycle: {:?}", context, path);
737                Some(LspError::internal_error(format!("Query cycle: {:?}", path)))
738            }
739            QueryError::InconsistentAssetResolution => {
740                unreachable!("InconsistentAssetResolution should not occur")
741            }
742            QueryError::UserError(e) => {
743                error!("{}: unexpected user error: {}", context, e);
744                Some(LspError::internal_error(e.to_string()))
745            }
746        }
747    }
748
749    /// Try to execute a command query.
750    fn try_execute(&mut self, command: &CommandQuery) -> Result<CommandResult, QueryError> {
751        match command {
752            CommandQuery::SemanticTokensFull(query) => {
753                let result = self.runtime.query(query.clone())?;
754                Ok(CommandResult::SemanticTokens(Some((*result).clone())))
755            }
756        }
757    }
758
759    /// Convert a command result to a JSON value.
760    fn result_to_value(&self, result: CommandResult) -> Value {
761        match result {
762            CommandResult::SemanticTokens(tokens) => {
763                serde_json::to_value(tokens).unwrap_or(Value::Null)
764            }
765        }
766    }
767
768    /// Collect pending assets and return effects for the platform to handle.
769    fn collect_pending_assets(&mut self) -> (Vec<Effect>, HashSet<TextFile>) {
770        let mut effects = Vec::new();
771        let mut waiting_for = HashSet::new();
772
773        for pending in self.runtime.pending_assets() {
774            if let Some(file) = pending.key::<TextFile>() {
775                if !self.pending_assets.contains(file) {
776                    self.pending_assets.insert(file.clone());
777                    effects.push(Effect::FetchFile(file.clone()));
778                }
779                waiting_for.insert(file.clone());
780            } else if let Some(glob_key) = pending.key::<Glob>() {
781                // Generate a unique ID for this glob request
782                let id = format!(
783                    "{}:{}",
784                    glob_key.base_dir.to_string_lossy(),
785                    glob_key.pattern
786                );
787                if !self.pending_globs.contains_key(&id) {
788                    self.pending_globs.insert(id.clone(), glob_key.clone());
789                    effects.push(Effect::ExpandGlob {
790                        id,
791                        glob: glob_key.clone(),
792                    });
793                }
794            }
795        }
796
797        (effects, waiting_for)
798    }
799}
800
801impl Default for LspCore {
802    fn default() -> Self {
803        Self::new()
804    }
805}