1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use crate::extension_dispatch::{
8 dispatch_code_action as ext_dispatch_code_action,
9 dispatch_completion as ext_dispatch_completion, dispatch_hover as ext_dispatch_hover,
10 LspExtensionState,
11};
12use crate::features::commands::{self, execute_command};
13use crate::features::document_links::collect_document_links;
14use crate::features::document_symbols::{collect_document_symbols, LexDocumentSymbol};
15use crate::features::extract::{self, ExtractError};
16use crate::features::folding_ranges::{folding_ranges as collect_folding_ranges, LexFoldingRange};
17use crate::features::formatting::{self, LineRange as FormattingLineRange, TextEditSpan};
18use crate::features::go_to_definition::goto_definition;
19use crate::features::hover::{hover as compute_hover, HoverResult};
20use crate::features::references::find_references;
21use crate::features::semantic_tokens::{
22 collect_semantic_tokens, LexSemanticToken, SEMANTIC_TOKEN_KINDS,
23};
24use clapfig::{Boundary, Clapfig, SearchPath};
25use lex_analysis::completion::{completion_items, CompletionCandidate, CompletionWorkspace};
26use lex_analysis::diagnostics::{
27 analyze as analyze_diagnostics, apply_rules, AnalysisDiagnostic, DiagnosticKind,
28};
29use lex_babel::formats::lex::formatting_rules::FormattingRules;
30use lex_babel::templates::{
31 build_asset_snippet, build_verbatim_snippet, AssetSnippetRequest, VerbatimSnippetRequest,
32};
33use lex_config::{
34 collect_extension_diagnostic_rules, LabelsConfig, LexConfig, LoadedLexConfig, CONFIG_FILE_NAME,
35 DIAGNOSTICS_RULES_PATH,
36};
37use lex_core::lex::ast::links::{DocumentLink as AstDocumentLink, LinkType};
38use lex_core::lex::ast::range::SourceLocation;
39use lex_core::lex::ast::{Document, Position as AstPosition, Range as AstRange};
40use lex_core::lex::builtins as lex_builtins;
41use lex_core::lex::includes::{resolve_from_source, FsLoader, IncludeError, ResolveConfig};
42use lex_core::lex::parsing;
43use lex_extension_host::registry::Registry;
44use lex_lsp_core::prepare_paste::{
45 prepare_paste as prepare_paste_transform, PasteMode, PreparePasteParams, PreparePasteResult,
46};
47use serde_json::{json, Value};
48use tokio::sync::RwLock;
49use tower_lsp::async_trait;
50use tower_lsp::jsonrpc::{Error, Result};
51use tower_lsp::lsp_types::{
52 CodeActionParams, CodeActionProviderCapability, CodeActionResponse, CompletionItem,
53 CompletionOptions, CompletionParams, CompletionResponse, DidChangeConfigurationParams,
54 DidChangeWorkspaceFoldersParams, DocumentFormattingParams, DocumentLink, DocumentLinkOptions,
55 DocumentLinkParams, DocumentRangeFormattingParams, DocumentSymbol, DocumentSymbolParams,
56 DocumentSymbolResponse, ExecuteCommandOptions, ExecuteCommandParams, FoldingRange,
57 FoldingRangeParams, FoldingRangeProviderCapability, GotoDefinitionParams,
58 GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
59 InitializeParams, InitializeResult, InitializedParams, Location, MarkupContent, MarkupKind,
60 OneOf, Position, Range, ReferenceParams, SemanticToken, SemanticTokenType, SemanticTokens,
61 SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams,
62 SemanticTokensResult, ServerCapabilities, ServerInfo, TextDocumentItem,
63 TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkDoneProgressOptions,
64 WorkspaceFoldersServerCapabilities,
65};
66use tower_lsp::Client;
67
68use tower_lsp::lsp_types::Diagnostic;
69
70use tower_lsp::lsp_types::MessageType;
71
72#[async_trait]
73pub trait LspClient:
74 crate::trust_prompt::LspTrustRequester + Send + Sync + Clone + 'static
75{
76 async fn publish_diagnostics(&self, uri: Url, diags: Vec<Diagnostic>, version: Option<i32>);
77 async fn show_message(&self, typ: MessageType, message: String);
78}
79
80#[async_trait]
81impl LspClient for Client {
82 async fn publish_diagnostics(&self, uri: Url, diags: Vec<Diagnostic>, version: Option<i32>) {
83 self.publish_diagnostics(uri, diags, version).await;
84 }
85
86 async fn show_message(&self, typ: MessageType, message: String) {
87 self.show_message(typ, message).await;
88 }
89}
90
91pub trait FeatureProvider: Send + Sync + 'static {
92 fn semantic_tokens(&self, document: &Document) -> Vec<LexSemanticToken>;
93 fn document_symbols(&self, document: &Document) -> Vec<LexDocumentSymbol>;
94 fn folding_ranges(&self, document: &Document) -> Vec<LexFoldingRange>;
95 fn hover(&self, document: &Document, position: AstPosition) -> Option<HoverResult>;
96 fn goto_definition(&self, document: &Document, position: AstPosition) -> Vec<AstRange>;
97 fn references(
98 &self,
99 document: &Document,
100 position: AstPosition,
101 include_declaration: bool,
102 ) -> Vec<AstRange>;
103 fn document_links(&self, document: &Document) -> Vec<AstDocumentLink>;
104 fn format_document(
105 &self,
106 document: &Document,
107 source: &str,
108 rules: Option<FormattingRules>,
109 ) -> Vec<TextEditSpan>;
110 fn format_range(
111 &self,
112 document: &Document,
113 source: &str,
114 range: FormattingLineRange,
115 rules: Option<FormattingRules>,
116 ) -> Vec<TextEditSpan>;
117 fn completion(
118 &self,
119 document: &Document,
120 position: AstPosition,
121 current_line: Option<&str>,
122 workspace: Option<&CompletionWorkspace>,
123 trigger_char: Option<&str>,
124 ) -> Vec<CompletionCandidate>;
125 fn execute_command(&self, command: &str, arguments: &[Value]) -> Result<Option<Value>>;
126}
127
128#[derive(Default)]
129pub struct DefaultFeatureProvider;
130
131impl DefaultFeatureProvider {
132 pub fn new() -> Self {
133 Self
134 }
135}
136
137#[async_trait]
138impl FeatureProvider for DefaultFeatureProvider {
139 fn semantic_tokens(&self, document: &Document) -> Vec<LexSemanticToken> {
140 collect_semantic_tokens(document)
141 }
142
143 fn document_symbols(&self, document: &Document) -> Vec<LexDocumentSymbol> {
144 collect_document_symbols(document)
145 }
146
147 fn folding_ranges(&self, document: &Document) -> Vec<LexFoldingRange> {
148 collect_folding_ranges(document)
149 }
150
151 fn hover(&self, document: &Document, position: AstPosition) -> Option<HoverResult> {
152 compute_hover(document, position)
153 }
154
155 fn goto_definition(&self, document: &Document, position: AstPosition) -> Vec<AstRange> {
156 goto_definition(document, position)
157 }
158
159 fn references(
160 &self,
161 document: &Document,
162 position: AstPosition,
163 include_declaration: bool,
164 ) -> Vec<AstRange> {
165 find_references(document, position, include_declaration)
166 }
167
168 fn document_links(&self, document: &Document) -> Vec<AstDocumentLink> {
169 collect_document_links(document)
170 }
171
172 fn format_document(
173 &self,
174 document: &Document,
175 source: &str,
176 rules: Option<FormattingRules>,
177 ) -> Vec<TextEditSpan> {
178 formatting::format_document(document, source, rules)
179 }
180
181 fn format_range(
182 &self,
183 document: &Document,
184 source: &str,
185 range: FormattingLineRange,
186 rules: Option<FormattingRules>,
187 ) -> Vec<TextEditSpan> {
188 formatting::format_range(document, source, range, rules)
189 }
190
191 fn completion(
192 &self,
193 document: &Document,
194 position: AstPosition,
195 current_line: Option<&str>,
196 workspace: Option<&CompletionWorkspace>,
197 trigger_char: Option<&str>,
198 ) -> Vec<CompletionCandidate> {
199 completion_items(document, position, current_line, workspace, trigger_char)
200 }
201
202 fn execute_command(&self, command: &str, arguments: &[Value]) -> Result<Option<Value>> {
203 execute_command(command, arguments)
204 }
205}
206
207#[derive(Clone)]
208struct DocumentEntry {
209 document: Arc<Document>,
210 text: Arc<String>,
211}
212
213#[derive(Default)]
214struct DocumentStore {
215 entries: RwLock<HashMap<Url, Option<DocumentEntry>>>,
216}
217
218impl DocumentStore {
219 async fn upsert(&self, uri: Url, text: String) -> Option<DocumentEntry> {
220 let parsed = match parsing::parse_document_permissive(&text) {
228 Ok(document) => Some(DocumentEntry {
229 document: Arc::new(document),
230 text: Arc::new(text),
231 }),
232 Err(_) => None,
233 };
234 self.entries.write().await.insert(uri, parsed.clone());
235 parsed
236 }
237
238 async fn get(&self, uri: &Url) -> Option<DocumentEntry> {
239 self.entries.read().await.get(uri).cloned().flatten()
240 }
241
242 async fn remove(&self, uri: &Url) {
243 self.entries.write().await.remove(uri);
244 }
245}
246
247fn document_directory_from_uri(uri: &Url) -> Option<PathBuf> {
248 uri.to_file_path()
249 .ok()
250 .and_then(|path| path.parent().map(|parent| parent.to_path_buf()))
251}
252
253fn indent_level_from_position(
254 entry: &DocumentEntry,
255 position: &Position,
256 rules: &FormattingRules,
257) -> usize {
258 let indent_unit = rules.indent_string.as_str();
259 if indent_unit.is_empty() {
260 return 0;
261 }
262 let indent_len = indent_unit.len();
263 let line = entry.text.lines().nth(position.line as usize).unwrap_or("");
264 let prefix: String = line.chars().take(position.character as usize).collect();
265 let mut level = 0;
266 let mut remainder = prefix.as_str();
267 while remainder.starts_with(indent_unit) {
268 level += 1;
269 remainder = &remainder[indent_len..];
270 }
271 level
272}
273
274fn semantic_tokens_legend() -> SemanticTokensLegend {
275 SemanticTokensLegend {
276 token_types: SEMANTIC_TOKEN_KINDS
277 .iter()
278 .map(|kind| SemanticTokenType::new(kind.as_str()))
279 .collect(),
280 token_modifiers: Vec::new(),
281 }
282}
283
284pub struct LexLanguageServer<C = Client, P = DefaultFeatureProvider> {
285 client: C,
286 documents: DocumentStore,
287 features: Arc<P>,
288 workspace_roots: RwLock<Vec<PathBuf>>,
289 config: RwLock<LoadedLexConfig>,
290 extension: RwLock<Option<Arc<LspExtensionState>>>,
297 extension_init: tokio::sync::Mutex<()>,
306}
307
308impl LexLanguageServer<Client, DefaultFeatureProvider> {
309 pub fn new(client: Client) -> Self {
310 Self::with_features(client, Arc::new(DefaultFeatureProvider::new()))
311 }
312}
313
314impl<C, P> LexLanguageServer<C, P>
315where
316 C: LspClient,
317 P: FeatureProvider,
318{
319 pub fn with_features(client: C, features: Arc<P>) -> Self {
320 let config = load_config(None);
321 Self {
322 client,
323 documents: DocumentStore::default(),
324 features,
325 workspace_roots: RwLock::new(Vec::new()),
326 config: RwLock::new(config),
327 extension: RwLock::new(None),
328 extension_init: tokio::sync::Mutex::new(()),
329 }
330 }
331
332 async fn extension_state(&self) -> Option<Arc<LspExtensionState>> {
349 if let Some(s) = self.extension.read().await.clone() {
351 return Some(s);
352 }
353
354 let _guard = self.extension_init.lock().await;
357
358 if let Some(s) = self.extension.read().await.clone() {
361 return Some(s);
362 }
363
364 let workspace_root = {
365 let roots = self.workspace_roots.read().await;
366 roots.first().cloned()?
367 };
368 let labels_config = LabelsConfig {
369 namespaces: self.config.read().await.config.labels.clone(),
370 };
371
372 let workspace_root_owned = workspace_root.clone();
382 let trust_requester = std::sync::Arc::new(self.client.clone());
383 let runtime_handle = tokio::runtime::Handle::current();
384 let outcome = match tokio::task::spawn_blocking(move || {
385 lex_fmt::boot_registry(lex_fmt::ExtensionSetup {
386 workspace_root: workspace_root_owned.as_path(),
387 labels_config: &labels_config,
388 ext_schemas: &[],
392 enable_handlers: false,
397 surface_override: Some(lex_extension_host::Surface::LspSession),
398 trust_prompt: Box::new(crate::trust_prompt::LspPromptHandler::new(
403 trust_requester,
404 runtime_handle,
405 )),
406 host_version: env!("CARGO_PKG_VERSION"),
410 })
411 })
412 .await
413 {
414 Ok(outcome) => outcome,
415 Err(_) => {
416 return None;
420 }
421 };
422
423 for diag in &outcome.diagnostics {
432 let prefix = match &diag.namespace {
433 Some(ns) => format!("lex extension `{ns}`: "),
434 None => "lex extensions: ".to_string(),
435 };
436 self.client
437 .show_message(MessageType::WARNING, format!("{prefix}{}", diag.message))
438 .await;
439 }
440
441 let rule_findings = {
453 let cfg = self.config.read().await;
454 lex_fmt::validate_extension_diagnostic_rules(
455 &cfg.extension_diagnostic_rules,
456 &outcome.registry,
457 )
458 };
459 for finding in rule_findings {
460 self.client
461 .show_message(MessageType::WARNING, finding.message)
462 .await;
463 }
464
465 let state = Arc::new(LspExtensionState::from(outcome));
466 *self.extension.write().await = Some(state.clone());
467 Some(state)
468 }
469
470 async fn invalidate_extension_state(&self) {
474 *self.extension.write().await = None;
475 }
476
477 async fn parse_and_store(&self, uri: Url, text: String) {
478 let include_diags = self.resolve_and_upsert(&uri, &text).await;
486
487 let mut diagnostics: Vec<Diagnostic> = include_diags;
488 if let Some(entry) = self.documents.get(&uri).await {
489 let mut analysis_diags = analyze_diagnostics(&entry.document);
490 let cfg = self.config.read().await;
491 apply_rules(&mut analysis_diags, |code| {
492 cfg.lookup_diagnostic_rule(code).cloned()
493 });
494 drop(cfg);
495 diagnostics.extend(analysis_diags.into_iter().map(to_lsp_diagnostic));
496 }
497
498 self.client
499 .publish_diagnostics(uri, diagnostics, None)
500 .await;
501 }
502
503 async fn resolve_and_upsert(&self, uri: &Url, text: &str) -> Vec<Diagnostic> {
523 self.documents.upsert(uri.clone(), text.to_string()).await;
526
527 if !text.contains("lex.include") {
533 return Vec::new();
534 }
535
536 let path = match uri.to_file_path() {
537 Ok(p) => p,
538 Err(_) => return Vec::new(),
541 };
542
543 let path = absolutize_path(&path);
550
551 let cfg = self.config.read().await;
552 let inc_root = inc_root_for(&path, &cfg.config);
553 let max_depth = cfg.config.includes.max_depth;
554 let max_total_includes = cfg.config.includes.max_total_includes;
555 let max_file_size = cfg.config.includes.max_file_size;
556 drop(cfg);
557
558 let resolve_config = ResolveConfig {
559 root: inc_root.clone(),
560 max_depth,
561 max_total_includes,
562 };
563 let loader = FsLoader::new(inc_root).with_max_file_size(max_file_size);
564 let registry = Registry::new();
565 if let Err(e) = lex_builtins::register_into(
566 ®istry,
567 std::sync::Arc::new(loader),
568 resolve_config.clone(),
569 ) {
570 return vec![registry_setup_diagnostic(&e.to_string())];
571 }
572
573 match resolve_from_source(text, Some(path), &resolve_config, ®istry) {
574 Ok(_doc) => {
575 Vec::new()
579 }
580 Err(err) => vec![include_error_to_diagnostic(&err)],
581 }
582 }
583
584 async fn document_entry(&self, uri: &Url) -> Option<DocumentEntry> {
585 self.documents.get(uri).await
586 }
587
588 async fn goto_for_include(
600 &self,
601 uri: &Url,
602 document: &Document,
603 position: AstPosition,
604 ) -> Option<Location> {
605 let annotation = lex_analysis::utils::find_annotation_at_position(document, position)?;
606 if !annotation.is_include() {
607 return None;
608 }
609 let src = annotation.include_src()?;
610
611 let host_path = absolutize_path(&uri.to_file_path().ok()?);
612 let cfg = self.config.read().await;
613 let inc_root = inc_root_for(&host_path, &cfg.config);
614 drop(cfg);
615
616 let target = lex_core::lex::includes::resolve_file_reference(
617 &src,
618 Some(host_path.as_path()),
619 inc_root.as_path(),
620 )
621 .ok()?;
622 if !target.is_file() {
629 return None;
630 }
631 let target_uri = Url::from_file_path(&target).ok()?;
632 Some(Location {
633 uri: target_uri,
634 range: head_range(),
635 })
636 }
637
638 async fn hover_for_include(
645 &self,
646 uri: &Url,
647 document: &Document,
648 position: AstPosition,
649 ) -> Option<Hover> {
650 let annotation = lex_analysis::utils::find_annotation_at_position(document, position)?;
651 if !annotation.is_include() {
652 return None;
653 }
654 let src = annotation.include_src()?;
655
656 let host_path = absolutize_path(&uri.to_file_path().ok()?);
657 let cfg = self.config.read().await;
658 let inc_root = inc_root_for(&host_path, &cfg.config);
659 drop(cfg);
660
661 let target = lex_core::lex::includes::resolve_file_reference(
662 &src,
663 Some(host_path.as_path()),
664 inc_root.as_path(),
665 )
666 .ok()?;
667
668 let loader = FsLoader::new(inc_root.clone());
669 let loaded = lex_core::lex::includes::Loader::load(&loader, target.as_path()).ok()?;
670 let preview = include_preview_markdown(&src, &target, &loaded.source);
671
672 Some(Hover {
673 contents: HoverContents::Markup(MarkupContent {
674 kind: MarkupKind::Markdown,
675 value: preview,
676 }),
677 range: Some(to_lsp_range(annotation.header_location())),
678 })
679 }
680
681 async fn document(&self, uri: &Url) -> Option<Arc<Document>> {
682 self.document_entry(uri).await.map(|entry| entry.document)
683 }
684
685 #[allow(deprecated)]
686 async fn update_workspace_roots(&self, params: &InitializeParams) {
687 let mut roots = Vec::new();
688
689 if let Some(folders) = params.workspace_folders.as_ref() {
690 for folder in folders {
691 if let Ok(path) = folder.uri.to_file_path() {
692 roots.push(path);
693 }
694 }
695 }
696
697 if roots.is_empty() {
698 if let Some(root_uri) = params.root_uri.as_ref() {
699 if let Ok(path) = root_uri.to_file_path() {
700 roots.push(path);
701 }
702 } else if let Some(root_path) = params.root_path.as_ref() {
703 roots.push(PathBuf::from(root_path));
704 } else if let Ok(current_dir) = std::env::current_dir() {
705 roots.push(current_dir);
706 }
707 }
708
709 *self.workspace_roots.write().await = roots;
710 }
711
712 async fn workspace_context_for_uri(&self, uri: &Url) -> Option<CompletionWorkspace> {
713 let document_path = uri.to_file_path().ok()?;
714 let roots = self.workspace_roots.read().await;
715 let project_root = best_matching_root(&roots, &document_path)
716 .or_else(|| document_directory_from_uri(uri))
717 .or_else(|| document_path.parent().map(|path| path.to_path_buf()))
718 .unwrap_or_else(|| document_path.clone());
719
720 Some(CompletionWorkspace {
721 project_root,
722 document_path,
723 })
724 }
725
726 async fn resolve_formatting_rules(&self, options: &FormattingOptions) -> FormattingRules {
728 let config = self.config.read().await;
729 let mut rules = FormattingRules::from(&config.config.formatting.rules);
730
731 apply_formatting_overrides(&mut rules, options);
733
734 rules
735 }
736}
737
738fn load_config(workspace_root: Option<&Path>) -> LoadedLexConfig {
743 let mut search_paths = vec![SearchPath::Platform];
744 if let Some(root) = workspace_root {
745 search_paths.push(SearchPath::Path(root.to_path_buf()));
746 } else {
747 search_paths.push(SearchPath::Ancestors(Boundary::Marker(".git")));
748 search_paths.push(SearchPath::Cwd);
749 }
750 load_with(search_paths, false).unwrap_or_else(|_| {
751 load_with(vec![], true).expect("compiled defaults must load")
753 })
754}
755
756fn load_with(
757 search_paths: Vec<SearchPath>,
758 no_env: bool,
759) -> std::result::Result<LoadedLexConfig, clapfig::ClapfigError> {
760 let mut builder = Clapfig::schema_builder::<LexConfig>()
761 .app_name("lex")
762 .file_name(CONFIG_FILE_NAME)
763 .search_paths(search_paths)
764 .accept_dotted_extension_keys_in(
765 DIAGNOSTICS_RULES_PATH,
766 clapfig::UnknownKeyDecision::Collect,
767 );
768 if no_env {
769 builder = builder.no_env();
770 }
771 let (config, unknowns) = builder.load_with_unknowns()?;
772 Ok(LoadedLexConfig {
773 config,
774 extension_diagnostic_rules: collect_extension_diagnostic_rules(unknowns),
775 })
776}
777
778fn best_matching_root(roots: &[PathBuf], document_path: &Path) -> Option<PathBuf> {
779 roots
780 .iter()
781 .filter(|root| document_path.starts_with(root))
782 .max_by_key(|root| root.components().count())
783 .cloned()
784}
785
786fn to_lsp_position(position: &AstPosition) -> Position {
787 Position::new(position.line as u32, position.column as u32)
788}
789
790fn to_lsp_range(range: &AstRange) -> Range {
791 Range {
792 start: to_lsp_position(&range.start),
793 end: to_lsp_position(&range.end),
794 }
795}
796
797fn to_lsp_location(uri: &Url, range: &AstRange) -> Location {
798 Location {
799 uri: uri.clone(),
800 range: to_lsp_range(range),
801 }
802}
803
804fn spans_to_text_edits(text: &str, spans: Vec<TextEditSpan>) -> Vec<TextEdit> {
805 if spans.is_empty() {
806 return Vec::new();
807 }
808 let locator = SourceLocation::new(text);
809 spans
810 .into_iter()
811 .map(|span| TextEdit {
812 range: Range {
813 start: to_lsp_position(&locator.byte_to_position(span.start)),
814 end: to_lsp_position(&locator.byte_to_position(span.end)),
815 },
816 new_text: span.new_text,
817 })
818 .collect()
819}
820
821fn to_formatting_line_range(range: &Range) -> FormattingLineRange {
822 let start = range.start.line as usize;
823 let mut end = range.end.line as usize;
824 if range.end.character > 0 || end == start {
825 end += 1;
826 }
827 FormattingLineRange { start, end }
828}
829
830use lsp_types::{FormattingOptions, FormattingProperty};
831
832fn apply_formatting_overrides(rules: &mut FormattingRules, options: &FormattingOptions) {
845 for (key, value) in &options.properties {
846 match key.as_str() {
847 "lex.session_blank_lines_before" => {
848 if let FormattingProperty::Number(n) = value {
849 rules.session_blank_lines_before = (*n).max(0) as usize;
850 }
851 }
852 "lex.session_blank_lines_after" => {
853 if let FormattingProperty::Number(n) = value {
854 rules.session_blank_lines_after = (*n).max(0) as usize;
855 }
856 }
857 "lex.normalize_seq_markers" => {
858 if let FormattingProperty::Bool(b) = value {
859 rules.normalize_seq_markers = *b;
860 }
861 }
862 "lex.unordered_seq_marker" => {
863 if let FormattingProperty::String(s) = value {
864 if let Some(c) = s.chars().next() {
865 rules.unordered_seq_marker = c;
866 }
867 }
868 }
869 "lex.max_blank_lines" => {
870 if let FormattingProperty::Number(n) = value {
871 rules.max_blank_lines = (*n).max(0) as usize;
872 }
873 }
874 "lex.indent_string" => {
875 if let FormattingProperty::String(s) = value {
876 rules.indent_string = s.clone();
877 }
878 }
879 "lex.preserve_trailing_blanks" => {
880 if let FormattingProperty::Bool(b) = value {
881 rules.preserve_trailing_blanks = *b;
882 }
883 }
884 "lex.normalize_verbatim_markers" => {
885 if let FormattingProperty::Bool(b) = value {
886 rules.normalize_verbatim_markers = *b;
887 }
888 }
889 _ => {}
890 }
891 }
892}
893
894fn from_lsp_position(position: Position) -> AstPosition {
895 AstPosition::new(position.line as usize, position.character as usize)
896}
897
898fn encode_semantic_tokens(tokens: &[LexSemanticToken], text: &str) -> Vec<SemanticToken> {
899 let line_offsets = compute_line_offsets(text);
900 let mut data = Vec::new();
901 let mut prev_line = 0u32;
902 let mut prev_start = 0u32;
903
904 for token in tokens {
905 let token_type_index = SEMANTIC_TOKEN_KINDS
906 .iter()
907 .position(|kind| *kind == token.kind)
908 .unwrap_or(0) as u32;
909 for (line, start, length) in split_token_on_lines(token, text, &line_offsets) {
910 if length == 0 {
911 continue;
912 }
913 let delta_line = line.saturating_sub(prev_line);
914 let delta_start = if delta_line == 0 {
915 start.saturating_sub(prev_start)
916 } else {
917 start
918 };
919 data.push(SemanticToken {
920 delta_line,
921 delta_start,
922 length,
923 token_type: token_type_index,
924 token_modifiers_bitset: 0,
925 });
926 prev_line = line;
927 prev_start = start;
928 }
929 }
930
931 data
932}
933
934fn compute_line_offsets(text: &str) -> Vec<usize> {
935 let mut offsets = vec![0];
936 for (idx, ch) in text.char_indices() {
937 if ch == '\n' {
938 offsets.push(idx + ch.len_utf8());
939 }
940 }
941 offsets
942}
943
944fn split_token_on_lines(
950 token: &LexSemanticToken,
951 text: &str,
952 line_offsets: &[usize],
953) -> Vec<(u32, u32, u32)> {
954 let span = &token.range.span;
955 if span.start > text.len() || span.end > text.len() {
956 return Vec::new();
959 }
960 let slice = &text[span.clone()];
961 let mut segments = Vec::new();
962 let mut current_line = token.range.start.line as u32;
963 let mut segment_start = 0;
964 let base_offset = token.range.span.start;
965
966 for (idx, ch) in slice.char_indices() {
967 if ch == '\n' {
968 if idx > segment_start {
969 let length = (idx - segment_start) as u32;
970 let absolute_start = base_offset + segment_start;
971 let line_offset = line_offsets
972 .get(current_line as usize)
973 .copied()
974 .unwrap_or(0);
975 let start_col = (absolute_start.saturating_sub(line_offset)) as u32;
976 segments.push((current_line, start_col, length));
977 }
978 current_line += 1;
979 segment_start = idx + ch.len_utf8();
980 }
981 }
982
983 if slice.len() > segment_start {
984 let length = (slice.len() - segment_start) as u32;
985 let absolute_start = base_offset + segment_start;
986 let line_offset = line_offsets
987 .get(current_line as usize)
988 .copied()
989 .unwrap_or(0);
990 let start_col = (absolute_start.saturating_sub(line_offset)) as u32;
991 segments.push((current_line, start_col, length));
992 }
993
994 segments
995}
996
997#[allow(deprecated)]
998fn to_document_symbol(symbol: &LexDocumentSymbol) -> DocumentSymbol {
999 DocumentSymbol {
1000 name: symbol.name.clone(),
1001 detail: symbol.detail.clone(),
1002 kind: symbol.kind,
1003 deprecated: None,
1004 range: to_lsp_range(&symbol.range),
1005 selection_range: to_lsp_range(&symbol.selection_range),
1006 children: if symbol.children.is_empty() {
1007 None
1008 } else {
1009 Some(symbol.children.iter().map(to_document_symbol).collect())
1010 },
1011 tags: None,
1012 }
1013}
1014
1015fn to_lsp_folding_range(range: &LexFoldingRange) -> FoldingRange {
1016 FoldingRange {
1017 start_line: range.start_line,
1018 start_character: range.start_character,
1019 end_line: range.end_line,
1020 end_character: range.end_character,
1021 kind: range.kind.clone(),
1022 collapsed_text: None,
1023 }
1024}
1025
1026fn to_lsp_completion_item(candidate: &CompletionCandidate) -> CompletionItem {
1027 CompletionItem {
1028 label: candidate.label.clone(),
1029 kind: Some(candidate.kind),
1030 detail: candidate.detail.clone(),
1031 insert_text: candidate.insert_text.clone(),
1032 ..Default::default()
1033 }
1034}
1035
1036fn build_document_link(uri: &Url, link: &AstDocumentLink) -> Option<DocumentLink> {
1037 let target = link_target_uri(uri, link)?;
1038 Some(DocumentLink {
1039 range: to_lsp_range(&link.range),
1040 target: Some(target),
1041 tooltip: None,
1042 data: None,
1043 })
1044}
1045
1046fn link_target_uri(document_uri: &Url, link: &AstDocumentLink) -> Option<Url> {
1047 match link.link_type {
1048 LinkType::Url => Url::parse(&link.target).ok(),
1049 LinkType::File | LinkType::VerbatimSrc => {
1050 resolve_file_like_target(document_uri, &link.target)
1051 }
1052 }
1053}
1054
1055fn resolve_file_like_target(document_uri: &Url, target: &str) -> Option<Url> {
1056 if target.is_empty() {
1057 return None;
1058 }
1059 let path = Path::new(target);
1060 if path.is_absolute() {
1061 return Url::from_file_path(path).ok();
1062 }
1063 if document_uri.scheme() == "file" {
1064 let mut base = document_uri.to_file_path().ok()?;
1065 base.pop();
1066 base.push(target);
1067 Url::from_file_path(base).ok()
1068 } else {
1069 parent_directory_uri(document_uri).join(target).ok()
1070 }
1071}
1072
1073fn parent_directory_uri(uri: &Url) -> Url {
1074 let mut base = uri.clone();
1075 let mut path = base.path().to_string();
1076 if let Some(idx) = path.rfind('/') {
1077 path.truncate(idx + 1);
1078 } else {
1079 path.push('/');
1080 }
1081 base.set_path(&path);
1082 base.set_query(None);
1083 base.set_fragment(None);
1084 base
1085}
1086
1087#[async_trait]
1088impl<C, P> tower_lsp::LanguageServer for LexLanguageServer<C, P>
1089where
1090 C: LspClient,
1091 P: FeatureProvider,
1092{
1093 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
1094 self.update_workspace_roots(¶ms).await;
1095
1096 {
1098 let roots = self.workspace_roots.read().await;
1099 let root = roots.first().map(|p| p.as_path());
1100 *self.config.write().await = load_config(root);
1101 }
1102
1103 let capabilities = ServerCapabilities {
1104 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
1105 hover_provider: Some(HoverProviderCapability::Simple(true)),
1106 document_symbol_provider: Some(OneOf::Left(true)),
1107 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
1108 definition_provider: Some(OneOf::Left(true)),
1109 references_provider: Some(OneOf::Left(true)),
1110 document_link_provider: Some(DocumentLinkOptions {
1111 work_done_progress_options: WorkDoneProgressOptions::default(),
1112 resolve_provider: Some(false),
1113 }),
1114 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
1115 completion_provider: Some(CompletionOptions {
1116 resolve_provider: Some(false),
1117 trigger_characters: Some(vec![
1118 "[".to_string(),
1119 ":".to_string(),
1120 "=".to_string(),
1121 "@".to_string(),
1122 ]),
1123 work_done_progress_options: WorkDoneProgressOptions::default(),
1124 all_commit_characters: None,
1125 ..Default::default()
1126 }),
1127 document_formatting_provider: Some(OneOf::Left(true)),
1128 document_range_formatting_provider: Some(OneOf::Left(true)),
1129 semantic_tokens_provider: Some(
1130 lsp_types::SemanticTokensServerCapabilities::SemanticTokensOptions(
1131 SemanticTokensOptions {
1132 work_done_progress_options: WorkDoneProgressOptions::default(),
1133 legend: semantic_tokens_legend(),
1134 range: None,
1135 full: Some(SemanticTokensFullOptions::Bool(true)),
1136 },
1137 ),
1138 ),
1139 execute_command_provider: Some(ExecuteCommandOptions {
1140 commands: vec![
1141 commands::COMMAND_ECHO.to_string(),
1142 commands::COMMAND_IMPORT.to_string(),
1143 commands::COMMAND_EXPORT.to_string(),
1144 commands::COMMAND_NEXT_ANNOTATION.to_string(),
1145 commands::COMMAND_PREVIOUS_ANNOTATION.to_string(),
1146 commands::COMMAND_RESOLVE_ANNOTATION.to_string(),
1147 commands::COMMAND_TOGGLE_ANNOTATIONS.to_string(),
1148 commands::COMMAND_INSERT_ASSET.to_string(),
1149 commands::COMMAND_INSERT_VERBATIM.to_string(),
1150 commands::COMMAND_FOOTNOTES_REORDER.to_string(),
1151 commands::COMMAND_TABLE_FORMAT.to_string(),
1152 commands::COMMAND_TABLE_NEXT_CELL.to_string(),
1153 commands::COMMAND_TABLE_PREVIOUS_CELL.to_string(),
1154 commands::COMMAND_FORMATS_LIST.to_string(),
1155 commands::COMMAND_EXTRACT_TO_INCLUDE.to_string(),
1156 ],
1157 work_done_progress_options: WorkDoneProgressOptions::default(),
1158 }),
1159 workspace: Some(lsp_types::WorkspaceServerCapabilities {
1160 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
1161 supported: Some(true),
1162 change_notifications: Some(OneOf::Left(true)),
1163 }),
1164 file_operations: None,
1165 }),
1166 experimental: Some(json!({ "lexPreparePaste": true })),
1171 ..ServerCapabilities::default()
1172 };
1173
1174 Ok(InitializeResult {
1175 capabilities,
1176 server_info: Some(ServerInfo {
1177 name: "lexd-lsp".to_string(),
1178 version: Some(env!("CARGO_PKG_VERSION").to_string()),
1179 }),
1180 })
1181 }
1182
1183 async fn initialized(&self, _: InitializedParams) {}
1184
1185 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
1186 let mut roots = self.workspace_roots.write().await;
1187
1188 for removed in ¶ms.event.removed {
1190 if let Ok(path) = removed.uri.to_file_path() {
1191 roots.retain(|r| r != &path);
1192 }
1193 }
1194
1195 for added in ¶ms.event.added {
1197 if let Ok(path) = added.uri.to_file_path() {
1198 if !roots.contains(&path) {
1199 roots.push(path);
1200 }
1201 }
1202 }
1203
1204 drop(roots);
1206 let roots = self.workspace_roots.read().await;
1207 let root = roots.first().map(|p| p.as_path());
1208 *self.config.write().await = load_config(root);
1209
1210 self.invalidate_extension_state().await;
1213 }
1214
1215 async fn shutdown(&self) -> Result<()> {
1216 Ok(())
1217 }
1218
1219 async fn did_open(&self, params: lsp_types::DidOpenTextDocumentParams) {
1220 let TextDocumentItem { uri, text, .. } = params.text_document;
1221 self.parse_and_store(uri, text).await;
1222 }
1223
1224 async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
1225 {
1227 let roots = self.workspace_roots.read().await;
1228 let root = roots.first().map(|p| p.as_path());
1229 *self.config.write().await = load_config(root);
1230 }
1231
1232 self.invalidate_extension_state().await;
1235
1236 let uris: Vec<Url> = self
1238 .documents
1239 .entries
1240 .read()
1241 .await
1242 .keys()
1243 .cloned()
1244 .collect();
1245
1246 for uri in uris {
1247 if let Some(entry) = self.documents.get(&uri).await {
1248 self.parse_and_store(uri, entry.text.to_string()).await;
1249 }
1250 }
1251 }
1252 async fn did_change(&self, params: lsp_types::DidChangeTextDocumentParams) {
1253 if let Some(change) = params.content_changes.into_iter().last() {
1254 self.parse_and_store(params.text_document.uri, change.text)
1255 .await;
1256 }
1257 }
1258
1259 async fn did_close(&self, params: lsp_types::DidCloseTextDocumentParams) {
1260 self.documents.remove(¶ms.text_document.uri).await;
1261 }
1262
1263 async fn semantic_tokens_full(
1264 &self,
1265 params: SemanticTokensParams,
1266 ) -> Result<Option<SemanticTokensResult>> {
1267 if let Some(entry) = self.document_entry(¶ms.text_document.uri).await {
1268 let DocumentEntry { document, text } = entry;
1269 let tokens = self.features.semantic_tokens(&document);
1270 let data = encode_semantic_tokens(&tokens, text.as_str());
1271 Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
1272 result_id: None,
1273 data,
1274 })))
1275 } else {
1276 Ok(None)
1277 }
1278 }
1279
1280 async fn document_symbol(
1281 &self,
1282 params: DocumentSymbolParams,
1283 ) -> Result<Option<DocumentSymbolResponse>> {
1284 if let Some(document) = self.document(¶ms.text_document.uri).await {
1285 let symbols = self.features.document_symbols(&document);
1286 let converted: Vec<DocumentSymbol> = symbols.iter().map(to_document_symbol).collect();
1287 Ok(Some(DocumentSymbolResponse::Nested(converted)))
1288 } else {
1289 Ok(None)
1290 }
1291 }
1292
1293 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
1294 let uri = ¶ms.text_document_position_params.text_document.uri;
1295 if let Some(document) = self.document(uri).await {
1296 let position = from_lsp_position(params.text_document_position_params.position);
1297
1298 if let Some(hover) = self.hover_for_include(uri, &document, position).await {
1304 return Ok(Some(hover));
1305 }
1306
1307 if let Some(state) = self.extension_state().await {
1313 if let Some(hover) =
1314 ext_dispatch_hover(&document, position, state.registry.as_ref())
1315 {
1316 return Ok(Some(hover));
1317 }
1318 }
1319
1320 if let Some(result) = self.features.hover(&document, position) {
1321 return Ok(Some(Hover {
1322 contents: HoverContents::Markup(MarkupContent {
1323 kind: MarkupKind::Markdown,
1324 value: result.contents,
1325 }),
1326 range: Some(to_lsp_range(&result.range)),
1327 }));
1328 }
1329 }
1330 Ok(None)
1331 }
1332
1333 async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
1334 if let Some(document) = self.document(¶ms.text_document.uri).await {
1335 let ranges = self.features.folding_ranges(&document);
1336 Ok(Some(ranges.iter().map(to_lsp_folding_range).collect()))
1337 } else {
1338 Ok(None)
1339 }
1340 }
1341
1342 async fn goto_definition(
1343 &self,
1344 params: GotoDefinitionParams,
1345 ) -> Result<Option<GotoDefinitionResponse>> {
1346 let uri = params.text_document_position_params.text_document.uri;
1347 if let Some(document) = self.document(&uri).await {
1348 let position = from_lsp_position(params.text_document_position_params.position);
1349
1350 if let Some(loc) = self.goto_for_include(&uri, &document, position).await {
1355 return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1356 }
1357
1358 let ranges = self.features.goto_definition(&document, position);
1359 if ranges.is_empty() {
1360 Ok(None)
1361 } else {
1362 let locations: Vec<Location> = ranges
1363 .iter()
1364 .map(|range| to_lsp_location(&uri, range))
1365 .collect();
1366 Ok(Some(GotoDefinitionResponse::Array(locations)))
1367 }
1368 } else {
1369 Ok(None)
1370 }
1371 }
1372
1373 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
1374 let uri = params.text_document_position.text_document.uri;
1375 if let Some(document) = self.document(&uri).await {
1376 let position = from_lsp_position(params.text_document_position.position);
1377 let include_declaration = params.context.include_declaration;
1378 let ranges = self
1379 .features
1380 .references(&document, position, include_declaration);
1381 if ranges.is_empty() {
1382 Ok(None)
1383 } else {
1384 Ok(Some(
1385 ranges
1386 .iter()
1387 .map(|range| to_lsp_location(&uri, range))
1388 .collect(),
1389 ))
1390 }
1391 } else {
1392 Ok(None)
1393 }
1394 }
1395
1396 async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
1397 let uri = params.text_document.uri;
1398 if let Some(document) = self.document(&uri).await {
1399 let links = self.features.document_links(&document);
1400 let resolved: Vec<DocumentLink> = links
1401 .iter()
1402 .filter_map(|link| build_document_link(&uri, link))
1403 .collect();
1404 Ok(Some(resolved))
1405 } else {
1406 Ok(None)
1407 }
1408 }
1409
1410 async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1411 let uri = params.text_document.uri;
1412 if let Some(entry) = self.document_entry(&uri).await {
1413 let DocumentEntry { document, text } = entry;
1414 let rules = self.resolve_formatting_rules(¶ms.options).await;
1415 let edits = self
1416 .features
1417 .format_document(&document, text.as_str(), Some(rules));
1418 Ok(Some(spans_to_text_edits(text.as_str(), edits)))
1419 } else {
1420 Ok(None)
1421 }
1422 }
1423
1424 async fn range_formatting(
1425 &self,
1426 params: DocumentRangeFormattingParams,
1427 ) -> Result<Option<Vec<TextEdit>>> {
1428 let uri = params.text_document.uri;
1429 if let Some(entry) = self.document_entry(&uri).await {
1430 let DocumentEntry { document, text } = entry;
1431 let line_range = to_formatting_line_range(¶ms.range);
1432 let rules = self.resolve_formatting_rules(¶ms.options).await;
1433 let edits =
1434 self.features
1435 .format_range(&document, text.as_str(), line_range, Some(rules));
1436 Ok(Some(spans_to_text_edits(text.as_str(), edits)))
1437 } else {
1438 Ok(None)
1439 }
1440 }
1441
1442 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
1443 let uri = params.text_document_position.text_document.uri;
1444 if let Some(entry) = self.document_entry(&uri).await {
1445 let DocumentEntry { document, text } = entry;
1446 let position = from_lsp_position(params.text_document_position.position);
1447 let workspace = self.workspace_context_for_uri(&uri).await;
1448
1449 let trigger_char = params
1451 .context
1452 .as_ref()
1453 .and_then(|ctx| ctx.trigger_character.as_deref());
1454
1455 let current_line = text.lines().nth(position.line);
1457
1458 let candidates = self.features.completion(
1459 &document,
1460 position,
1461 current_line,
1462 workspace.as_ref(),
1463 trigger_char,
1464 );
1465 let mut items: Vec<CompletionItem> =
1466 candidates.iter().map(to_lsp_completion_item).collect();
1467
1468 if let Some(state) = self.extension_state().await {
1473 items.extend(ext_dispatch_completion(
1474 &document,
1475 position,
1476 state.registry.as_ref(),
1477 ));
1478 }
1479
1480 Ok(Some(CompletionResponse::Array(items)))
1481 } else {
1482 Ok(None)
1483 }
1484 }
1485
1486 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
1487 let mut actions = Vec::new();
1488
1489 let document_uri = params.text_document.uri.clone();
1490 if let Some(entry) = self.documents.get(&document_uri).await {
1491 let lex_actions = crate::features::available_actions::compute_actions(
1492 &entry.document,
1493 &entry.text,
1494 ¶ms,
1495 );
1496 for action in lex_actions {
1497 actions.push(tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(
1498 action,
1499 ));
1500 }
1501
1502 if let Some(state) = self.extension_state().await {
1508 let start = from_lsp_position(params.range.start);
1509 for action in ext_dispatch_code_action(
1510 &entry.document,
1511 start,
1512 &document_uri,
1513 state.registry.as_ref(),
1514 ) {
1515 actions.push(tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(
1516 action,
1517 ));
1518 }
1519 }
1520 }
1521
1522 if actions.is_empty() {
1523 Ok(None)
1524 } else {
1525 Ok(Some(actions))
1526 }
1527 }
1528
1529 async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
1530 let command = params.command.as_str();
1531 match command {
1532 commands::COMMAND_NEXT_ANNOTATION | commands::COMMAND_PREVIOUS_ANNOTATION => {
1533 let uri_str = params.arguments.first().and_then(|v| v.as_str());
1534 let pos_val = params.arguments.get(1);
1535
1536 if let (Some(uri_str), Some(pos_val)) = (uri_str, pos_val) {
1537 if let Ok(uri) = Url::parse(uri_str) {
1538 if let Ok(position) = serde_json::from_value::<Position>(pos_val.clone()) {
1539 if let Some(document) = self.document(&uri).await {
1540 let ast_pos = from_lsp_position(position);
1541 let navigation = if command == commands::COMMAND_NEXT_ANNOTATION {
1542 lex_analysis::annotations::next_annotation(&document, ast_pos)
1543 } else {
1544 lex_analysis::annotations::previous_annotation(
1545 &document, ast_pos,
1546 )
1547 };
1548
1549 if let Some(result) = navigation {
1550 let location = to_lsp_location(&uri, &result.header);
1551 return Ok(Some(
1552 serde_json::to_value(location)
1553 .map_err(|_| Error::internal_error())?,
1554 ));
1555 }
1556 }
1557 }
1558 }
1559 }
1560 Ok(None)
1561 }
1562 commands::COMMAND_RESOLVE_ANNOTATION | commands::COMMAND_TOGGLE_ANNOTATIONS => {
1563 let uri_str = params.arguments.first().and_then(|v| v.as_str());
1564 let pos_val = params.arguments.get(1);
1565
1566 if let (Some(uri_str), Some(pos_val)) = (uri_str, pos_val) {
1567 if let Ok(uri) = Url::parse(uri_str) {
1568 if let Ok(position) = serde_json::from_value::<Position>(pos_val.clone()) {
1569 if let Some(document) = self.document(&uri).await {
1570 let ast_pos = from_lsp_position(position);
1571 let _resolved = command == commands::COMMAND_RESOLVE_ANNOTATION;
1572
1573 let target_state =
1584 if command == commands::COMMAND_RESOLVE_ANNOTATION {
1585 true
1586 } else {
1587 if let Some(annotation) =
1589 lex_analysis::utils::find_annotation_at_position(
1590 &document, ast_pos,
1591 )
1592 {
1593 let is_resolved =
1594 annotation.data.parameters.iter().any(|p| {
1595 p.key == "status" && p.value == "resolved"
1596 });
1597 !is_resolved
1598 } else {
1599 return Ok(None);
1600 }
1601 };
1602
1603 if let Some(edit) =
1604 lex_analysis::annotations::toggle_annotation_resolution(
1605 &document,
1606 ast_pos,
1607 target_state,
1608 )
1609 {
1610 let text_edit = TextEdit {
1611 range: to_lsp_range(&edit.range),
1612 new_text: edit.new_text,
1613 };
1614 let mut changes = HashMap::new();
1615 changes.insert(uri, vec![text_edit]);
1616 let workspace_edit = tower_lsp::lsp_types::WorkspaceEdit {
1617 changes: Some(changes),
1618 ..Default::default()
1619 };
1620 return Ok(Some(
1621 serde_json::to_value(workspace_edit)
1622 .map_err(|_| Error::internal_error())?,
1623 ));
1624 }
1625 }
1626 }
1627 }
1628 }
1629 Ok(None)
1630 }
1631 commands::COMMAND_INSERT_ASSET => {
1632 let uri_str = params.arguments.first().and_then(|v| v.as_str());
1633 let pos_val = params.arguments.get(1);
1634 let path_val = params.arguments.get(2).and_then(|v| v.as_str());
1635
1636 if let (Some(uri_str), Some(pos_val), Some(path)) = (uri_str, pos_val, path_val) {
1637 if let Ok(uri) = Url::parse(uri_str) {
1638 if let Ok(position) = serde_json::from_value::<Position>(pos_val.clone()) {
1639 let file_path = PathBuf::from(path);
1640 let rules = FormattingRules::default();
1641 let entry = self.document_entry(&uri).await;
1642 let indent_level = entry
1643 .as_ref()
1644 .map(|entry| indent_level_from_position(entry, &position, &rules))
1645 .unwrap_or(0);
1646 let document_directory = document_directory_from_uri(&uri);
1647 let snippet = {
1648 let request = AssetSnippetRequest {
1649 asset_path: file_path.as_path(),
1650 document_directory: document_directory.as_deref(),
1651 formatting: &rules,
1652 indent_level,
1653 };
1654 build_asset_snippet(&request)
1655 };
1656
1657 return Ok(Some(json!({
1658 "text": snippet.text,
1659 "cursorOffset": snippet.cursor_offset,
1660 })));
1661 }
1662 }
1663 }
1664 Ok(None)
1665 }
1666 commands::COMMAND_INSERT_VERBATIM => {
1667 let uri_str = params.arguments.first().and_then(|v| v.as_str());
1668 let pos_val = params.arguments.get(1);
1669 let path_val = params.arguments.get(2).and_then(|v| v.as_str());
1670
1671 if let (Some(uri_str), Some(pos_val), Some(path)) = (uri_str, pos_val, path_val) {
1672 if let Ok(uri) = Url::parse(uri_str) {
1673 if let Ok(position) = serde_json::from_value::<Position>(pos_val.clone()) {
1674 let file_path = PathBuf::from(path);
1675 let rules = FormattingRules::default();
1676 let entry = self.document_entry(&uri).await;
1677 let indent_level = entry
1678 .as_ref()
1679 .map(|entry| indent_level_from_position(entry, &position, &rules))
1680 .unwrap_or(0);
1681 let document_directory = document_directory_from_uri(&uri);
1682 let snippet_result = {
1683 let mut request =
1684 VerbatimSnippetRequest::new(file_path.as_path(), &rules);
1685 request.document_directory = document_directory.as_deref();
1686 request.indent_level = indent_level;
1687 build_verbatim_snippet(&request)
1688 };
1689
1690 match snippet_result {
1691 Ok(snippet) => {
1692 return Ok(Some(json!({
1693 "text": snippet.text,
1694 "cursorOffset": snippet.cursor_offset,
1695 })));
1696 }
1697 Err(err) => {
1698 return Err(Error::invalid_params(format!(
1699 "Failed to insert verbatim block: {err}"
1700 )));
1701 }
1702 }
1703 }
1704 }
1705 }
1706 Ok(None)
1707 }
1708 commands::COMMAND_EXTRACT_TO_INCLUDE => {
1709 self.handle_extract_to_include(¶ms.arguments).await
1710 }
1711 _ => self
1712 .features
1713 .execute_command(¶ms.command, ¶ms.arguments),
1714 }
1715 }
1716}
1717
1718impl<C, P> LexLanguageServer<C, P>
1719where
1720 C: LspClient,
1721 P: FeatureProvider,
1722{
1723 async fn handle_extract_to_include(&self, arguments: &[Value]) -> Result<Option<Value>> {
1731 let uri_str = arguments
1732 .first()
1733 .and_then(|v| v.as_str())
1734 .ok_or_else(|| Error::invalid_params("Missing 'uri' argument"))?;
1735 let range_val = arguments
1736 .get(1)
1737 .ok_or_else(|| Error::invalid_params("Missing 'range' argument"))?;
1738 let src = arguments
1739 .get(2)
1740 .and_then(|v| v.as_str())
1741 .ok_or_else(|| Error::invalid_params("Missing 'src' argument"))?;
1742
1743 let uri = Url::parse(uri_str)
1744 .map_err(|_| Error::invalid_params(format!("Invalid uri: {uri_str}")))?;
1745 let range: Range = serde_json::from_value(range_val.clone())
1746 .map_err(|e| Error::invalid_params(format!("Invalid range: {e}")))?;
1747
1748 let host_path = uri
1749 .to_file_path()
1750 .map_err(|_| Error::invalid_params(ExtractError::InvalidHostUri.message()))?;
1751 let host_path = absolutize_path(&host_path);
1752
1753 let entry = self
1754 .document_entry(&uri)
1755 .await
1756 .ok_or_else(|| Error::invalid_params("Document not open in the server"))?;
1757
1758 let selection_text = slice_text_by_range(&entry.text, range)
1759 .ok_or_else(|| Error::invalid_params("Selection range out of bounds"))?;
1760
1761 let host_indent = range.start.character as usize;
1762
1763 let cfg = self.config.read().await;
1764 let inc_root = inc_root_for(&host_path, &cfg.config);
1765 drop(cfg);
1766
1767 let edit = extract::build_extract_workspace_edit(
1768 &uri,
1769 &host_path,
1770 range,
1771 &selection_text,
1772 host_indent,
1773 src,
1774 &inc_root,
1775 )
1776 .map_err(|e| Error::invalid_params(e.message()))?;
1777
1778 Ok(Some(
1779 serde_json::to_value(edit).map_err(|_| Error::internal_error())?,
1780 ))
1781 }
1782
1783 pub async fn prepare_paste(&self, params: PreparePasteParams) -> Result<PreparePasteResult> {
1795 let Some(entry) = self.document_entry(¶ms.text_document.uri).await else {
1796 return Ok(PreparePasteResult {
1799 text: params.pasted_text,
1800 mode: PasteMode::Reanchor,
1801 });
1802 };
1803
1804 Ok(prepare_paste_transform(
1805 &entry.document,
1806 &entry.text,
1807 params.range,
1808 ¶ms.pasted_text,
1809 ))
1810 }
1811}
1812
1813fn slice_text_by_range(text: &str, range: Range) -> Option<String> {
1825 let start_line = range.start.line as usize;
1826 let end_line = range.end.line as usize;
1827 let start_col = range.start.character as usize;
1828 let end_col = range.end.character as usize;
1829 if start_line > end_line || (start_line == end_line && start_col > end_col) {
1830 return None;
1831 }
1832
1833 let lines: Vec<&str> = text.split_inclusive('\n').collect();
1834 if end_line >= lines.len() && !(end_line == lines.len() && end_col == 0) {
1835 return None;
1836 }
1837
1838 let mut out = String::new();
1839 for (i, line) in lines.iter().enumerate() {
1840 if i < start_line || i > end_line {
1841 continue;
1842 }
1843 let line_bytes = line.as_bytes();
1844 let from = if i == start_line { start_col } else { 0 };
1845 let to = if i == end_line {
1846 end_col
1847 } else {
1848 line_bytes.len()
1849 };
1850 if from > line_bytes.len() || to > line_bytes.len() {
1851 return None;
1852 }
1853 if !line.is_char_boundary(from) || !line.is_char_boundary(to) {
1856 return None;
1857 }
1858 out.push_str(&line[from..to]);
1859 }
1860 Some(out)
1861}
1862
1863fn inc_root_for(entry_path: &Path, cfg: &LexConfig) -> PathBuf {
1874 let raw = if let Some(root) = cfg.includes.root.as_ref() {
1875 PathBuf::from(root)
1876 } else {
1877 let start = entry_path
1878 .parent()
1879 .map(Path::to_path_buf)
1880 .unwrap_or_else(|| PathBuf::from("."));
1881 find_nearest_config_dir(&start).unwrap_or(start)
1882 };
1883 absolutize_path(&raw)
1884}
1885
1886fn find_nearest_config_dir(start: &Path) -> Option<PathBuf> {
1890 let mut cur: PathBuf = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
1891 loop {
1892 if cur.join(CONFIG_FILE_NAME).is_file() {
1893 return Some(cur);
1894 }
1895 if !cur.pop() {
1896 return None;
1897 }
1898 }
1899}
1900
1901fn absolutize_path(p: &Path) -> PathBuf {
1907 if let Ok(canon) = p.canonicalize() {
1908 return canon;
1909 }
1910 if p.is_absolute() {
1911 return p.to_path_buf();
1912 }
1913 std::env::current_dir()
1914 .map(|cwd| cwd.join(p))
1915 .unwrap_or_else(|_| p.to_path_buf())
1916}
1917
1918fn include_error_to_diagnostic(err: &IncludeError) -> Diagnostic {
1927 let (range, code, message) = match err {
1928 IncludeError::Cycle { include_site, .. } => {
1929 (to_lsp_range(include_site), "include-cycle", err.to_string())
1930 }
1931 IncludeError::DepthExceeded { include_site, .. } => (
1932 to_lsp_range(include_site),
1933 "include-depth-exceeded",
1934 err.to_string(),
1935 ),
1936 IncludeError::RootEscape { .. } => (head_range(), "include-root-escape", err.to_string()),
1937 IncludeError::AbsolutePath { .. } => {
1938 (head_range(), "include-absolute-path", err.to_string())
1939 }
1940 IncludeError::NotFound { include_site, .. } => (
1941 to_lsp_range(include_site),
1942 "include-not-found",
1943 err.to_string(),
1944 ),
1945 IncludeError::ParseFailed { .. } => (head_range(), "include-parse-failed", err.to_string()),
1946 IncludeError::ContainerPolicy { include_site, .. } => (
1947 to_lsp_range(include_site),
1948 "include-container-policy",
1949 err.to_string(),
1950 ),
1951 IncludeError::LoaderIo { .. } => (head_range(), "include-loader-io", err.to_string()),
1952 IncludeError::MissingSrc { include_site } => (
1953 to_lsp_range(include_site),
1954 "include-missing-src",
1955 err.to_string(),
1956 ),
1957 IncludeError::TotalIncludesExceeded { include_site, .. } => (
1958 to_lsp_range(include_site),
1959 "include-total-exceeded",
1960 err.to_string(),
1961 ),
1962 IncludeError::FileTooLarge { include_site, .. } => (
1963 to_lsp_range(include_site),
1964 "include-file-too-large",
1965 err.to_string(),
1966 ),
1967 IncludeError::HandlerFailed { include_site, .. } => (
1968 to_lsp_range(include_site),
1969 "include-handler-failed",
1970 err.to_string(),
1971 ),
1972 };
1973 Diagnostic {
1974 range,
1975 severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR),
1976 code: Some(tower_lsp::lsp_types::NumberOrString::String(
1977 code.to_string(),
1978 )),
1979 code_description: None,
1980 source: Some("lex".to_string()),
1981 message,
1982 related_information: None,
1983 tags: None,
1984 data: None,
1985 }
1986}
1987
1988fn registry_setup_diagnostic(message: &str) -> Diagnostic {
1995 Diagnostic {
1996 range: head_range(),
1997 severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR),
1998 code: Some(tower_lsp::lsp_types::NumberOrString::String(
1999 "include-registry-setup".to_string(),
2000 )),
2001 code_description: None,
2002 source: Some("lex".to_string()),
2003 message: format!("could not configure include resolver: {message}"),
2004 related_information: None,
2005 tags: None,
2006 data: None,
2007 }
2008}
2009
2010fn head_range() -> Range {
2011 Range {
2012 start: Position::new(0, 0),
2013 end: Position::new(0, 0),
2014 }
2015}
2016
2017fn include_preview_markdown(src: &str, target: &Path, target_source: &str) -> String {
2027 let mut out = String::new();
2028 out.push_str(&format!("**`lex.include`** → `{src}`\n\n"));
2029 out.push_str(&format!("Resolved: `{}`\n\n", target.display()));
2030
2031 let preview_lines: Vec<&str> = target_source
2032 .lines()
2033 .map(|l| l.trim_end())
2034 .filter(|l| !l.is_empty())
2035 .take(2)
2036 .collect();
2037 if preview_lines.is_empty() {
2038 out.push_str("_(empty file)_");
2039 } else {
2040 out.push_str("````lex\n");
2041 for line in &preview_lines {
2042 out.push_str(line);
2043 out.push('\n');
2044 }
2045 out.push_str("````");
2046 }
2047 out
2048}
2049
2050fn to_lsp_diagnostic(diag: AnalysisDiagnostic) -> Diagnostic {
2051 use lex_analysis::diagnostics::DiagnosticSeverity as AS;
2052 let severity = match diag.severity {
2053 AS::Error => tower_lsp::lsp_types::DiagnosticSeverity::ERROR,
2054 AS::Warning => tower_lsp::lsp_types::DiagnosticSeverity::WARNING,
2055 AS::Info => tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION,
2056 AS::Hint => tower_lsp::lsp_types::DiagnosticSeverity::HINT,
2057 };
2058
2059 let code = diag.kind.code().into_owned();
2060
2061 let source = match &diag.kind {
2062 DiagnosticKind::Handler { namespace, .. } => format!("lex:{namespace}"),
2063 _ => "lex".to_string(),
2064 };
2065
2066 Diagnostic {
2067 range: to_lsp_range(&diag.range),
2068 severity: Some(severity),
2069 code: Some(tower_lsp::lsp_types::NumberOrString::String(code)),
2070 code_description: None,
2071 source: Some(source),
2072 message: diag.message,
2073 related_information: None,
2074 tags: None,
2075 data: None,
2076 }
2077}
2078
2079#[cfg(test)]
2080mod tests {
2081 use super::*;
2082 use crate::features::semantic_tokens::LexSemanticTokenKind;
2083 use lex_analysis::test_support::sample_source;
2084 use serde::Deserialize;
2085 use std::fs;
2086 use std::sync::atomic::{AtomicUsize, Ordering};
2087 use std::sync::Mutex;
2088 use tempfile::tempdir;
2089 use tower_lsp::lsp_types::{
2090 CompletionItemKind, DidOpenTextDocumentParams, DocumentFormattingParams,
2091 DocumentLinkParams, DocumentRangeFormattingParams, DocumentSymbolParams, FoldingRangeKind,
2092 FoldingRangeParams, FormattingOptions, GotoDefinitionParams, HoverParams, Position, Range,
2093 ReferenceContext, ReferenceParams, SemanticTokensParams, SymbolKind,
2094 TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams,
2095 };
2096 use tower_lsp::LanguageServer;
2097
2098 #[derive(Clone, Default)]
2099 struct NoopClient;
2100 #[async_trait]
2101 impl LspClient for NoopClient {
2102 async fn publish_diagnostics(&self, _: Url, _: Vec<Diagnostic>, _: Option<i32>) {}
2103 async fn show_message(&self, _: MessageType, _: String) {}
2104 }
2105 #[async_trait]
2106 impl crate::trust_prompt::LspTrustRequester for NoopClient {
2107 async fn send_trust_request(
2108 &self,
2109 _: crate::trust_prompt::TrustRequestParams,
2110 ) -> tower_lsp::jsonrpc::Result<crate::trust_prompt::TrustResponse> {
2111 Ok(crate::trust_prompt::TrustResponse {
2115 decision: "denied".into(),
2116 reason: Some("test client".into()),
2117 })
2118 }
2119 }
2120
2121 #[derive(Default)]
2122 struct MockFeatureProvider {
2123 semantic_tokens_called: AtomicUsize,
2124 document_symbols_called: AtomicUsize,
2125 hover_called: AtomicUsize,
2126 folding_called: AtomicUsize,
2127 last_hover_position: Mutex<Option<AstPosition>>,
2128 definition_called: AtomicUsize,
2129 references_called: AtomicUsize,
2130 document_links_called: AtomicUsize,
2131 last_references_include: Mutex<Option<bool>>,
2132 formatting_called: AtomicUsize,
2133 range_formatting_called: AtomicUsize,
2134 completion_called: AtomicUsize,
2135 execute_command_called: AtomicUsize,
2136 }
2137
2138 impl FeatureProvider for MockFeatureProvider {
2139 fn semantic_tokens(&self, _: &Document) -> Vec<LexSemanticToken> {
2140 self.semantic_tokens_called.fetch_add(1, Ordering::SeqCst);
2141 vec![LexSemanticToken {
2142 kind: LexSemanticTokenKind::DocumentTitle,
2143 range: AstRange::new(0..5, AstPosition::new(0, 0), AstPosition::new(0, 5)),
2144 }]
2145 }
2146
2147 fn document_symbols(&self, _: &Document) -> Vec<LexDocumentSymbol> {
2148 self.document_symbols_called.fetch_add(1, Ordering::SeqCst);
2149 vec![LexDocumentSymbol {
2150 name: "symbol".into(),
2151 detail: None,
2152 kind: SymbolKind::FILE,
2153 range: AstRange::new(0..5, AstPosition::new(0, 0), AstPosition::new(0, 5)),
2154 selection_range: AstRange::new(
2155 0..5,
2156 AstPosition::new(0, 0),
2157 AstPosition::new(0, 5),
2158 ),
2159 children: Vec::new(),
2160 }]
2161 }
2162
2163 fn folding_ranges(&self, _: &Document) -> Vec<LexFoldingRange> {
2164 self.folding_called.fetch_add(1, Ordering::SeqCst);
2165 vec![LexFoldingRange {
2166 start_line: 0,
2167 start_character: Some(0),
2168 end_line: 1,
2169 end_character: Some(0),
2170 kind: Some(FoldingRangeKind::Region),
2171 }]
2172 }
2173
2174 fn hover(&self, _: &Document, position: AstPosition) -> Option<HoverResult> {
2175 self.hover_called.fetch_add(1, Ordering::SeqCst);
2176 *self.last_hover_position.lock().unwrap() = Some(position);
2177 Some(HoverResult {
2178 range: AstRange::new(0..5, AstPosition::new(0, 0), AstPosition::new(0, 5)),
2179 contents: "hover".into(),
2180 })
2181 }
2182
2183 fn goto_definition(&self, _: &Document, _: AstPosition) -> Vec<AstRange> {
2184 self.definition_called.fetch_add(1, Ordering::SeqCst);
2185 vec![AstRange::new(
2186 0..5,
2187 AstPosition::new(0, 0),
2188 AstPosition::new(0, 5),
2189 )]
2190 }
2191
2192 fn references(
2193 &self,
2194 _: &Document,
2195 _: AstPosition,
2196 include_declaration: bool,
2197 ) -> Vec<AstRange> {
2198 self.references_called.fetch_add(1, Ordering::SeqCst);
2199 *self.last_references_include.lock().unwrap() = Some(include_declaration);
2200 vec![AstRange::new(
2201 0..5,
2202 AstPosition::new(0, 0),
2203 AstPosition::new(0, 5),
2204 )]
2205 }
2206
2207 fn document_links(&self, _: &Document) -> Vec<AstDocumentLink> {
2208 self.document_links_called.fetch_add(1, Ordering::SeqCst);
2209 vec![AstDocumentLink::new(
2210 AstRange::new(0..5, AstPosition::new(0, 0), AstPosition::new(0, 5)),
2211 "https://example.com".to_string(),
2212 LinkType::Url,
2213 )]
2214 }
2215
2216 fn format_document(
2217 &self,
2218 _: &Document,
2219 _: &str,
2220 _: Option<FormattingRules>,
2221 ) -> Vec<TextEditSpan> {
2222 self.formatting_called.fetch_add(1, Ordering::SeqCst);
2223 vec![TextEditSpan {
2224 start: 0,
2225 end: 0,
2226 new_text: "formatted".into(),
2227 }]
2228 }
2229
2230 fn format_range(
2231 &self,
2232 _: &Document,
2233 _: &str,
2234 _: FormattingLineRange,
2235 _: Option<FormattingRules>,
2236 ) -> Vec<TextEditSpan> {
2237 self.range_formatting_called.fetch_add(1, Ordering::SeqCst);
2238 vec![TextEditSpan {
2239 start: 0,
2240 end: 0,
2241 new_text: "range".into(),
2242 }]
2243 }
2244
2245 fn completion(
2246 &self,
2247 _: &Document,
2248 _: AstPosition,
2249 _: Option<&str>,
2250 _: Option<&CompletionWorkspace>,
2251 _: Option<&str>,
2252 ) -> Vec<CompletionCandidate> {
2253 self.completion_called.fetch_add(1, Ordering::SeqCst);
2254 vec![CompletionCandidate {
2255 label: "completion".into(),
2256 detail: None,
2257 kind: CompletionItemKind::TEXT,
2258 insert_text: None,
2259 }]
2260 }
2261
2262 fn execute_command(&self, command: &str, _: &[Value]) -> Result<Option<Value>> {
2263 self.execute_command_called.fetch_add(1, Ordering::SeqCst);
2264 if command == "test.command" {
2265 Ok(Some(Value::String("executed".into())))
2266 } else {
2267 Ok(None)
2268 }
2269 }
2270 }
2271
2272 fn sample_uri() -> Url {
2273 Url::parse("file:///sample.lex").unwrap()
2274 }
2275
2276 fn sample_text() -> String {
2277 sample_source().to_string()
2278 }
2279
2280 fn offset_to_position(source: &str, offset: usize) -> AstPosition {
2281 let mut line = 0;
2282 let mut line_start = 0;
2283 for (idx, ch) in source.char_indices() {
2284 if idx >= offset {
2285 break;
2286 }
2287 if ch == '\n' {
2288 line += 1;
2289 line_start = idx + ch.len_utf8();
2290 }
2291 }
2292 AstPosition::new(line, offset - line_start)
2293 }
2294
2295 fn range_for_snippet(snippet: &str) -> AstRange {
2296 let source = sample_source();
2297 let start = source
2298 .find(snippet)
2299 .unwrap_or_else(|| panic!("snippet not found: {snippet}"));
2300 let end = start + snippet.len();
2301 let start_pos = offset_to_position(source, start);
2302 let end_pos = offset_to_position(source, end);
2303 AstRange::new(start..end, start_pos, end_pos)
2304 }
2305
2306 async fn open_sample_document(server: &LexLanguageServer<NoopClient, MockFeatureProvider>) {
2307 let uri = sample_uri();
2308 server
2309 .did_open(DidOpenTextDocumentParams {
2310 text_document: TextDocumentItem {
2311 uri,
2312 language_id: "lex".into(),
2313 version: 1,
2314 text: sample_text(),
2315 },
2316 })
2317 .await;
2318 }
2319
2320 #[test]
2321 fn encode_semantic_tokens_splits_multi_line_ranges() {
2322 let snippet = " CLI Example:\n lex build\n lex serve";
2323 let range = range_for_snippet(snippet);
2324 let tokens = vec![LexSemanticToken {
2325 kind: LexSemanticTokenKind::DocumentTitle,
2326 range,
2327 }];
2328 let source = sample_source();
2329 let encoded = encode_semantic_tokens(&tokens, source);
2330 assert_eq!(encoded.len(), 3);
2331 let snippet_offset = source
2332 .find(snippet)
2333 .expect("snippet not found in sample document");
2334 let mut cursor = 0;
2335 let lines: Vec<&str> = snippet.split('\n').collect();
2336 let mut expected_positions = Vec::new();
2337 for (idx, line) in lines.iter().enumerate() {
2338 let offset = snippet_offset + cursor;
2339 expected_positions.push(offset_to_position(source, offset));
2340 cursor += line.len();
2341 if idx < lines.len() - 1 {
2342 cursor += 1; }
2344 }
2345 let mut absolute_positions = Vec::new();
2346 let mut line = 0u32;
2347 let mut column = 0u32;
2348 for token in &encoded {
2349 line += token.delta_line;
2350 let start = if token.delta_line == 0 {
2351 column + token.delta_start
2352 } else {
2353 token.delta_start
2354 };
2355 column = start;
2356 absolute_positions.push((line, start));
2357 }
2358 for (actual, expected) in absolute_positions.iter().zip(expected_positions.iter()) {
2359 assert_eq!(actual.0, expected.line as u32);
2360 assert_eq!(actual.1, expected.column as u32);
2361 }
2362 let expected_len: usize = snippet.lines().map(|line| line.len()).sum();
2363 let actual_len: usize = encoded.iter().map(|token| token.length as usize).sum();
2364 assert_eq!(actual_len, expected_len);
2365 }
2366
2367 #[tokio::test]
2368 async fn semantic_tokens_call_feature_layer() {
2369 let provider = Arc::new(MockFeatureProvider::default());
2370 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2371 open_sample_document(&server).await;
2372
2373 let result = server
2374 .semantic_tokens_full(SemanticTokensParams {
2375 text_document: TextDocumentIdentifier { uri: sample_uri() },
2376 work_done_progress_params: Default::default(),
2377 partial_result_params: Default::default(),
2378 })
2379 .await
2380 .unwrap()
2381 .unwrap();
2382
2383 assert_eq!(provider.semantic_tokens_called.load(Ordering::SeqCst), 1);
2384 let data_len = match result {
2385 SemanticTokensResult::Tokens(tokens) => tokens.data.len(),
2386 SemanticTokensResult::Partial(partial) => partial.data.len(),
2387 };
2388 assert!(data_len > 0);
2389 }
2390
2391 #[tokio::test]
2392 async fn document_symbols_call_feature_layer() {
2393 let provider = Arc::new(MockFeatureProvider::default());
2394 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2395 open_sample_document(&server).await;
2396
2397 let response = server
2398 .document_symbol(DocumentSymbolParams {
2399 text_document: TextDocumentIdentifier { uri: sample_uri() },
2400 work_done_progress_params: Default::default(),
2401 partial_result_params: Default::default(),
2402 })
2403 .await
2404 .unwrap()
2405 .unwrap();
2406
2407 match response {
2408 DocumentSymbolResponse::Nested(symbols) => assert!(!symbols.is_empty()),
2409 _ => panic!("unexpected symbol response"),
2410 }
2411 assert_eq!(provider.document_symbols_called.load(Ordering::SeqCst), 1);
2412 }
2413
2414 #[tokio::test]
2415 async fn hover_uses_feature_provider_position() {
2416 let provider = Arc::new(MockFeatureProvider::default());
2417 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2418 open_sample_document(&server).await;
2419
2420 let hover = server
2421 .hover(HoverParams {
2422 text_document_position_params: TextDocumentPositionParams {
2423 text_document: TextDocumentIdentifier { uri: sample_uri() },
2424 position: Position::new(0, 0),
2425 },
2426 work_done_progress_params: Default::default(),
2427 })
2428 .await
2429 .unwrap()
2430 .unwrap();
2431
2432 assert!(matches!(hover.contents, HoverContents::Markup(_)));
2433 assert_eq!(provider.hover_called.load(Ordering::SeqCst), 1);
2434 let stored = provider.last_hover_position.lock().unwrap().unwrap();
2435 assert_eq!(stored.line, 0);
2436 assert_eq!(stored.column, 0);
2437 }
2438
2439 #[tokio::test]
2440 async fn folding_range_uses_feature_provider() {
2441 let provider = Arc::new(MockFeatureProvider::default());
2442 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2443 open_sample_document(&server).await;
2444
2445 let ranges = server
2446 .folding_range(FoldingRangeParams {
2447 text_document: TextDocumentIdentifier { uri: sample_uri() },
2448 work_done_progress_params: Default::default(),
2449 partial_result_params: Default::default(),
2450 })
2451 .await
2452 .unwrap()
2453 .unwrap();
2454
2455 assert_eq!(provider.folding_called.load(Ordering::SeqCst), 1);
2456 assert_eq!(ranges.len(), 1);
2457 }
2458
2459 #[tokio::test]
2460 async fn goto_definition_uses_feature_provider() {
2461 let provider = Arc::new(MockFeatureProvider::default());
2462 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2463 open_sample_document(&server).await;
2464
2465 let response = server
2466 .goto_definition(GotoDefinitionParams {
2467 text_document_position_params: TextDocumentPositionParams {
2468 text_document: TextDocumentIdentifier { uri: sample_uri() },
2469 position: Position::new(0, 0),
2470 },
2471 work_done_progress_params: Default::default(),
2472 partial_result_params: Default::default(),
2473 })
2474 .await
2475 .unwrap()
2476 .unwrap();
2477
2478 assert_eq!(provider.definition_called.load(Ordering::SeqCst), 1);
2479 match response {
2480 GotoDefinitionResponse::Array(locations) => assert_eq!(locations.len(), 1),
2481 _ => panic!("unexpected goto definition response"),
2482 }
2483 }
2484
2485 #[derive(Deserialize)]
2486 struct SnippetResponse {
2487 text: String,
2488 #[serde(rename = "cursorOffset")]
2489 cursor_offset: usize,
2490 }
2491
2492 #[tokio::test]
2493 async fn execute_insert_commands() {
2494 let provider = Arc::new(MockFeatureProvider::default());
2495 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2496 open_sample_document(&server).await;
2497
2498 let temp_dir = tempdir().unwrap();
2499 let asset_file = temp_dir.path().join("diagram.png");
2500 fs::write(&asset_file, [0u8, 159u8, 146u8, 150u8]).unwrap();
2501
2502 let params = ExecuteCommandParams {
2503 command: commands::COMMAND_INSERT_ASSET.to_string(),
2504 arguments: vec![
2505 serde_json::to_value(sample_uri().to_string()).unwrap(),
2506 serde_json::to_value(Position::new(0, 0)).unwrap(),
2507 serde_json::to_value(asset_file.to_string_lossy()).unwrap(),
2508 ],
2509 work_done_progress_params: Default::default(),
2510 };
2511 let result = server.execute_command(params).await.unwrap();
2512 let snippet: SnippetResponse = serde_json::from_value(result.unwrap()).unwrap();
2513 assert!(snippet.text.contains(":: image"));
2514 assert!(snippet.text.contains(asset_file.to_string_lossy().as_ref()));
2515
2516 let verbatim_file = temp_dir.path().join("example.py");
2517 fs::write(&verbatim_file, "print('hi')\n").unwrap();
2518
2519 let params = ExecuteCommandParams {
2520 command: commands::COMMAND_INSERT_VERBATIM.to_string(),
2521 arguments: vec![
2522 serde_json::to_value(sample_uri().to_string()).unwrap(),
2523 serde_json::to_value(Position::new(0, 0)).unwrap(),
2524 serde_json::to_value(verbatim_file.to_string_lossy()).unwrap(),
2525 ],
2526 work_done_progress_params: Default::default(),
2527 };
2528 let result = server.execute_command(params).await.unwrap();
2529 let snippet: SnippetResponse = serde_json::from_value(result.unwrap()).unwrap();
2530 assert!(snippet.text.contains(":: python"));
2531 assert!(snippet.text.contains("print('hi')"));
2532 assert_eq!(snippet.cursor_offset, 0);
2533 }
2534
2535 #[tokio::test]
2536 async fn execute_annotation_navigation_commands() {
2537 let provider = Arc::new(MockFeatureProvider::default());
2538 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2539 let uri = Url::parse("file:///annotations.lex").unwrap();
2540 let text = ":: note ::\n First\n::\n\n:: note ::\n Second\n::\n";
2541 server
2542 .did_open(DidOpenTextDocumentParams {
2543 text_document: TextDocumentItem {
2544 uri: uri.clone(),
2545 language_id: "lex".into(),
2546 version: 1,
2547 text: text.to_string(),
2548 },
2549 })
2550 .await;
2551
2552 let next_params = ExecuteCommandParams {
2553 command: commands::COMMAND_NEXT_ANNOTATION.to_string(),
2554 arguments: vec![
2555 serde_json::to_value(uri.to_string()).unwrap(),
2556 serde_json::to_value(Position::new(0, 0)).unwrap(),
2557 ],
2558 work_done_progress_params: Default::default(),
2559 };
2560 let next_location: Location =
2561 serde_json::from_value(server.execute_command(next_params).await.unwrap().unwrap())
2562 .unwrap();
2563 assert_eq!(next_location.range.start.line, 0);
2564
2565 let previous_params = ExecuteCommandParams {
2566 command: commands::COMMAND_PREVIOUS_ANNOTATION.to_string(),
2567 arguments: vec![
2568 serde_json::to_value(uri.to_string()).unwrap(),
2569 serde_json::to_value(Position::new(0, 0)).unwrap(),
2570 ],
2571 work_done_progress_params: Default::default(),
2572 };
2573 let previous_location: Location = serde_json::from_value(
2574 server
2575 .execute_command(previous_params)
2576 .await
2577 .unwrap()
2578 .unwrap(),
2579 )
2580 .unwrap();
2581 assert_eq!(previous_location.range.start.line, 4);
2582
2583 let resolve_params = ExecuteCommandParams {
2584 command: commands::COMMAND_RESOLVE_ANNOTATION.to_string(),
2585 arguments: vec![
2586 serde_json::to_value(uri.to_string()).unwrap(),
2587 serde_json::to_value(Position::new(0, 0)).unwrap(),
2588 ],
2589 work_done_progress_params: Default::default(),
2590 };
2591 let edit_value = server
2592 .execute_command(resolve_params)
2593 .await
2594 .unwrap()
2595 .unwrap();
2596 let workspace_edit: tower_lsp::lsp_types::WorkspaceEdit =
2597 serde_json::from_value(edit_value).unwrap();
2598 let changes = workspace_edit.changes.expect("workspace edit changes");
2599 let edits = changes.get(&uri).expect("edits for document");
2600 assert_eq!(edits[0].new_text, ":: note status=resolved ::");
2601 }
2602
2603 #[tokio::test]
2604 async fn references_use_feature_provider() {
2605 let provider = Arc::new(MockFeatureProvider::default());
2606 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2607 open_sample_document(&server).await;
2608
2609 let result = server
2610 .references(ReferenceParams {
2611 text_document_position: TextDocumentPositionParams {
2612 text_document: TextDocumentIdentifier { uri: sample_uri() },
2613 position: Position::new(0, 0),
2614 },
2615 context: ReferenceContext {
2616 include_declaration: true,
2617 },
2618 work_done_progress_params: Default::default(),
2619 partial_result_params: Default::default(),
2620 })
2621 .await
2622 .unwrap()
2623 .unwrap();
2624
2625 assert_eq!(provider.references_called.load(Ordering::SeqCst), 1);
2626 assert_eq!(result.len(), 1);
2627 assert_eq!(
2628 *provider.last_references_include.lock().unwrap(),
2629 Some(true)
2630 );
2631 }
2632
2633 #[tokio::test]
2634 async fn document_links_use_feature_provider() {
2635 let provider = Arc::new(MockFeatureProvider::default());
2636 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2637 open_sample_document(&server).await;
2638
2639 let links = server
2640 .document_link(DocumentLinkParams {
2641 text_document: TextDocumentIdentifier { uri: sample_uri() },
2642 work_done_progress_params: Default::default(),
2643 partial_result_params: Default::default(),
2644 })
2645 .await
2646 .unwrap()
2647 .unwrap();
2648
2649 assert_eq!(provider.document_links_called.load(Ordering::SeqCst), 1);
2650 assert_eq!(links.len(), 1);
2651 assert_eq!(
2652 links[0].target.as_ref().map(|url| url.as_str()),
2653 Some("https://example.com/")
2654 );
2655 }
2656
2657 #[tokio::test]
2658 async fn formatting_uses_feature_provider() {
2659 let provider = Arc::new(MockFeatureProvider::default());
2660 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2661 open_sample_document(&server).await;
2662
2663 let edits = server
2664 .formatting(DocumentFormattingParams {
2665 text_document: TextDocumentIdentifier { uri: sample_uri() },
2666 options: FormattingOptions::default(),
2667 work_done_progress_params: Default::default(),
2668 })
2669 .await
2670 .unwrap()
2671 .unwrap();
2672
2673 assert_eq!(provider.formatting_called.load(Ordering::SeqCst), 1);
2674 assert_eq!(edits.len(), 1);
2675 assert_eq!(edits[0].new_text, "formatted");
2676 }
2677
2678 #[tokio::test]
2679 async fn range_formatting_uses_feature_provider() {
2680 let provider = Arc::new(MockFeatureProvider::default());
2681 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2682 open_sample_document(&server).await;
2683
2684 let edits = server
2685 .range_formatting(DocumentRangeFormattingParams {
2686 text_document: TextDocumentIdentifier { uri: sample_uri() },
2687 range: Range {
2688 start: Position::new(0, 0),
2689 end: Position::new(0, 0),
2690 },
2691 options: FormattingOptions::default(),
2692 work_done_progress_params: Default::default(),
2693 })
2694 .await
2695 .unwrap()
2696 .unwrap();
2697
2698 assert_eq!(provider.range_formatting_called.load(Ordering::SeqCst), 1);
2699 assert_eq!(edits.len(), 1);
2700 assert_eq!(edits[0].new_text, "range");
2701 }
2702
2703 #[tokio::test]
2704 async fn semantic_tokens_returns_none_when_document_missing() {
2705 let provider = Arc::new(MockFeatureProvider::default());
2706 let server = LexLanguageServer::with_features(NoopClient, provider);
2707
2708 let result = server
2709 .semantic_tokens_full(SemanticTokensParams {
2710 text_document: TextDocumentIdentifier { uri: sample_uri() },
2711 work_done_progress_params: Default::default(),
2712 partial_result_params: Default::default(),
2713 })
2714 .await
2715 .unwrap();
2716
2717 assert!(result.is_none());
2718 }
2719
2720 #[tokio::test]
2721 async fn execute_command_uses_feature_provider() {
2722 let provider = Arc::new(MockFeatureProvider::default());
2723 let server = LexLanguageServer::with_features(NoopClient, provider.clone());
2724
2725 let result = server
2726 .execute_command(ExecuteCommandParams {
2727 command: "test.command".into(),
2728 arguments: vec![],
2729 work_done_progress_params: Default::default(),
2730 })
2731 .await
2732 .unwrap()
2733 .unwrap();
2734
2735 assert_eq!(provider.execute_command_called.load(Ordering::SeqCst), 1);
2736 assert_eq!(result, Value::String("executed".into()));
2737 }
2738
2739 #[tokio::test]
2740 async fn hover_returns_none_without_document_entry() {
2741 let provider = Arc::new(MockFeatureProvider::default());
2742 let server = LexLanguageServer::with_features(NoopClient, provider);
2743
2744 let hover = server
2745 .hover(HoverParams {
2746 text_document_position_params: TextDocumentPositionParams {
2747 text_document: TextDocumentIdentifier { uri: sample_uri() },
2748 position: Position::new(0, 0),
2749 },
2750 work_done_progress_params: Default::default(),
2751 })
2752 .await
2753 .unwrap();
2754
2755 assert!(hover.is_none());
2756 }
2757
2758 #[test]
2759 fn apply_formatting_overrides_noop_without_lex_properties() {
2760 let options = FormattingOptions {
2761 tab_size: 4,
2762 insert_spaces: true,
2763 properties: Default::default(),
2764 trim_trailing_whitespace: None,
2765 insert_final_newline: None,
2766 trim_final_newlines: None,
2767 };
2768 let mut rules = FormattingRules::default();
2769 let original = rules.clone();
2770 apply_formatting_overrides(&mut rules, &options);
2771 assert_eq!(rules.indent_string, original.indent_string);
2772 assert_eq!(rules.max_blank_lines, original.max_blank_lines);
2773 }
2774
2775 #[test]
2776 fn apply_formatting_overrides_applies_lex_properties() {
2777 use std::collections::HashMap;
2778
2779 let mut properties = HashMap::new();
2780 properties.insert(
2781 "lex.indent_string".to_string(),
2782 FormattingProperty::String(" ".to_string()),
2783 );
2784 properties.insert(
2785 "lex.max_blank_lines".to_string(),
2786 FormattingProperty::Number(3),
2787 );
2788 properties.insert(
2789 "lex.normalize_seq_markers".to_string(),
2790 FormattingProperty::Bool(false),
2791 );
2792 properties.insert(
2793 "lex.unordered_seq_marker".to_string(),
2794 FormattingProperty::String("*".to_string()),
2795 );
2796
2797 let options = FormattingOptions {
2798 tab_size: 4,
2799 insert_spaces: true,
2800 properties,
2801 trim_trailing_whitespace: None,
2802 insert_final_newline: None,
2803 trim_final_newlines: None,
2804 };
2805
2806 let mut rules = FormattingRules::default();
2807 apply_formatting_overrides(&mut rules, &options);
2808 assert_eq!(rules.indent_string, " ");
2809 assert_eq!(rules.max_blank_lines, 3);
2810 assert!(!rules.normalize_seq_markers);
2811 assert_eq!(rules.unordered_seq_marker, '*');
2812 }
2813
2814 #[tokio::test]
2815 async fn did_change_workspace_folders_adds_roots() {
2816 let provider = Arc::new(MockFeatureProvider::default());
2817 let server = LexLanguageServer::with_features(NoopClient, provider);
2818
2819 server
2821 .initialize(InitializeParams {
2822 root_uri: Some(Url::from_file_path("/initial").unwrap()),
2823 ..Default::default()
2824 })
2825 .await
2826 .unwrap();
2827
2828 assert_eq!(server.workspace_roots.read().await.len(), 1);
2829
2830 server
2832 .did_change_workspace_folders(DidChangeWorkspaceFoldersParams {
2833 event: lsp_types::WorkspaceFoldersChangeEvent {
2834 added: vec![lsp_types::WorkspaceFolder {
2835 uri: Url::from_file_path("/added").unwrap(),
2836 name: "added".to_string(),
2837 }],
2838 removed: vec![],
2839 },
2840 })
2841 .await;
2842
2843 let roots = server.workspace_roots.read().await;
2844 assert_eq!(roots.len(), 2);
2845 assert_eq!(roots[1], PathBuf::from("/added"));
2846 }
2847
2848 #[tokio::test]
2849 async fn did_change_workspace_folders_removes_roots() {
2850 let provider = Arc::new(MockFeatureProvider::default());
2851 let server = LexLanguageServer::with_features(NoopClient, provider);
2852
2853 server
2854 .initialize(InitializeParams {
2855 root_uri: Some(Url::from_file_path("/initial").unwrap()),
2856 ..Default::default()
2857 })
2858 .await
2859 .unwrap();
2860
2861 server
2863 .did_change_workspace_folders(DidChangeWorkspaceFoldersParams {
2864 event: lsp_types::WorkspaceFoldersChangeEvent {
2865 added: vec![lsp_types::WorkspaceFolder {
2866 uri: Url::from_file_path("/new-root").unwrap(),
2867 name: "new-root".to_string(),
2868 }],
2869 removed: vec![lsp_types::WorkspaceFolder {
2870 uri: Url::from_file_path("/initial").unwrap(),
2871 name: "initial".to_string(),
2872 }],
2873 },
2874 })
2875 .await;
2876
2877 let roots = server.workspace_roots.read().await;
2878 assert_eq!(roots.len(), 1);
2879 assert_eq!(roots[0], PathBuf::from("/new-root"));
2880 }
2881
2882 #[tokio::test]
2883 async fn did_change_workspace_folders_does_not_duplicate() {
2884 let provider = Arc::new(MockFeatureProvider::default());
2885 let server = LexLanguageServer::with_features(NoopClient, provider);
2886
2887 server
2888 .initialize(InitializeParams {
2889 root_uri: Some(Url::from_file_path("/root").unwrap()),
2890 ..Default::default()
2891 })
2892 .await
2893 .unwrap();
2894
2895 server
2897 .did_change_workspace_folders(DidChangeWorkspaceFoldersParams {
2898 event: lsp_types::WorkspaceFoldersChangeEvent {
2899 added: vec![lsp_types::WorkspaceFolder {
2900 uri: Url::from_file_path("/root").unwrap(),
2901 name: "root".to_string(),
2902 }],
2903 removed: vec![],
2904 },
2905 })
2906 .await;
2907
2908 assert_eq!(server.workspace_roots.read().await.len(), 1);
2909 }
2910
2911 #[tokio::test]
2912 async fn initialize_advertises_workspace_folder_support() {
2913 let provider = Arc::new(MockFeatureProvider::default());
2914 let server = LexLanguageServer::with_features(NoopClient, provider);
2915
2916 let result = server
2917 .initialize(InitializeParams::default())
2918 .await
2919 .unwrap();
2920
2921 let workspace = result
2922 .capabilities
2923 .workspace
2924 .expect("workspace capabilities");
2925 let folders = workspace
2926 .workspace_folders
2927 .expect("workspace folder support");
2928 assert_eq!(folders.supported, Some(true));
2929 assert_eq!(folders.change_notifications, Some(OneOf::Left(true)));
2930 }
2931
2932 type DiagnosticLog = Arc<Mutex<Vec<(Url, Vec<Diagnostic>)>>>;
2942
2943 #[derive(Clone, Default)]
2944 struct CapturingClient {
2945 last_diagnostics: DiagnosticLog,
2946 }
2947
2948 #[async_trait]
2949 impl LspClient for CapturingClient {
2950 async fn publish_diagnostics(&self, uri: Url, diags: Vec<Diagnostic>, _: Option<i32>) {
2951 self.last_diagnostics.lock().unwrap().push((uri, diags));
2952 }
2953 async fn show_message(&self, _: MessageType, _: String) {}
2954 }
2955 #[async_trait]
2956 impl crate::trust_prompt::LspTrustRequester for CapturingClient {
2957 async fn send_trust_request(
2958 &self,
2959 _: crate::trust_prompt::TrustRequestParams,
2960 ) -> tower_lsp::jsonrpc::Result<crate::trust_prompt::TrustResponse> {
2961 Ok(crate::trust_prompt::TrustResponse {
2964 decision: "denied".into(),
2965 reason: Some("test client".into()),
2966 })
2967 }
2968 }
2969
2970 impl CapturingClient {
2971 fn diagnostics_for(&self, uri: &Url) -> Vec<Diagnostic> {
2972 self.last_diagnostics
2973 .lock()
2974 .unwrap()
2975 .iter()
2976 .rev()
2977 .find(|(u, _)| u == uri)
2978 .map(|(_, d)| d.clone())
2979 .unwrap_or_default()
2980 }
2981 }
2982
2983 async fn open_in_tempdir(
2988 files: &[(&str, &str)],
2989 entry: &str,
2990 ) -> (
2991 LexLanguageServer<CapturingClient, DefaultFeatureProvider>,
2992 CapturingClient,
2993 Url,
2994 tempfile::TempDir,
2995 ) {
2996 let dir = tempdir().expect("tempdir");
2997 for (rel, contents) in files {
2998 let path = dir.path().join(rel);
2999 if let Some(parent) = path.parent() {
3000 std::fs::create_dir_all(parent).expect("mkdir -p");
3001 }
3002 std::fs::write(&path, contents).expect("write fixture");
3003 }
3004 let entry_path = dir.path().join(entry);
3005 let entry_text = std::fs::read_to_string(&entry_path).expect("read entry");
3006 let uri = Url::from_file_path(&entry_path).expect("file uri");
3007
3008 let client = CapturingClient::default();
3009 let server = LexLanguageServer::with_features(
3010 client.clone(),
3011 Arc::new(DefaultFeatureProvider::new()),
3012 );
3013
3014 server
3015 .did_open(DidOpenTextDocumentParams {
3016 text_document: TextDocumentItem {
3017 uri: uri.clone(),
3018 language_id: "lex".into(),
3019 version: 1,
3020 text: entry_text,
3021 },
3022 })
3023 .await;
3024
3025 (server, client, uri, dir)
3026 }
3027
3028 fn has_diag_with_code(diags: &[Diagnostic], code: &str) -> bool {
3029 diags.iter().any(|d| {
3030 matches!(
3031 &d.code,
3032 Some(tower_lsp::lsp_types::NumberOrString::String(c)) if c == code
3033 )
3034 })
3035 }
3036
3037 #[tokio::test]
3038 async fn includes_did_open_resolves_and_publishes_no_include_diagnostic() {
3039 let (_server, client, uri, _dir) = open_in_tempdir(
3040 &[
3041 (
3042 "main.lex",
3043 "1. Host\n\n :: lex.include src=\"chapter.lex\" ::\n",
3044 ),
3045 ("chapter.lex", "1.1 Chapter\n\n Body of chapter.\n"),
3046 ],
3047 "main.lex",
3048 )
3049 .await;
3050
3051 let diags = client.diagnostics_for(&uri);
3052 assert!(
3053 !diags.iter().any(|d| matches!(
3054 &d.code,
3055 Some(tower_lsp::lsp_types::NumberOrString::String(c)) if c.starts_with("include-")
3056 )),
3057 "successful include resolution should produce no include-* diagnostics, got {diags:?}"
3058 );
3059 }
3060
3061 #[tokio::test]
3062 async fn includes_missing_target_emits_diagnostic_with_path() {
3063 let (_server, client, uri, _dir) = open_in_tempdir(
3069 &[("main.lex", ":: lex.include src=\"missing.lex\" ::\n")],
3070 "main.lex",
3071 )
3072 .await;
3073
3074 let diags = client.diagnostics_for(&uri);
3075 assert!(
3076 has_diag_with_code(&diags, "include-not-found"),
3077 "missing include should surface include-not-found, got {diags:?}"
3078 );
3079 assert!(
3080 diags.iter().any(|d| d.message.contains("missing.lex")),
3081 "diagnostic should name the missing file, got {diags:?}"
3082 );
3083 let not_found = diags
3089 .iter()
3090 .find(|d| {
3091 matches!(
3092 &d.code,
3093 Some(tower_lsp::lsp_types::NumberOrString::String(c)) if c == "include-not-found"
3094 )
3095 })
3096 .expect("not-found diag");
3097 let r = ¬_found.range;
3098 assert!(
3099 r.end.line > r.start.line || r.end.character > r.start.character,
3100 "include-not-found should span the annotation, not collapse to a point; got {r:?}",
3101 );
3102 }
3103
3104 #[tokio::test]
3105 async fn includes_cycle_emits_diagnostic_pointing_at_include_site() {
3106 let (_server, client, uri, _dir) = open_in_tempdir(
3107 &[
3108 ("main.lex", ":: lex.include src=\"a.lex\" ::\n"),
3109 ("a.lex", ":: lex.include src=\"b.lex\" ::\n"),
3110 ("b.lex", ":: lex.include src=\"a.lex\" ::\n"),
3111 ],
3112 "main.lex",
3113 )
3114 .await;
3115
3116 let diags = client.diagnostics_for(&uri);
3117 assert!(
3118 has_diag_with_code(&diags, "include-cycle"),
3119 "cycle should surface include-cycle, got {diags:?}"
3120 );
3121 let cycle = diags
3124 .iter()
3125 .find(|d| {
3126 matches!(
3127 &d.code,
3128 Some(tower_lsp::lsp_types::NumberOrString::String(c)) if c == "include-cycle"
3129 )
3130 })
3131 .expect("cycle diag");
3132 assert_eq!(cycle.range.start.line, 0);
3134 }
3135
3136 #[tokio::test]
3137 async fn includes_root_escape_emits_diagnostic() {
3138 let (_server, client, uri, _dir) = open_in_tempdir(
3139 &[(
3140 "main.lex",
3141 "1. Host\n\n :: lex.include src=\"../../etc/passwd\" ::\n",
3142 )],
3143 "main.lex",
3144 )
3145 .await;
3146
3147 let diags = client.diagnostics_for(&uri);
3148 assert!(
3149 has_diag_with_code(&diags, "include-root-escape"),
3150 "root escape should surface include-root-escape, got {diags:?}"
3151 );
3152 }
3153
3154 #[tokio::test]
3155 async fn includes_stored_tree_remains_unresolved_so_positions_match_host_buffer() {
3156 let (server, _client, uri, _dir) = open_in_tempdir(
3164 &[
3165 ("main.lex", ":: lex.include src=\"chapter.lex\" ::\n"),
3166 (
3167 "chapter.lex",
3168 "1. Spliced Chapter\n\n Body content here.\n",
3169 ),
3170 ],
3171 "main.lex",
3172 )
3173 .await;
3174
3175 let entry = server.document_entry(&uri).await.expect("entry stored");
3176 use lex_core::lex::ast::elements::content_item::ContentItem;
3180 let titles: Vec<String> = entry
3181 .document
3182 .root
3183 .children
3184 .iter()
3185 .filter_map(|i| match i {
3186 ContentItem::Session(s) => Some(s.title.as_string().to_string()),
3187 _ => None,
3188 })
3189 .collect();
3190 assert!(
3191 !titles.iter().any(|t| t == "1. Spliced Chapter"),
3192 "spliced chapter must NOT be in the stored host tree (its Ranges \
3193 would point at the wrong buffer); got titles {titles:?}"
3194 );
3195 }
3196
3197 fn goto_at(uri: &Url, line: u32, character: u32) -> GotoDefinitionParams {
3204 GotoDefinitionParams {
3205 text_document_position_params: TextDocumentPositionParams {
3206 text_document: TextDocumentIdentifier { uri: uri.clone() },
3207 position: Position { line, character },
3208 },
3209 work_done_progress_params: Default::default(),
3210 partial_result_params: Default::default(),
3211 }
3212 }
3213
3214 fn hover_at(uri: &Url, line: u32, character: u32) -> HoverParams {
3215 HoverParams {
3216 text_document_position_params: TextDocumentPositionParams {
3217 text_document: TextDocumentIdentifier { uri: uri.clone() },
3218 position: Position { line, character },
3219 },
3220 work_done_progress_params: Default::default(),
3221 }
3222 }
3223
3224 #[tokio::test]
3225 async fn goto_definition_on_include_returns_target_file_location() {
3226 let (server, _client, uri, dir) = open_in_tempdir(
3227 &[
3228 ("main.lex", ":: lex.include src=\"chapter.lex\" ::\n"),
3229 ("chapter.lex", "1. Chapter\n\n Body.\n"),
3230 ],
3231 "main.lex",
3232 )
3233 .await;
3234
3235 let response = server.goto_definition(goto_at(&uri, 0, 5)).await.unwrap();
3237 let location = match response {
3238 Some(GotoDefinitionResponse::Scalar(loc)) => loc,
3239 other => panic!("expected scalar Location, got {other:?}"),
3240 };
3241
3242 let expected = Url::from_file_path(absolutize_path(&dir.path().join("chapter.lex")))
3245 .expect("file uri");
3246 assert_eq!(location.uri, expected);
3247 assert_eq!(location.range.start.line, 0);
3249 assert_eq!(location.range.start.character, 0);
3250 }
3251
3252 #[tokio::test]
3253 async fn goto_definition_off_include_falls_through_to_normal_logic() {
3254 let (server, _client, uri, _dir) = open_in_tempdir(
3259 &[("main.lex", "1. Chapter\n\n Just a paragraph.\n")],
3260 "main.lex",
3261 )
3262 .await;
3263 let response = server.goto_definition(goto_at(&uri, 2, 8)).await.unwrap();
3264 assert!(
3265 response.is_none(),
3266 "non-include cursor should fall through, got {response:?}"
3267 );
3268 }
3269
3270 #[tokio::test]
3271 async fn hover_on_include_returns_preview_of_target_file() {
3272 let (server, _client, uri, _dir) = open_in_tempdir(
3273 &[
3274 ("main.lex", ":: lex.include src=\"chapter.lex\" ::\n"),
3275 ("chapter.lex", "1. Chapter\n\n Body line.\n"),
3276 ],
3277 "main.lex",
3278 )
3279 .await;
3280
3281 let hover = server
3282 .hover(hover_at(&uri, 0, 5))
3283 .await
3284 .unwrap()
3285 .expect("hover");
3286 let body = match hover.contents {
3287 HoverContents::Markup(m) => m.value,
3288 other => panic!("expected markup hover, got {other:?}"),
3289 };
3290 assert!(
3292 body.contains("chapter.lex"),
3293 "hover should name target: {body}"
3294 );
3295 assert!(
3297 body.contains("1. Chapter"),
3298 "hover should preview content: {body}"
3299 );
3300 }
3301
3302 #[tokio::test]
3303 async fn hover_off_include_falls_through_to_normal_hover() {
3304 let (server, _client, uri, _dir) = open_in_tempdir(
3308 &[("main.lex", "1. Chapter\n\n Just text.\n")],
3309 "main.lex",
3310 )
3311 .await;
3312 let hover = server.hover(hover_at(&uri, 2, 8)).await.unwrap();
3313 if let Some(h) = hover {
3314 let body = match h.contents {
3317 HoverContents::Markup(m) => m.value,
3318 _ => String::new(),
3319 };
3320 assert!(
3321 !body.contains("lex.include"),
3322 "non-include cursor must not get include preview, got {body}"
3323 );
3324 }
3325 }
3326
3327 #[tokio::test]
3328 async fn goto_definition_on_include_with_missing_target_returns_none() {
3329 let (server, _client, uri, _dir) = open_in_tempdir(
3335 &[("main.lex", ":: lex.include src=\"missing.lex\" ::\n")],
3336 "main.lex",
3337 )
3338 .await;
3339 let response = server.goto_definition(goto_at(&uri, 0, 5)).await.unwrap();
3340 assert!(
3341 response.is_none(),
3342 "goto-def must return None for missing targets, got {response:?}"
3343 );
3344 }
3345
3346 #[tokio::test]
3351 async fn extract_to_include_returns_workspace_edit_with_create_and_replace() {
3352 let host_text = "Doc\n===\n\n1. Section\n\n Some content.\n More content.\n";
3353 let (server, _client, uri, dir) =
3354 open_in_tempdir(&[("main.lex", host_text)], "main.lex").await;
3355
3356 let range = Range::new(Position::new(5, 4), Position::new(7, 0));
3358 let result = server
3359 .execute_command(ExecuteCommandParams {
3360 command: commands::COMMAND_EXTRACT_TO_INCLUDE.to_string(),
3361 arguments: vec![
3362 Value::String(uri.to_string()),
3363 serde_json::to_value(range).unwrap(),
3364 Value::String("section-body.lex".to_string()),
3365 ],
3366 work_done_progress_params: Default::default(),
3367 })
3368 .await
3369 .unwrap()
3370 .expect("extract command should return WorkspaceEdit");
3371
3372 let edit: tower_lsp::lsp_types::WorkspaceEdit = serde_json::from_value(result).unwrap();
3373 let ops = match edit.document_changes.unwrap() {
3374 tower_lsp::lsp_types::DocumentChanges::Operations(ops) => ops,
3375 _ => panic!("expected operations"),
3376 };
3377 assert_eq!(ops.len(), 3, "create + target-content + host-replace");
3378
3379 match &ops[0] {
3381 tower_lsp::lsp_types::DocumentChangeOperation::Op(
3382 tower_lsp::lsp_types::ResourceOp::Create(c),
3383 ) => {
3384 assert!(c.uri.path().ends_with("section-body.lex"));
3385 }
3386 other => panic!("expected CreateFile, got {other:?}"),
3387 }
3388
3389 let target_text = match &ops[1] {
3391 tower_lsp::lsp_types::DocumentChangeOperation::Edit(e) => match &e.edits[0] {
3392 OneOf::Left(t) => t.new_text.clone(),
3393 _ => panic!("unexpected edit shape"),
3394 },
3395 _ => panic!("expected TextDocumentEdit for target"),
3396 };
3397 assert!(
3398 target_text.contains("Some content.") && target_text.contains("More content."),
3399 "target should hold the extracted body, got: {target_text:?}"
3400 );
3401 assert!(
3403 target_text.starts_with("Some content."),
3404 "expected indent shift to drop leading 4 spaces, got: {target_text:?}"
3405 );
3406
3407 let host_replace = match &ops[2] {
3409 tower_lsp::lsp_types::DocumentChangeOperation::Edit(e) => match &e.edits[0] {
3410 OneOf::Left(t) => t.new_text.clone(),
3411 _ => panic!("unexpected edit shape"),
3412 },
3413 _ => panic!("expected TextDocumentEdit for host"),
3414 };
3415 assert_eq!(
3416 host_replace,
3417 " :: lex.include src=\"section-body.lex\" ::"
3418 );
3419
3420 drop(dir);
3422 }
3423
3424 #[tokio::test]
3425 async fn extract_to_include_surfaces_validation_errors_as_invalid_params() {
3426 let host_text = "Doc\n===\n\n Body text.\n";
3427 let (server, _client, uri, _dir) =
3428 open_in_tempdir(&[("main.lex", host_text)], "main.lex").await;
3429
3430 let range = Range::new(Position::new(3, 4), Position::new(4, 0));
3431 let err = server
3432 .execute_command(ExecuteCommandParams {
3433 command: commands::COMMAND_EXTRACT_TO_INCLUDE.to_string(),
3434 arguments: vec![
3435 Value::String(uri.to_string()),
3436 serde_json::to_value(range).unwrap(),
3437 Value::String("https://elsewhere/foo.lex".to_string()),
3438 ],
3439 work_done_progress_params: Default::default(),
3440 })
3441 .await
3442 .unwrap_err();
3443 assert!(
3444 err.message.contains("URL"),
3445 "expected URL-scheme error message, got: {}",
3446 err.message
3447 );
3448 }
3449
3450 #[tokio::test]
3451 async fn extract_to_include_capability_advertises_command() {
3452 let provider = Arc::new(MockFeatureProvider::default());
3453 let server = LexLanguageServer::with_features(NoopClient, provider);
3454 let init = server
3455 .initialize(InitializeParams::default())
3456 .await
3457 .unwrap();
3458 let advertised = init
3459 .capabilities
3460 .execute_command_provider
3461 .expect("execute_command_provider")
3462 .commands;
3463 assert!(
3464 advertised.contains(&commands::COMMAND_EXTRACT_TO_INCLUDE.to_string()),
3465 "extractToInclude must be in advertised commands, got: {advertised:?}"
3466 );
3467 }
3468
3469 #[test]
3476 fn slice_text_by_range_uses_utf8_byte_offsets() {
3477 let text = "café\nrestaurant\n";
3478 let range = Range::new(Position::new(0, 0), Position::new(0, 5));
3480 assert_eq!(slice_text_by_range(text, range).as_deref(), Some("café"));
3481
3482 let bad = Range::new(Position::new(0, 0), Position::new(0, 4));
3484 assert!(slice_text_by_range(text, bad).is_none());
3485
3486 let multi = Range::new(Position::new(0, 0), Position::new(1, 10));
3488 assert_eq!(
3489 slice_text_by_range(text, multi).as_deref(),
3490 Some("café\nrestaurant")
3491 );
3492 }
3493
3494 #[tokio::test]
3495 async fn includes_untitled_uri_skips_resolution_without_error() {
3496 let client = CapturingClient::default();
3500 let server = LexLanguageServer::with_features(
3501 client.clone(),
3502 Arc::new(DefaultFeatureProvider::new()),
3503 );
3504 let uri: Url = "untitled:Untitled-1".parse().unwrap();
3505 server
3506 .did_open(DidOpenTextDocumentParams {
3507 text_document: TextDocumentItem {
3508 uri: uri.clone(),
3509 language_id: "lex".into(),
3510 version: 1,
3511 text: "1. Host\n\n Some content.\n".to_string(),
3512 },
3513 })
3514 .await;
3515
3516 let diags = client.diagnostics_for(&uri);
3517 assert!(
3518 !diags.iter().any(|d| matches!(
3519 &d.code,
3520 Some(tower_lsp::lsp_types::NumberOrString::String(c)) if c.starts_with("include-")
3521 )),
3522 "untitled URIs should produce no include-* diagnostics, got {diags:?}"
3523 );
3524 }
3525
3526 #[tokio::test]
3536 async fn initialize_advertises_prepare_paste_capability() {
3537 let provider = Arc::new(MockFeatureProvider::default());
3538 let server = LexLanguageServer::with_features(NoopClient, provider);
3539
3540 let result = server
3541 .initialize(InitializeParams::default())
3542 .await
3543 .unwrap();
3544
3545 let experimental = result
3546 .capabilities
3547 .experimental
3548 .expect("experimental capabilities advertised");
3549 assert_eq!(experimental["lexPreparePaste"], serde_json::json!(true));
3550 }
3551
3552 #[tokio::test]
3553 async fn prepare_paste_reanchors_against_open_buffer() {
3554 let provider = Arc::new(MockFeatureProvider::default());
3555 let server = LexLanguageServer::with_features(NoopClient, provider);
3556 let uri = Url::from_file_path("/tmp/paste.lex").unwrap();
3557
3558 server
3559 .did_open(DidOpenTextDocumentParams {
3560 text_document: TextDocumentItem {
3561 uri: uri.clone(),
3562 language_id: "lex".into(),
3563 version: 1,
3564 text: "Top:\n\n existing\n\n".to_string(),
3565 },
3566 })
3567 .await;
3568
3569 let result = server
3572 .prepare_paste(PreparePasteParams {
3573 text_document: TextDocumentIdentifier { uri },
3574 range: Range {
3575 start: Position::new(3, 0),
3576 end: Position::new(3, 0),
3577 },
3578 pasted_text: "first\n second\n".to_string(),
3579 })
3580 .await
3581 .unwrap();
3582
3583 assert_eq!(result.mode, PasteMode::Reanchor);
3584 assert_eq!(result.text, " first\n second\n");
3585 }
3586
3587 #[tokio::test]
3588 async fn prepare_paste_passes_through_verbatim() {
3589 let provider = Arc::new(MockFeatureProvider::default());
3590 let server = LexLanguageServer::with_features(NoopClient, provider);
3591 let uri = Url::from_file_path("/tmp/verb.lex").unwrap();
3592
3593 server
3594 .did_open(DidOpenTextDocumentParams {
3595 text_document: TextDocumentItem {
3596 uri: uri.clone(),
3597 language_id: "lex".into(),
3598 version: 1,
3599 text: "Code:\n line one\n line two\n:: text ::\n".to_string(),
3600 },
3601 })
3602 .await;
3603
3604 let pasted = " weird\n indent\n".to_string();
3605 let result = server
3606 .prepare_paste(PreparePasteParams {
3607 text_document: TextDocumentIdentifier { uri },
3608 range: Range {
3609 start: Position::new(1, 8),
3610 end: Position::new(1, 8),
3611 },
3612 pasted_text: pasted.clone(),
3613 })
3614 .await
3615 .unwrap();
3616
3617 assert_eq!(result.mode, PasteMode::PassthroughVerbatim);
3618 assert_eq!(result.text, pasted);
3619 }
3620
3621 #[tokio::test]
3622 async fn prepare_paste_unopened_buffer_echoes_clipboard() {
3623 let provider = Arc::new(MockFeatureProvider::default());
3624 let server = LexLanguageServer::with_features(NoopClient, provider);
3625 let uri = Url::from_file_path("/tmp/never-opened.lex").unwrap();
3626
3627 let pasted = "anything\n here\n".to_string();
3628 let result = server
3629 .prepare_paste(PreparePasteParams {
3630 text_document: TextDocumentIdentifier { uri },
3631 range: Range {
3632 start: Position::new(0, 0),
3633 end: Position::new(0, 0),
3634 },
3635 pasted_text: pasted.clone(),
3636 })
3637 .await
3638 .unwrap();
3639
3640 assert_eq!(result.text, pasted);
3643 }
3644}