1use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::Duration;
6
7use dashmap::DashMap;
8use gruel_compiler::PreviewFeatures;
9use gruel_manifest::Manifest;
10use gruel_target::Target;
11use lsp_types::{
12 CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability, CodeActionResponse,
13 CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
14 Diagnostic, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
15 DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
16 DocumentFormattingParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents,
17 HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams,
18 InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams, Location, MarkupContent, MarkupKind,
19 MessageType, OneOf, ParameterInformation, ParameterLabel, Position, PositionEncodingKind,
20 Range, ReferenceParams, ServerCapabilities, ServerInfo, SignatureHelp, SignatureHelpOptions,
21 SignatureHelpParams, SignatureInformation, SymbolInformation, SymbolKind as LspSymbolKind,
22 TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkspaceSymbolParams,
23};
24use rustc_hash::{FxHashMap, FxHashSet};
25use tokio::sync::Mutex;
26use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
27use tokio_util::sync::CancellationToken;
28use tower_lsp::{Client, LanguageServer, LspService, Server, jsonrpc};
29
30use crate::analysis::{self, Snapshot, WorkspaceFile};
31use crate::diagnostics;
32use crate::document::DocState;
33use crate::position::PositionEncoding;
34
35const DEFAULT_DEBOUNCE: Duration = Duration::from_millis(150);
37const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
39
40pub struct Backend {
42 pub client: Client,
43 pub docs: Arc<DashMap<Url, DocState>>,
44 pub snapshots: Arc<DashMap<Url, Arc<Snapshot>>>,
50 pub preview_features: PreviewFeatures,
51 pub workspace_root: Arc<Mutex<Option<PathBuf>>>,
52 pub manifest: Arc<Mutex<Option<Manifest>>>,
56 pub encoding: Arc<Mutex<PositionEncoding>>,
57 pub analysis_tx: UnboundedSender<AnalysisRequest>,
58 pub current_cancel: Arc<Mutex<Option<CancellationToken>>>,
59 pub last_diagnostics: Arc<DashMap<Url, Vec<Diagnostic>>>,
64}
65
66#[derive(Debug, Clone)]
67pub struct AnalysisRequest {
68 pub debounce: Duration,
69 pub timeout: Duration,
70}
71
72impl Backend {
73 pub fn new(client: Client, preview_features: PreviewFeatures) -> Self {
74 let (tx, rx) = unbounded_channel();
75 let me = Self {
76 client: client.clone(),
77 docs: Arc::new(DashMap::new()),
78 snapshots: Arc::new(DashMap::new()),
79 preview_features,
80 workspace_root: Arc::new(Mutex::new(None)),
81 manifest: Arc::new(Mutex::new(None)),
82 encoding: Arc::new(Mutex::new(PositionEncoding::Utf16)),
83 analysis_tx: tx,
84 current_cancel: Arc::new(Mutex::new(None)),
85 last_diagnostics: Arc::new(DashMap::new()),
86 };
87 let worker = AnalysisWorker {
89 client,
90 docs: me.docs.clone(),
91 snapshots: me.snapshots.clone(),
92 preview_features: me.preview_features.clone(),
93 workspace_root: me.workspace_root.clone(),
94 manifest: me.manifest.clone(),
95 encoding: me.encoding.clone(),
96 current_cancel: me.current_cancel.clone(),
97 rx,
98 target: Target::host(),
99 published_files: DashMap::new(),
100 last_diagnostics: me.last_diagnostics.clone(),
101 };
102 tokio::spawn(worker.run());
103 me
104 }
105
106 fn queue_analysis(&self) {
107 let _ = self.analysis_tx.send(AnalysisRequest {
108 debounce: DEFAULT_DEBOUNCE,
109 timeout: DEFAULT_TIMEOUT,
110 });
111 }
112
113 pub async fn analyze_now(&self) -> Vec<Diagnostic> {
117 let workspace_root = self.workspace_root.lock().await.clone();
118 let manifest = self.manifest.lock().await.clone();
119 let target = Target::host();
120 let mut combined_diagnostics: FxHashSet<UriDiagKey> = FxHashSet::default();
121 let mut by_uri: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default();
122
123 let roots = collect_analysis_roots(&self.docs, manifest.as_ref());
124
125 for (uri, root) in roots {
126 let docs = self.docs.clone();
127 let result = analysis::analyze_root(
128 root,
129 workspace_root.as_deref(),
130 &self.preview_features,
131 &target,
132 |path| open_text_lookup(&docs, path),
133 );
134 if let Some(snap) = result.snapshot {
135 self.snapshots.insert(uri.clone(), Arc::new(snap));
136 }
137 let by_file = diagnostics::group_by_file(
138 result.diagnostics.into_iter(),
139 workspace_root.as_deref(),
140 );
141 for (path, diags) in by_file {
142 if let Ok(diag_uri) = Url::from_file_path(&path) {
143 for d in &diags {
144 let key = (
145 diag_uri.clone(),
146 range_key(&d.range),
147 d.message.clone(),
148 d.code
149 .as_ref()
150 .map(|c| match c {
151 lsp_types::NumberOrString::String(s) => s.clone(),
152 lsp_types::NumberOrString::Number(n) => n.to_string(),
153 })
154 .unwrap_or_default(),
155 );
156 if combined_diagnostics.insert(key) {
157 by_uri.entry(diag_uri.clone()).or_default().push(d.clone());
158 }
159 }
160 }
161 }
162 }
163
164 let mut flat = Vec::new();
165 for (uri, diags) in by_uri {
166 self.client
167 .publish_diagnostics(uri.clone(), diags.clone(), None)
168 .await;
169 self.last_diagnostics.insert(uri, diags.clone());
170 flat.extend(diags);
171 }
172 flat
173 }
174}
175
176fn collect_roots(docs: &DashMap<Url, DocState>) -> Vec<(Url, WorkspaceFile)> {
177 docs.iter()
178 .enumerate()
179 .map(|(idx, kv)| {
180 let doc = kv.value();
181 let file = WorkspaceFile {
182 path: doc.path.clone(),
183 text: doc.text.clone(),
184 file_id: gruel_compiler::FileId::new((idx as u32).saturating_add(1).max(1)),
185 };
186 (kv.key().clone(), file)
187 })
188 .collect()
189}
190
191pub(crate) fn collect_analysis_roots(
197 docs: &DashMap<Url, DocState>,
198 manifest: Option<&Manifest>,
199) -> Vec<(Url, WorkspaceFile)> {
200 if let Some(m) = manifest {
201 let entry_path = m.target.root().to_path_buf();
202 let text = match open_text_lookup(docs, &entry_path) {
203 Some(t) => t,
204 None => match std::fs::read_to_string(&entry_path) {
205 Ok(t) => t,
206 Err(_) => {
207 return Vec::new();
212 }
213 },
214 };
215 let uri = match Url::from_file_path(&entry_path) {
216 Ok(u) => u,
217 Err(_) => return Vec::new(),
218 };
219 let file = WorkspaceFile {
220 path: entry_path,
221 text,
222 file_id: gruel_compiler::FileId::new(1),
223 };
224 vec![(uri, file)]
225 } else {
226 collect_roots(docs)
227 }
228}
229
230type RangeKey = (u32, u32, u32, u32);
234
235type UriDiagKey = (Url, RangeKey, String, String);
237
238type PathDiagKey = (PathBuf, RangeKey, String, String);
240
241fn range_key(r: &lsp_types::Range) -> RangeKey {
242 (r.start.line, r.start.character, r.end.line, r.end.character)
243}
244
245fn open_text_lookup(docs: &DashMap<Url, DocState>, path: &std::path::Path) -> Option<String> {
246 for kv in docs.iter() {
247 if kv.value().path == path {
248 return Some(kv.value().text.clone());
249 }
250 }
251 None
252}
253
254impl Backend {
255 pub async fn reload_manifest(&self, root: &std::path::Path) {
260 let Some(path) = gruel_manifest::discover_at_root(root) else {
261 *self.manifest.lock().await = None;
262 return;
263 };
264 match gruel_manifest::load_at(&path) {
265 Ok(m) => {
266 self.client
267 .log_message(
268 MessageType::INFO,
269 format!("gruel-lsp: loaded manifest at {}", path.display()),
270 )
271 .await;
272 *self.manifest.lock().await = Some(m);
273 }
274 Err(err) => {
275 self.client
276 .log_message(
277 MessageType::WARNING,
278 format!("gruel-lsp: invalid manifest at {}: {}", path.display(), err),
279 )
280 .await;
281 *self.manifest.lock().await = None;
282 }
283 }
284 }
285
286 fn snapshot_for(&self, uri: &Url) -> Option<Arc<Snapshot>> {
292 if let Some(snap) = self.snapshots.get(uri) {
293 return Some(snap.value().clone());
294 }
295 let path = uri.to_file_path().ok()?;
296 for kv in self.snapshots.iter() {
297 if kv.value().path_to_file_id.contains_key(&path) {
298 return Some(kv.value().clone());
299 }
300 }
301 None
302 }
303
304 fn all_snapshots(&self) -> Vec<Arc<Snapshot>> {
307 self.snapshots.iter().map(|kv| kv.value().clone()).collect()
308 }
309}
310
311struct AnalysisWorker {
312 client: Client,
313 docs: Arc<DashMap<Url, DocState>>,
314 snapshots: Arc<DashMap<Url, Arc<Snapshot>>>,
315 preview_features: PreviewFeatures,
316 workspace_root: Arc<Mutex<Option<PathBuf>>>,
317 manifest: Arc<Mutex<Option<Manifest>>>,
321 #[allow(dead_code)]
325 encoding: Arc<Mutex<PositionEncoding>>,
326 current_cancel: Arc<Mutex<Option<CancellationToken>>>,
327 rx: tokio::sync::mpsc::UnboundedReceiver<AnalysisRequest>,
328 target: Target,
329 published_files: DashMap<Url, ()>,
332 last_diagnostics: Arc<DashMap<Url, Vec<Diagnostic>>>,
334}
335
336impl AnalysisWorker {
337 async fn run(mut self) {
338 while let Some(req) = self.rx.recv().await {
339 let debounce = req.debounce;
341 let timeout = req.timeout;
342 tokio::time::sleep(debounce).await;
344 while let Ok(_extra) = self.rx.try_recv() {
345 tokio::time::sleep(debounce).await;
346 }
347
348 let token = CancellationToken::new();
350 {
351 let mut cur = self.current_cancel.lock().await;
352 if let Some(old) = cur.replace(token.clone()) {
353 old.cancel();
354 }
355 }
356
357 let workspace_root = self.workspace_root.lock().await.clone();
358 let manifest = self.manifest.lock().await.clone();
359 let roots = collect_analysis_roots(&self.docs, manifest.as_ref());
360 let preview_features = self.preview_features.clone();
361 let target = self.target.clone();
362 let docs_for_lookup = self.docs.clone();
363 let workspace_root_for_task = workspace_root.clone();
364
365 let analysis = tokio::task::spawn_blocking(move || {
371 let mut snapshots: Vec<(Url, Snapshot)> = Vec::new();
372 let mut all_by_file: FxHashMap<PathBuf, Vec<Diagnostic>> = FxHashMap::default();
373 let mut seen_keys: FxHashSet<PathDiagKey> = FxHashSet::default();
374
375 for (uri, root) in roots {
376 let result = analysis::analyze_root(
377 root,
378 workspace_root_for_task.as_deref(),
379 &preview_features,
380 &target,
381 |path| open_text_lookup(&docs_for_lookup, path),
382 );
383 if let Some(snap) = result.snapshot {
384 snapshots.push((uri, snap));
385 }
386 let by_file = diagnostics::group_by_file(
387 result.diagnostics.into_iter(),
388 workspace_root_for_task.as_deref(),
389 );
390 for (path, diags) in by_file {
391 for d in diags {
392 let key = (
393 path.clone(),
394 range_key(&d.range),
395 d.message.clone(),
396 d.code
397 .as_ref()
398 .map(|c| match c {
399 lsp_types::NumberOrString::String(s) => s.clone(),
400 lsp_types::NumberOrString::Number(n) => n.to_string(),
401 })
402 .unwrap_or_default(),
403 );
404 if seen_keys.insert(key) {
405 all_by_file.entry(path.clone()).or_default().push(d);
406 }
407 }
408 }
409 }
410 (snapshots, all_by_file)
411 });
412 let (snapshots, by_file) = tokio::select! {
413 res = analysis => match res {
414 Ok(r) => r,
415 Err(_) => continue,
416 },
417 _ = tokio::time::sleep(timeout) => {
418 self.client
420 .log_message(
421 MessageType::WARNING,
422 "gruel-lsp: compile timed out, keeping previous snapshots",
423 )
424 .await;
425 continue;
426 }
427 _ = token.cancelled() => continue,
428 };
429
430 let mut live_root_uris: FxHashSet<Url> =
435 self.docs.iter().map(|kv| kv.key().clone()).collect();
436 if let Some(m) = manifest.as_ref()
437 && let Ok(u) = Url::from_file_path(m.target.root())
438 {
439 live_root_uris.insert(u);
440 }
441 let stale_snapshot_uris: Vec<Url> = self
442 .snapshots
443 .iter()
444 .filter(|kv| !live_root_uris.contains(kv.key()))
445 .map(|kv| kv.key().clone())
446 .collect();
447 for uri in stale_snapshot_uris {
448 self.snapshots.remove(&uri);
449 }
450
451 for (uri, snap) in snapshots {
453 self.snapshots.insert(uri, Arc::new(snap));
454 }
455
456 let mut current_files = std::collections::HashSet::new();
459 for path in by_file.keys() {
460 if let Ok(uri) = Url::from_file_path(path) {
461 current_files.insert(uri);
462 }
463 }
464 let previously_published: Vec<Url> = self
465 .published_files
466 .iter()
467 .map(|kv| kv.key().clone())
468 .collect();
469 for uri in previously_published {
470 if !current_files.contains(&uri) {
471 self.client
472 .publish_diagnostics(uri.clone(), vec![], None)
473 .await;
474 self.published_files.remove(&uri);
475 self.last_diagnostics.remove(&uri);
476 }
477 }
478 for kv in self.docs.iter() {
481 let uri = kv.key().clone();
482 if !current_files.contains(&uri) {
483 self.client
484 .publish_diagnostics(uri.clone(), vec![], None)
485 .await;
486 self.published_files.remove(&uri);
487 self.last_diagnostics.remove(&uri);
488 }
489 }
490
491 for (path, diags) in by_file {
492 if let Ok(uri) = Url::from_file_path(&path) {
493 self.client
494 .publish_diagnostics(uri.clone(), diags.clone(), None)
495 .await;
496 self.published_files.insert(uri.clone(), ());
497 self.last_diagnostics.insert(uri, diags);
498 }
499 }
500 }
501 }
502}
503
504#[tower_lsp::async_trait]
505impl LanguageServer for Backend {
506 async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result<InitializeResult> {
507 let chosen_encoding = pick_encoding(¶ms);
509 *self.encoding.lock().await = chosen_encoding;
510 let encoding_kind = match chosen_encoding {
511 PositionEncoding::Utf8 => PositionEncodingKind::UTF8,
512 PositionEncoding::Utf16 => PositionEncodingKind::UTF16,
513 };
514
515 let root_path = params
519 .workspace_folders
520 .as_ref()
521 .and_then(|fs| fs.first())
522 .and_then(|f| f.uri.to_file_path().ok())
523 .or_else(|| {
524 #[allow(deprecated)]
525 params.root_uri.as_ref().and_then(|u| u.to_file_path().ok())
526 })
527 .or_else(|| std::env::var_os("GRUEL_LSP_ROOT").map(PathBuf::from));
528
529 if let Some(root) = root_path.as_deref() {
533 self.reload_manifest(root).await;
534 }
535 *self.workspace_root.lock().await = root_path;
536
537 Ok(InitializeResult {
538 capabilities: ServerCapabilities {
539 position_encoding: Some(encoding_kind),
540 text_document_sync: Some(TextDocumentSyncCapability::Kind(
541 TextDocumentSyncKind::INCREMENTAL,
542 )),
543 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
544 hover_provider: Some(HoverProviderCapability::Simple(true)),
545 definition_provider: Some(OneOf::Left(true)),
546 signature_help_provider: Some(SignatureHelpOptions {
547 trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
548 retrigger_characters: None,
549 work_done_progress_options: Default::default(),
550 }),
551 references_provider: Some(OneOf::Left(true)),
552 workspace_symbol_provider: Some(OneOf::Left(true)),
553 completion_provider: Some(CompletionOptions {
554 resolve_provider: Some(false),
555 trigger_characters: Some(vec![
556 ".".to_string(),
557 "@".to_string(),
558 ":".to_string(),
559 "(".to_string(),
560 ]),
561 all_commit_characters: None,
562 work_done_progress_options: Default::default(),
563 completion_item: None,
564 }),
565 inlay_hint_provider: Some(OneOf::Left(true)),
566 document_formatting_provider: Some(OneOf::Left(true)),
567 ..ServerCapabilities::default()
568 },
569 server_info: Some(ServerInfo {
570 name: "gruel-lsp".to_string(),
571 version: Some(env!("CARGO_PKG_VERSION").to_string()),
572 }),
573 })
574 }
575
576 async fn initialized(&self, _: InitializedParams) {
577 self.client
578 .log_message(MessageType::INFO, "gruel-lsp ready")
579 .await;
580 self.queue_analysis();
581 }
582
583 async fn shutdown(&self) -> jsonrpc::Result<()> {
584 Ok(())
585 }
586
587 async fn did_open(&self, params: DidOpenTextDocumentParams) {
588 let doc = params.text_document;
589 let state = DocState::new(doc.uri.clone(), doc.text, doc.version, true);
590 self.docs.insert(doc.uri, state);
591 self.queue_analysis();
592 }
593
594 async fn did_change(&self, params: DidChangeTextDocumentParams) {
595 let encoding = *self.encoding.lock().await;
596 if let Some(mut entry) = self.docs.get_mut(¶ms.text_document.uri) {
597 for change in params.content_changes {
598 if !entry.apply_change(change, encoding) {
599 self.client
600 .log_message(
601 MessageType::WARNING,
602 format!("gruel-lsp: invalid range in {}", entry.uri),
603 )
604 .await;
605 }
606 }
607 entry.version = params.text_document.version;
608 }
609 self.queue_analysis();
610 }
611
612 async fn did_save(&self, _params: DidSaveTextDocumentParams) {
613 self.queue_analysis();
614 }
615
616 async fn did_close(&self, params: DidCloseTextDocumentParams) {
617 let uri = params.text_document.uri;
618 self.snapshots.remove(&uri);
627 if let Some(mut entry) = self.docs.get_mut(&uri) {
628 entry.open = false;
629 }
630 }
631
632 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
633 let manifest_changed = params.changes.iter().any(|change| {
638 change
639 .uri
640 .to_file_path()
641 .ok()
642 .and_then(|p| p.file_name().map(|n| n == "gruel.json"))
643 .unwrap_or(false)
644 });
645 if !manifest_changed {
646 return;
647 }
648 let root = self.workspace_root.lock().await.clone();
649 if let Some(root) = root {
650 self.reload_manifest(&root).await;
651 } else {
652 *self.manifest.lock().await = None;
653 }
654 self.snapshots.clear();
656 self.queue_analysis();
657 }
658
659 async fn hover(&self, params: HoverParams) -> jsonrpc::Result<Option<Hover>> {
660 let uri = params
661 .text_document_position_params
662 .text_document
663 .uri
664 .clone();
665 let position = params.text_document_position_params.position;
666 let encoding = *self.encoding.lock().await;
667
668 let snap = match self.snapshot_for(&uri) {
670 Some(s) => s,
671 None => return Ok(None),
672 };
673 let path = match uri.to_file_path() {
674 Ok(p) => p,
675 Err(_) => return Ok(None),
676 };
677 let file_id = match snap.path_to_file_id.get(&path) {
678 Some(id) => *id,
679 None => return Ok(None),
680 };
681 let source = match snap.sources.get(&file_id) {
682 Some(s) => s,
683 None => return Ok(None),
684 };
685 let line_map = match snap.line_maps.get(&file_id) {
686 Some(m) => m,
687 None => return Ok(None),
688 };
689 let byte = crate::position::position_to_byte(line_map, &source.text, position, encoding);
690
691 let hover = match crate::hover::hover_at_with_expr_types(
692 &snap.ast,
693 &snap.interner,
694 &snap.expr_types,
695 snap.type_pool.as_deref(),
696 file_id,
697 byte,
698 ) {
699 Some(h) => h,
700 None => return Ok(None),
701 };
702
703 let range = crate::position::span_to_range(line_map, &source.text, hover.span, encoding);
704
705 Ok(Some(Hover {
706 contents: HoverContents::Markup(MarkupContent {
707 kind: MarkupKind::Markdown,
708 value: hover.markdown,
709 }),
710 range: Some(range),
711 }))
712 }
713
714 async fn goto_definition(
715 &self,
716 params: GotoDefinitionParams,
717 ) -> jsonrpc::Result<Option<GotoDefinitionResponse>> {
718 let uri = params
719 .text_document_position_params
720 .text_document
721 .uri
722 .clone();
723 let position = params.text_document_position_params.position;
724 let encoding = *self.encoding.lock().await;
725
726 let snap = match self.snapshot_for(&uri) {
727 Some(s) => s,
728 None => return Ok(None),
729 };
730 let path = match uri.to_file_path() {
731 Ok(p) => p,
732 Err(_) => return Ok(None),
733 };
734 let file_id = match snap.path_to_file_id.get(&path) {
735 Some(id) => *id,
736 None => return Ok(None),
737 };
738 let source = match snap.sources.get(&file_id) {
739 Some(s) => s,
740 None => return Ok(None),
741 };
742 let line_map = match snap.line_maps.get(&file_id) {
743 Some(m) => m,
744 None => return Ok(None),
745 };
746 let byte = crate::position::position_to_byte(line_map, &source.text, position, encoding);
747
748 let def_span = match crate::goto::definition_at(&snap.ast, &snap.interner, file_id, byte) {
749 Some(s) => s,
750 None => return Ok(None),
751 };
752
753 let def_file_id = def_span.file_id;
756 let def_source = match snap.sources.get(&def_file_id) {
757 Some(s) => s,
758 None => return Ok(None),
759 };
760 let def_line_map = match snap.line_maps.get(&def_file_id) {
761 Some(m) => m,
762 None => return Ok(None),
763 };
764 let def_uri = match Url::from_file_path(&def_source.path) {
765 Ok(u) => u,
766 Err(_) => return Ok(None),
767 };
768 let range =
769 crate::position::span_to_range(def_line_map, &def_source.text, def_span, encoding);
770
771 Ok(Some(GotoDefinitionResponse::Scalar(Location {
772 uri: def_uri,
773 range,
774 })))
775 }
776
777 async fn signature_help(
778 &self,
779 params: SignatureHelpParams,
780 ) -> jsonrpc::Result<Option<SignatureHelp>> {
781 let uri = params
782 .text_document_position_params
783 .text_document
784 .uri
785 .clone();
786 let position = params.text_document_position_params.position;
787 let encoding = *self.encoding.lock().await;
788
789 let snap = match self.snapshot_for(&uri) {
790 Some(s) => s,
791 None => return Ok(None),
792 };
793 let path = match uri.to_file_path() {
794 Ok(p) => p,
795 Err(_) => return Ok(None),
796 };
797 let file_id = match snap.path_to_file_id.get(&path) {
798 Some(id) => *id,
799 None => return Ok(None),
800 };
801 let source = match snap.sources.get(&file_id) {
802 Some(s) => s,
803 None => return Ok(None),
804 };
805 let line_map = match snap.line_maps.get(&file_id) {
806 Some(m) => m,
807 None => return Ok(None),
808 };
809 let byte = crate::position::position_to_byte(line_map, &source.text, position, encoding);
810
811 let result =
812 match crate::signature::signature_help(&snap.ast, &snap.interner, file_id, byte) {
813 Some(r) => r,
814 None => return Ok(None),
815 };
816
817 let parameters = result
818 .parameters
819 .iter()
820 .map(|(s, e)| ParameterInformation {
821 label: ParameterLabel::LabelOffsets([*s, *e]),
822 documentation: None,
823 })
824 .collect();
825
826 Ok(Some(SignatureHelp {
827 signatures: vec![SignatureInformation {
828 label: result.label,
829 documentation: None,
830 parameters: Some(parameters),
831 active_parameter: Some(result.active_parameter as u32),
832 }],
833 active_signature: Some(0),
834 active_parameter: Some(result.active_parameter as u32),
835 }))
836 }
837
838 async fn references(&self, params: ReferenceParams) -> jsonrpc::Result<Option<Vec<Location>>> {
839 let uri = params.text_document_position.text_document.uri.clone();
840 let position = params.text_document_position.position;
841 let include_decl = params.context.include_declaration;
842 let encoding = *self.encoding.lock().await;
843
844 let target_path = match uri.to_file_path() {
845 Ok(p) => p,
846 Err(_) => return Ok(None),
847 };
848
849 let mut locations = Vec::new();
854 let mut seen: FxHashSet<(Url, RangeKey)> = FxHashSet::default();
855 for snap in self.all_snapshots() {
856 let Some(&file_id) = snap.path_to_file_id.get(&target_path) else {
857 continue;
858 };
859 let Some(source) = snap.sources.get(&file_id) else {
860 continue;
861 };
862 let Some(line_map) = snap.line_maps.get(&file_id) else {
863 continue;
864 };
865 let byte =
866 crate::position::position_to_byte(line_map, &source.text, position, encoding);
867 let spans = crate::references::references_at(
868 &snap.ast,
869 &snap.interner,
870 file_id,
871 byte,
872 include_decl,
873 );
874 for s in spans {
875 let Some(src) = snap.sources.get(&s.file_id) else {
876 continue;
877 };
878 let Some(lm) = snap.line_maps.get(&s.file_id) else {
879 continue;
880 };
881 let Ok(loc_uri) = Url::from_file_path(&src.path) else {
882 continue;
883 };
884 let range = crate::position::span_to_range(lm, &src.text, s, encoding);
885 if seen.insert((loc_uri.clone(), range_key(&range))) {
886 locations.push(Location {
887 uri: loc_uri,
888 range,
889 });
890 }
891 }
892 }
893
894 if locations.is_empty() {
895 Ok(None)
896 } else {
897 Ok(Some(locations))
898 }
899 }
900
901 async fn symbol(
902 &self,
903 params: WorkspaceSymbolParams,
904 ) -> jsonrpc::Result<Option<Vec<SymbolInformation>>> {
905 let encoding = *self.encoding.lock().await;
906 let mut out: Vec<SymbolInformation> = Vec::new();
910 let mut seen: FxHashSet<(Url, RangeKey, String)> = FxHashSet::default();
911 for snap in self.all_snapshots() {
912 let syms = crate::workspace_symbols::workspace_symbols(
913 &snap.ast,
914 &snap.interner,
915 ¶ms.query,
916 );
917 for sym in syms {
918 let src = match snap.sources.get(&sym.span.file_id) {
919 Some(x) => x,
920 None => continue,
921 };
922 let lm = match snap.line_maps.get(&sym.span.file_id) {
923 Some(m) => m,
924 None => continue,
925 };
926 let uri = match Url::from_file_path(&src.path) {
927 Ok(u) => u,
928 Err(_) => continue,
929 };
930 let range = crate::position::span_to_range(lm, &src.text, sym.span, encoding);
931 if !seen.insert((uri.clone(), range_key(&range), sym.name.clone())) {
932 continue;
933 }
934 #[allow(deprecated)]
935 out.push(SymbolInformation {
936 name: sym.name,
937 kind: to_lsp_kind(sym.kind),
938 tags: None,
939 deprecated: None,
940 location: Location { uri, range },
941 container_name: sym.container,
942 });
943 }
944 }
945 if out.is_empty() {
946 Ok(None)
947 } else {
948 Ok(Some(out))
949 }
950 }
951
952 async fn completion(
953 &self,
954 params: CompletionParams,
955 ) -> jsonrpc::Result<Option<CompletionResponse>> {
956 let uri = params.text_document_position.text_document.uri.clone();
957 let position = params.text_document_position.position;
958 let encoding = *self.encoding.lock().await;
959 let trigger = params
960 .context
961 .as_ref()
962 .and_then(|c| c.trigger_character.as_ref())
963 .and_then(|s| s.chars().next());
964
965 let snap = match self.snapshot_for(&uri) {
966 Some(s) => s,
967 None => return Ok(None),
968 };
969 let path = match uri.to_file_path() {
970 Ok(p) => p,
971 Err(_) => return Ok(None),
972 };
973 let file_id = match snap.path_to_file_id.get(&path) {
974 Some(id) => *id,
975 None => return Ok(None),
976 };
977 let source = match snap.sources.get(&file_id) {
978 Some(s) => s,
979 None => return Ok(None),
980 };
981 let line_map = match snap.line_maps.get(&file_id) {
982 Some(m) => m,
983 None => return Ok(None),
984 };
985 let byte = crate::position::position_to_byte(line_map, &source.text, position, encoding);
986
987 let items =
988 crate::completion::complete_at(&snap.ast, &snap.interner, file_id, byte, trigger);
989 let lsp_items: Vec<CompletionItem> = items
990 .into_iter()
991 .map(|i| CompletionItem {
992 label: i.label,
993 kind: Some(to_completion_kind(i.kind)),
994 detail: i.detail,
995 ..CompletionItem::default()
996 })
997 .collect();
998 if lsp_items.is_empty() {
999 Ok(None)
1000 } else {
1001 Ok(Some(CompletionResponse::Array(lsp_items)))
1002 }
1003 }
1004
1005 async fn inlay_hint(&self, params: InlayHintParams) -> jsonrpc::Result<Option<Vec<InlayHint>>> {
1006 let uri = params.text_document.uri.clone();
1007 let encoding = *self.encoding.lock().await;
1008
1009 let snap = match self.snapshot_for(&uri) {
1010 Some(s) => s,
1011 None => return Ok(None),
1012 };
1013 let path = match uri.to_file_path() {
1014 Ok(p) => p,
1015 Err(_) => return Ok(None),
1016 };
1017 let file_id = match snap.path_to_file_id.get(&path) {
1018 Some(id) => *id,
1019 None => return Ok(None),
1020 };
1021 let source = match snap.sources.get(&file_id) {
1022 Some(s) => s,
1023 None => return Ok(None),
1024 };
1025 let line_map = match snap.line_maps.get(&file_id) {
1026 Some(m) => m,
1027 None => return Ok(None),
1028 };
1029
1030 let hints = crate::inlay_hints::inlay_hints(
1031 &snap.ast,
1032 &snap.interner,
1033 &snap.expr_types,
1034 snap.type_pool.as_deref(),
1035 file_id,
1036 );
1037
1038 let lsp_hints: Vec<InlayHint> = hints
1039 .into_iter()
1040 .filter(|h| h.file_id == file_id)
1041 .map(|h| {
1042 let pos =
1043 crate::position::byte_to_position(line_map, &source.text, h.byte, encoding);
1044 InlayHint {
1045 position: pos,
1046 label: InlayHintLabel::String(h.label),
1047 kind: Some(match h.kind {
1048 crate::inlay_hints::InlayKind::Type => InlayHintKind::TYPE,
1049 crate::inlay_hints::InlayKind::Parameter => InlayHintKind::PARAMETER,
1050 }),
1051 text_edits: None,
1052 tooltip: None,
1053 padding_left: None,
1054 padding_right: None,
1055 data: None,
1056 }
1057 })
1058 .collect();
1059 if lsp_hints.is_empty() {
1060 Ok(None)
1061 } else {
1062 Ok(Some(lsp_hints))
1063 }
1064 }
1065
1066 async fn formatting(
1067 &self,
1068 params: DocumentFormattingParams,
1069 ) -> jsonrpc::Result<Option<Vec<TextEdit>>> {
1070 let uri = params.text_document.uri.clone();
1071
1072 let original = match self.docs.get(&uri) {
1075 Some(doc) => doc.text.clone(),
1076 None => return Ok(None),
1077 };
1078
1079 let formatted = match gruel_fmt::format_source(&original) {
1083 Ok(s) => s,
1084 Err(e) => {
1085 tracing::debug!(uri = %uri, error = %e, "gruel-fmt: parse failed");
1086 return Ok(None);
1087 }
1088 };
1089
1090 if formatted == original {
1091 return Ok(Some(Vec::new()));
1094 }
1095
1096 Ok(Some(diff_to_text_edits(&original, &formatted)))
1097 }
1098
1099 async fn code_action(
1100 &self,
1101 params: CodeActionParams,
1102 ) -> jsonrpc::Result<Option<CodeActionResponse>> {
1103 let uri = params.text_document.uri.clone();
1104 let range = params.range;
1105 let encoding = *self.encoding.lock().await;
1106 let workspace_root = self.workspace_root.lock().await.clone();
1107 let diagnostics = self
1108 .last_diagnostics
1109 .get(&uri)
1110 .map(|d| d.clone())
1111 .unwrap_or_default();
1112 let actions: Vec<CodeActionOrCommand> = crate::code_actions::code_actions_for_range(
1113 &diagnostics,
1114 range,
1115 &self.docs,
1116 encoding,
1117 workspace_root.as_deref(),
1118 );
1119 if actions.is_empty() {
1120 Ok(None)
1121 } else {
1122 Ok(Some(actions))
1123 }
1124 }
1125}
1126
1127fn diff_to_text_edits(original: &str, formatted: &str) -> Vec<TextEdit> {
1136 use similar::{DiffOp, TextDiff};
1137
1138 let diff = TextDiff::from_lines(original, formatted);
1139 let new_lines: Vec<&str> = formatted.split_inclusive('\n').collect();
1140
1141 let mut edits = Vec::new();
1142 for group in diff.grouped_ops(0) {
1145 if group.is_empty() {
1146 continue;
1147 }
1148 let all_equal = group.iter().all(|op| matches!(op, DiffOp::Equal { .. }));
1150 if all_equal {
1151 continue;
1152 }
1153
1154 let mut old_start = usize::MAX;
1155 let mut old_end = 0;
1156 let mut new_start = usize::MAX;
1157 let mut new_end = 0;
1158 for op in &group {
1159 let (os, oe) = (op.old_range().start, op.old_range().end);
1160 let (ns, ne) = (op.new_range().start, op.new_range().end);
1161 old_start = old_start.min(os);
1162 old_end = old_end.max(oe);
1163 new_start = new_start.min(ns);
1164 new_end = new_end.max(ne);
1165 }
1166
1167 let new_text: String = new_lines[new_start..new_end].concat();
1171
1172 let range = Range {
1173 start: Position {
1174 line: old_start as u32,
1175 character: 0,
1176 },
1177 end: Position {
1178 line: old_end as u32,
1179 character: 0,
1180 },
1181 };
1182 edits.push(TextEdit { range, new_text });
1183 }
1184 edits
1185}
1186
1187fn pick_encoding(params: &InitializeParams) -> PositionEncoding {
1188 if let Some(general) = params.capabilities.general.as_ref() {
1189 if let Some(encs) = general.position_encodings.as_ref() {
1190 for e in encs {
1191 if *e == PositionEncodingKind::UTF8 {
1192 return PositionEncoding::Utf8;
1193 }
1194 }
1195 }
1196 }
1197 PositionEncoding::Utf16
1198}
1199
1200pub async fn run_stdio_server(preview_features: PreviewFeatures) -> std::io::Result<()> {
1202 let stdin = tokio::io::stdin();
1203 let stdout = tokio::io::stdout();
1204 let (service, socket) =
1205 LspService::new(move |client| Backend::new(client, preview_features.clone()));
1206 Server::new(stdin, stdout, socket).serve(service).await;
1207 Ok(())
1208}
1209
1210fn to_lsp_kind(k: crate::workspace_symbols::SymbolKind) -> LspSymbolKind {
1211 use crate::workspace_symbols::SymbolKind as K;
1212 match k {
1213 K::Function => LspSymbolKind::FUNCTION,
1214 K::Struct => LspSymbolKind::STRUCT,
1215 K::Enum => LspSymbolKind::ENUM,
1216 K::Interface => LspSymbolKind::INTERFACE,
1217 K::Derive => LspSymbolKind::CLASS,
1218 K::Constant => LspSymbolKind::CONSTANT,
1219 K::Field => LspSymbolKind::FIELD,
1220 K::EnumMember => LspSymbolKind::ENUM_MEMBER,
1221 K::Method => LspSymbolKind::METHOD,
1222 }
1223}
1224
1225fn to_completion_kind(k: crate::completion::CompletionKind) -> CompletionItemKind {
1226 use crate::completion::CompletionKind as K;
1227 match k {
1228 K::Function => CompletionItemKind::FUNCTION,
1229 K::Struct => CompletionItemKind::STRUCT,
1230 K::Enum => CompletionItemKind::ENUM,
1231 K::Interface => CompletionItemKind::INTERFACE,
1232 K::Derive => CompletionItemKind::CLASS,
1233 K::Constant => CompletionItemKind::CONSTANT,
1234 K::Field => CompletionItemKind::FIELD,
1235 K::EnumMember => CompletionItemKind::ENUM_MEMBER,
1236 K::Variable => CompletionItemKind::VARIABLE,
1237 K::Method => CompletionItemKind::METHOD,
1238 K::Keyword => CompletionItemKind::KEYWORD,
1239 K::Intrinsic => CompletionItemKind::FUNCTION,
1240 }
1241}
1242
1243pub fn run_server(preview_features: PreviewFeatures) -> std::io::Result<()> {
1246 let rt = tokio::runtime::Builder::new_multi_thread()
1247 .enable_all()
1248 .build()?;
1249 rt.block_on(run_stdio_server(preview_features))
1250}