1use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use tokio::sync::{RwLock, mpsc};
11use tower_lsp::jsonrpc::Result as JsonRpcResult;
12use tower_lsp::lsp_types::*;
13use tower_lsp::{Client, LanguageServer};
14
15use crate::config::{Config, ConfigValidated, SourcedConfig, is_valid_rule_name};
16use crate::discovery::{ExcludeMatchers, is_markdown_extension};
17use crate::lsp::index_worker::{IndexWorker, SharedIndexState};
18use crate::lsp::types::{IndexState, IndexUpdate, LspRuleSettings, RelintRequest, RumdlLspConfig};
19use crate::workspace_index::WorkspaceIndex;
20
21const MAX_RULE_LIST_SIZE: usize = 100;
23
24const MAX_LINE_LENGTH: usize = 10_000;
26
27fn names_lsp_config_field(settings: &serde_json::Value) -> bool {
32 let serde_json::Value::Object(settings) = settings else {
33 return false;
34 };
35 let Ok(serde_json::Value::Object(fields)) = serde_json::to_value(RumdlLspConfig::default()) else {
36 return false;
37 };
38 settings.keys().any(|key| fields.contains_key(key))
39}
40
41fn merge_lsp_config(current: &RumdlLspConfig, incoming: &serde_json::Value) -> Option<RumdlLspConfig> {
53 let serde_json::Value::Object(incoming) = incoming else {
54 return None;
55 };
56 let serde_json::Value::Object(mut base) = serde_json::to_value(current).ok()? else {
57 return None;
58 };
59 for (key, value) in incoming {
60 base.insert(key.clone(), value.clone());
61 }
62 serde_json::from_value(serde_json::Value::Object(base)).ok()
63}
64
65#[derive(Clone, Debug, PartialEq)]
67pub(crate) struct DocumentEntry {
68 pub(crate) content: String,
70 pub(crate) version: Option<i32>,
72 pub(crate) from_disk: bool,
74}
75
76#[derive(Clone, Debug)]
78pub(crate) struct ConfigCacheEntry {
79 pub(crate) config: Config,
81 pub(crate) sourced: Option<Arc<SourcedConfig<ConfigValidated>>>,
86 pub(crate) config_file: Option<PathBuf>,
88 pub(crate) from_global_fallback: bool,
90}
91
92#[derive(Clone)]
99pub(crate) struct ConfigResolver {
100 pub(super) config: Arc<RwLock<RumdlLspConfig>>,
101 pub(super) rumdl_config: Arc<RwLock<Config>>,
102 pub(super) rumdl_sourced: Arc<RwLock<Option<Arc<SourcedConfig<ConfigValidated>>>>>,
103 pub(super) workspace_roots: Arc<RwLock<Vec<PathBuf>>>,
104 pub(super) config_cache: Arc<RwLock<HashMap<PathBuf, ConfigCacheEntry>>>,
105 pub(super) cli_config_path: Option<String>,
106}
107
108#[derive(Clone)]
118pub struct RumdlLanguageServer {
119 pub(crate) client: Client,
120 pub(crate) config: Arc<RwLock<RumdlLspConfig>>,
122 pub(crate) rumdl_config: Arc<RwLock<Config>>,
124 pub(crate) rumdl_sourced: Arc<RwLock<Option<Arc<SourcedConfig<ConfigValidated>>>>>,
127 pub(crate) documents: Arc<RwLock<HashMap<Url, DocumentEntry>>>,
129 pub(crate) document_aliases: Arc<RwLock<HashMap<Url, Vec<Url>>>>,
142 pub(crate) workspace_roots: Arc<RwLock<Vec<PathBuf>>>,
144 pub(crate) config_cache: Arc<RwLock<HashMap<PathBuf, ConfigCacheEntry>>>,
147 pub(crate) config_resolver: ConfigResolver,
149 pub(crate) workspace_index: Arc<RwLock<WorkspaceIndex>>,
151 pub(crate) index_state: Arc<RwLock<IndexState>>,
153 update_tx: Option<mpsc::Sender<IndexUpdate>>,
161 pub(crate) client_supports_pull_diagnostics: Arc<RwLock<bool>>,
164 pub(crate) client_supports_hierarchical_symbols: Arc<RwLock<bool>>,
168 pub(crate) cli_config_path: Option<String>,
176}
177
178impl RumdlLanguageServer {
179 pub fn new(client: Client, cli_config_path: Option<&str>) -> Self {
180 let initial_config = RumdlLspConfig::default();
181 let cli_config_path = cli_config_path.map(str::to_string);
182
183 let workspace_index = Arc::new(RwLock::new(WorkspaceIndex::new()));
185 let index_state = Arc::new(RwLock::new(IndexState::default()));
186 let workspace_roots = Arc::new(RwLock::new(Vec::new()));
187 let config = Arc::new(RwLock::new(initial_config));
188 let rumdl_config = Arc::new(RwLock::new(Config::default()));
189 let rumdl_sourced = Arc::new(RwLock::new(None));
190 let config_cache = Arc::new(RwLock::new(HashMap::new()));
191 let documents = Arc::new(RwLock::new(HashMap::new()));
192
193 let config_resolver = ConfigResolver {
194 config: config.clone(),
195 rumdl_config: rumdl_config.clone(),
196 rumdl_sourced: rumdl_sourced.clone(),
197 workspace_roots: workspace_roots.clone(),
198 config_cache: config_cache.clone(),
199 cli_config_path: cli_config_path.clone(),
200 };
201
202 let (update_tx, update_rx) = mpsc::channel::<IndexUpdate>(100);
204 let (relint_tx, relint_rx) = mpsc::channel::<RelintRequest>(100);
205
206 let server = Self {
207 client,
208 config,
209 rumdl_config,
210 rumdl_sourced,
211 documents,
212 document_aliases: Arc::new(RwLock::new(HashMap::new())),
213 workspace_roots,
214 config_cache,
215 config_resolver: config_resolver.clone(),
216 workspace_index,
217 index_state,
218 update_tx: Some(update_tx),
219 client_supports_pull_diagnostics: Arc::new(RwLock::new(false)),
220 client_supports_hierarchical_symbols: Arc::new(RwLock::new(false)),
221 cli_config_path,
222 };
223
224 let worker = IndexWorker::new(
228 update_rx,
229 server.client.clone(),
230 relint_tx,
231 SharedIndexState {
232 workspace_index: server.workspace_index.clone(),
233 index_state: server.index_state.clone(),
234 workspace_roots: server.workspace_roots.clone(),
235 config_resolver,
236 documents: server.documents.clone(),
237 },
238 );
239 tokio::spawn(worker.run());
240
241 tokio::spawn(server.detached_for_background().run_relint_worker(relint_rx));
246
247 server
248 }
249
250 fn detached_for_background(&self) -> Self {
261 Self {
262 update_tx: None,
263 ..self.clone()
264 }
265 }
266
267 pub(crate) async fn queue_index_update(&self, update: IndexUpdate) -> bool {
273 let Some(update_tx) = &self.update_tx else {
274 return false;
275 };
276 if let IndexUpdate::FileChanged { path, .. } = &update
278 && !super::is_markdown_path(path)
279 {
280 return true;
281 }
282 update_tx.send(update).await.is_ok()
283 }
284
285 pub(super) async fn get_document_content(&self, uri: &Url) -> Option<String> {
291 let uri = &self.store_uri(uri).await;
292
293 {
295 let docs = self.documents.read().await;
296 if let Some(entry) = docs.get(uri) {
297 return Some(entry.content.clone());
298 }
299 }
300
301 if let Ok(path) = uri.to_file_path() {
303 if let Ok(content) = tokio::fs::read_to_string(&path).await {
304 let entry = DocumentEntry {
306 content: content.clone(),
307 version: None,
308 from_disk: true,
309 };
310
311 let mut docs = self.documents.write().await;
312 docs.insert(uri.clone(), entry);
313
314 log::debug!("Loaded document from disk and cached: {uri}");
315 return Some(content);
316 } else {
317 log::debug!("Failed to read file from disk: {uri}");
318 }
319 }
320
321 None
322 }
323
324 pub(super) async fn get_open_document_content(&self, uri: &Url) -> Option<String> {
330 let uri = self.store_uri(uri).await;
331 let docs = self.documents.read().await;
332 docs.get(&uri)
333 .and_then(|entry| (!entry.from_disk).then(|| entry.content.clone()))
334 }
335
336 async fn reindex_from_disk(&self, path: PathBuf) {
345 let roots = self.workspace_roots.read().await.clone();
346 let (options, includes, excludes) = {
347 let config = self.rumdl_config.read().await;
348 (
349 crate::lsp::index_worker::index_walk_options(&config),
350 config.global.include.clone(),
351 ExcludeMatchers::new(&config.global.exclude),
352 )
353 };
354 let scanned = roots.iter().any(|root| path.starts_with(root))
355 && path.extension().is_some_and(is_markdown_extension)
356 && !crate::lsp::index_worker::path_is_ignored_for_index(&roots, &path, &options, &includes, &excludes);
357 let content = if scanned {
358 crate::lsp::read_markdown_lossy(&path).await.ok()
359 } else {
360 None
361 };
362 let update = match content {
363 Some(content) => IndexUpdate::FileChanged { path, content },
364 None => IndexUpdate::FileRemoved { path },
365 };
366 self.queue_index_update(update).await;
367 }
368
369 async fn store_uri(&self, uri: &Url) -> Url {
381 let Some(spellings) = self.document_aliases.read().await.get(uri).cloned() else {
382 return uri.clone();
383 };
384 let docs = self.documents.read().await;
385 let is_open = |u: &Url| matches!(docs.get(u), Some(entry) if !entry.from_disk);
386 if is_open(uri) {
387 return uri.clone();
388 }
389 spellings
391 .iter()
392 .rev()
393 .find(|u| is_open(u))
394 .cloned()
395 .unwrap_or_else(|| uri.clone())
396 }
397
398 pub(super) async fn resolve_flavor_for_uri(&self, uri: &Url) -> crate::config::MarkdownFlavor {
401 match super::resolve_uri(uri) {
402 Some(path) => self.resolve_config_for_file(&path).await.get_flavor_for_file(&path),
403 None => self.rumdl_config.read().await.markdown_flavor(),
404 }
405 }
406}
407
408#[tower_lsp::async_trait]
409impl LanguageServer for RumdlLanguageServer {
410 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
411 log::info!("Initializing rumdl Language Server");
412
413 if let Some(options) = params.initialization_options
415 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
416 {
417 *self.config.write().await = config;
418 }
419
420 let supports_pull = params
423 .capabilities
424 .text_document
425 .as_ref()
426 .and_then(|td| td.diagnostic.as_ref())
427 .is_some();
428
429 if supports_pull {
430 log::info!("Client supports pull diagnostics - disabling push to avoid duplicates");
431 *self.client_supports_pull_diagnostics.write().await = true;
432 } else {
433 log::info!("Client does not support pull diagnostics - using push model");
434 }
435
436 let supports_hierarchical_symbols = params
439 .capabilities
440 .text_document
441 .as_ref()
442 .and_then(|td| td.document_symbol.as_ref())
443 .and_then(|ds| ds.hierarchical_document_symbol_support)
444 .unwrap_or(false);
445 *self.client_supports_hierarchical_symbols.write().await = supports_hierarchical_symbols;
446
447 let mut roots = Vec::new();
449 if let Some(workspace_folders) = params.workspace_folders {
450 for folder in workspace_folders {
451 if let Ok(path) = folder.uri.to_file_path() {
452 let path = super::resolve_workspace_root(&path);
453 log::info!("Workspace root: {}", path.display());
454 roots.push(path);
455 }
456 }
457 } else if let Some(root_uri) = params.root_uri
458 && let Ok(path) = root_uri.to_file_path()
459 {
460 let path = super::resolve_workspace_root(&path);
461 log::info!("Workspace root: {}", path.display());
462 roots.push(path);
463 }
464 *self.workspace_roots.write().await = roots;
465
466 self.load_configuration(false).await;
468
469 let (enable_link_navigation, enable_link_completions, enable_symbols) = {
470 let config = self.config.read().await;
471 (
472 config.enable_link_navigation,
473 config.enable_link_completions,
474 config.enable_symbols,
475 )
476 };
477
478 Ok(InitializeResult {
479 capabilities: ServerCapabilities {
480 text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
481 open_close: Some(true),
482 change: Some(TextDocumentSyncKind::FULL),
483 will_save: Some(false),
484 will_save_wait_until: Some(true),
485 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
486 include_text: Some(false),
487 })),
488 })),
489 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
490 code_action_kinds: Some(vec![
491 CodeActionKind::QUICKFIX,
492 CodeActionKind::SOURCE_FIX_ALL,
493 CodeActionKind::new("source.fixAll.rumdl"),
494 ]),
495 work_done_progress_options: WorkDoneProgressOptions::default(),
496 resolve_provider: None,
497 })),
498 document_formatting_provider: Some(OneOf::Left(true)),
499 document_range_formatting_provider: Some(OneOf::Left(true)),
500 document_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
501 workspace_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
502 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
503 identifier: Some("rumdl".to_string()),
504 inter_file_dependencies: true,
505 workspace_diagnostics: false,
506 work_done_progress_options: WorkDoneProgressOptions::default(),
507 })),
508 completion_provider: Some(CompletionOptions {
514 trigger_characters: Some(if enable_link_completions {
515 vec![
516 "`".to_string(),
517 "(".to_string(),
518 "#".to_string(),
519 "/".to_string(),
520 ".".to_string(),
521 "-".to_string(),
522 ]
523 } else {
524 vec!["`".to_string()]
525 }),
526 resolve_provider: Some(false),
527 work_done_progress_options: WorkDoneProgressOptions::default(),
528 all_commit_characters: None,
529 completion_item: None,
530 }),
531 definition_provider: enable_link_navigation.then_some(OneOf::Left(true)),
532 references_provider: enable_link_navigation.then_some(OneOf::Left(true)),
533 hover_provider: enable_link_navigation.then_some(HoverProviderCapability::Simple(true)),
534 rename_provider: enable_link_navigation.then_some(OneOf::Right(RenameOptions {
535 prepare_provider: Some(true),
536 work_done_progress_options: WorkDoneProgressOptions::default(),
537 })),
538 workspace: Some(WorkspaceServerCapabilities {
539 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
540 supported: Some(true),
541 change_notifications: Some(OneOf::Left(true)),
542 }),
543 file_operations: None,
544 }),
545 ..Default::default()
546 },
547 server_info: Some(ServerInfo {
548 name: "rumdl".to_string(),
549 version: Some(env!("CARGO_PKG_VERSION").to_string()),
550 }),
551 })
552 }
553
554 async fn initialized(&self, _: InitializedParams) {
555 let version = env!("CARGO_PKG_VERSION");
556
557 let (binary_path, build_time) = std::env::current_exe().ok().map_or_else(
559 || ("unknown".to_string(), "unknown".to_string()),
560 |path| {
561 let path_str = path.to_str().unwrap_or("unknown").to_string();
562 let build_time = std::fs::metadata(&path)
563 .ok()
564 .and_then(|metadata| metadata.modified().ok())
565 .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
566 .and_then(|duration| {
567 let secs = duration.as_secs();
568 chrono::DateTime::from_timestamp(secs as i64, 0)
569 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
570 })
571 .unwrap_or_else(|| "unknown".to_string());
572 (path_str, build_time)
573 },
574 );
575
576 let working_dir = std::env::current_dir()
577 .ok()
578 .and_then(|p| p.to_str().map(std::string::ToString::to_string))
579 .unwrap_or_else(|| "unknown".to_string());
580
581 log::info!("rumdl Language Server v{version} initialized (built: {build_time}, binary: {binary_path})");
582 log::info!("Working directory: {working_dir}");
583
584 self.client
585 .log_message(MessageType::INFO, format!("rumdl v{version} Language Server started"))
586 .await;
587
588 if !self.queue_index_update(IndexUpdate::FullRescan).await {
590 log::warn!("Failed to trigger initial workspace indexing");
591 } else {
592 log::info!("Triggered initial workspace indexing for cross-file analysis");
593 }
594
595 let markdown_patterns = [
597 "**/*.md",
598 "**/*.markdown",
599 "**/*.mdx",
600 "**/*.mkd",
601 "**/*.mkdn",
602 "**/*.mdown",
603 "**/*.mdwn",
604 "**/*.qmd",
605 "**/*.rmd",
606 ];
607 let config_patterns = [
611 "**/.rumdl.toml",
612 "**/rumdl.toml",
613 "**/pyproject.toml",
614 "**/.markdownlint.json",
615 "**/.markdownlint-cli2.yaml",
616 "**/.markdownlint-cli2.jsonc",
617 "**/.editorconfig",
618 ];
619 let watchers: Vec<_> = markdown_patterns
620 .iter()
621 .chain(config_patterns.iter())
622 .map(|pattern| FileSystemWatcher {
623 glob_pattern: GlobPattern::String((*pattern).to_string()),
624 kind: Some(WatchKind::all()),
625 })
626 .collect();
627
628 let registration = Registration {
629 id: "markdown-watcher".to_string(),
630 method: "workspace/didChangeWatchedFiles".to_string(),
631 register_options: Some(
632 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { watchers }).unwrap(),
633 ),
634 };
635
636 if self.client.register_capability(vec![registration]).await.is_err() {
637 log::debug!("Client does not support file watching capability");
638 }
639 }
640
641 async fn completion(&self, params: CompletionParams) -> JsonRpcResult<Option<CompletionResponse>> {
642 let uri = params.text_document_position.text_document.uri;
643 let position = params.text_document_position.position;
644
645 let Some(text) = self.get_document_content(&uri).await else {
647 return Ok(None);
648 };
649
650 if let Some((start_col, current_text)) = Self::detect_code_fence_language_position(&text, position) {
652 log::debug!(
653 "Code fence completion triggered at {}:{}, current text: '{}'",
654 position.line,
655 position.character,
656 current_text
657 );
658 let items = self
659 .get_language_completions(&uri, ¤t_text, start_col, position)
660 .await;
661 if !items.is_empty() {
662 return Ok(Some(CompletionResponse::Array(items)));
663 }
664 }
665
666 if self.config.read().await.enable_link_completions {
668 let trigger = params.context.as_ref().and_then(|c| c.trigger_character.as_deref());
672 let skip_link_check = matches!(trigger, Some("." | "-")) && {
673 let line_num = position.line as usize;
674 !text.lines().nth(line_num).is_some_and(|line| line.contains("]("))
677 };
678
679 if !skip_link_check && let Some(link_info) = Self::detect_link_target_position(&text, position) {
680 if let Some((partial_anchor, anchor_start_col)) = link_info.anchor {
681 log::debug!(
682 "Anchor completion triggered at {}:{}, file: '{}', partial: '{}'",
683 position.line,
684 position.character,
685 link_info.file_path,
686 partial_anchor
687 );
688 let items = self
689 .get_anchor_completions(&uri, &link_info.file_path, &partial_anchor, anchor_start_col, position)
690 .await;
691 if !items.is_empty() {
692 return Ok(Some(CompletionResponse::Array(items)));
693 }
694 } else {
695 log::debug!(
696 "File path completion triggered at {}:{}, partial: '{}'",
697 position.line,
698 position.character,
699 link_info.file_path
700 );
701 let list = self
702 .get_file_completions(&uri, &link_info.file_path, link_info.path_start_col, position)
703 .await;
704 if !list.items.is_empty() {
705 return Ok(Some(CompletionResponse::List(list)));
706 }
707 }
708 }
709 }
710
711 Ok(None)
712 }
713
714 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
715 let mut roots = self.workspace_roots.write().await;
717
718 for removed in ¶ms.event.removed {
722 if let Ok(path) = removed.uri.to_file_path() {
723 let path = super::resolve_workspace_root(&path);
724 roots.retain(|r| r != &path);
725 log::info!("Removed workspace root: {}", path.display());
726 }
727 }
728
729 for added in ¶ms.event.added {
731 if let Ok(path) = added.uri.to_file_path()
732 && let path = super::resolve_workspace_root(&path)
733 && !roots.contains(&path)
734 {
735 log::info!("Added workspace root: {}", path.display());
736 roots.push(path);
737 }
738 }
739 drop(roots);
740
741 self.config_cache.write().await.clear();
743
744 self.reload_configuration().await;
746
747 if !self.queue_index_update(IndexUpdate::FullRescan).await {
749 log::warn!("Failed to trigger workspace rescan after folder change");
750 }
751 }
752
753 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
754 log::debug!("Configuration changed: {:?}", params.settings);
755
756 let settings_value = params.settings;
760
761 let rumdl_settings = if let serde_json::Value::Object(ref obj) = settings_value {
763 obj.get("rumdl").cloned().unwrap_or(settings_value.clone())
764 } else {
765 settings_value
766 };
767
768 let mut config_applied = false;
770 let mut warnings: Vec<String> = Vec::new();
771
772 if let Ok(rule_settings) = serde_json::from_value::<LspRuleSettings>(rumdl_settings.clone())
776 && (rule_settings.disable.is_some()
777 || rule_settings.enable.is_some()
778 || rule_settings.line_length.is_some()
779 || (!rule_settings.rules.is_empty() && rule_settings.rules.keys().all(|k| is_valid_rule_name(k))))
780 {
781 if let Some(ref disable) = rule_settings.disable {
783 for rule in disable {
784 if !is_valid_rule_name(rule) {
785 warnings.push(format!("Unknown rule in disable list: {rule}"));
786 }
787 }
788 }
789 if let Some(ref enable) = rule_settings.enable {
790 for rule in enable {
791 if !is_valid_rule_name(rule) {
792 warnings.push(format!("Unknown rule in enable list: {rule}"));
793 }
794 }
795 }
796 for rule_name in rule_settings.rules.keys() {
798 if !is_valid_rule_name(rule_name) {
799 warnings.push(format!("Unknown rule in settings: {rule_name}"));
800 }
801 }
802
803 log::info!("Applied rule settings from configuration (Neovim style)");
804 let mut config = self.config.write().await;
805 config.settings = Some(rule_settings);
806 drop(config);
807 config_applied = true;
808 } else if names_lsp_config_field(&rumdl_settings)
809 && let Ok(full_config) = serde_json::from_value::<RumdlLspConfig>(rumdl_settings.clone())
810 {
811 if let Some(ref rules) = full_config.enable_rules {
813 for rule in rules {
814 if !is_valid_rule_name(rule) {
815 warnings.push(format!("Unknown rule in enableRules: {rule}"));
816 }
817 }
818 }
819 if let Some(ref rules) = full_config.disable_rules {
820 for rule in rules {
821 if !is_valid_rule_name(rule) {
822 warnings.push(format!("Unknown rule in disableRules: {rule}"));
823 }
824 }
825 }
826
827 {
835 let mut config = self.config.write().await;
836 if let Some(merged) = merge_lsp_config(&config, &rumdl_settings) {
837 *config = merged;
838 drop(config);
839 log::info!("Merged LSP configuration from client settings");
840 config_applied = true;
841 } else {
842 drop(config);
843 warnings.push("Could not merge LSP configuration update; keeping current settings".to_string());
844 }
845 }
846 } else if let serde_json::Value::Object(obj) = rumdl_settings {
847 let mut config = self.config.write().await;
850
851 let mut rules = std::collections::HashMap::new();
853 let mut disable = Vec::new();
854 let mut enable = Vec::new();
855 let mut line_length = None;
856 let mut names_a_rule_setting = false;
860
861 for (key, value) in obj {
862 match key.as_str() {
863 "disable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
864 Ok(d) => {
865 if d.len() > MAX_RULE_LIST_SIZE {
866 warnings.push(format!(
867 "Too many rules in 'disable' ({} > {}), truncating",
868 d.len(),
869 MAX_RULE_LIST_SIZE
870 ));
871 }
872 for rule in d.iter().take(MAX_RULE_LIST_SIZE) {
873 if !is_valid_rule_name(rule) {
874 warnings.push(format!("Unknown rule in disable: {rule}"));
875 }
876 }
877 disable = d.into_iter().take(MAX_RULE_LIST_SIZE).collect();
878 names_a_rule_setting = true;
879 }
880 Err(_) => {
881 warnings.push(format!(
882 "Invalid 'disable' value: expected array of strings, got {value}"
883 ));
884 }
885 },
886 "enable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
887 Ok(e) => {
888 if e.len() > MAX_RULE_LIST_SIZE {
889 warnings.push(format!(
890 "Too many rules in 'enable' ({} > {}), truncating",
891 e.len(),
892 MAX_RULE_LIST_SIZE
893 ));
894 }
895 for rule in e.iter().take(MAX_RULE_LIST_SIZE) {
896 if !is_valid_rule_name(rule) {
897 warnings.push(format!("Unknown rule in enable: {rule}"));
898 }
899 }
900 enable = e.into_iter().take(MAX_RULE_LIST_SIZE).collect();
901 names_a_rule_setting = true;
902 }
903 Err(_) => {
904 warnings.push(format!(
905 "Invalid 'enable' value: expected array of strings, got {value}"
906 ));
907 }
908 },
909 "lineLength" | "line_length" | "line-length" => {
910 if let Some(l) = value.as_u64() {
911 match usize::try_from(l) {
912 Ok(len) if len <= MAX_LINE_LENGTH => {
913 line_length = Some(len);
914 names_a_rule_setting = true;
915 }
916 Ok(len) => warnings.push(format!(
917 "Invalid 'lineLength' value: {len} exceeds maximum ({MAX_LINE_LENGTH})"
918 )),
919 Err(_) => warnings.push(format!("Invalid 'lineLength' value: {l} is too large")),
920 }
921 } else {
922 warnings.push(format!("Invalid 'lineLength' value: expected number, got {value}"));
923 }
924 }
925 _ if key.starts_with("MD") || key.starts_with("md") => {
927 let normalized = key.to_uppercase();
928 if !is_valid_rule_name(&normalized) {
929 warnings.push(format!("Unknown rule: {key}"));
930 }
931 rules.insert(normalized, value);
932 names_a_rule_setting = true;
933 }
934 _ => {
935 warnings.push(format!("Unknown configuration key: {key}"));
937 }
938 }
939 }
940
941 let settings = LspRuleSettings {
942 line_length,
943 disable: if disable.is_empty() { None } else { Some(disable) },
944 enable: if enable.is_empty() { None } else { Some(enable) },
945 rules,
946 };
947
948 if names_a_rule_setting {
949 log::info!("Applied Neovim-style rule settings (manual parse)");
950 config.settings = Some(settings);
951 config_applied = true;
952 }
953 drop(config);
954 } else {
955 log::warn!("Could not parse configuration settings: {rumdl_settings:?}");
956 }
957
958 for warning in &warnings {
960 log::warn!("{warning}");
961 }
962
963 if !warnings.is_empty() {
965 let message = if warnings.len() == 1 {
966 format!("rumdl: {}", warnings[0])
967 } else {
968 format!("rumdl configuration warnings:\n{}", warnings.join("\n"))
969 };
970 self.client.log_message(MessageType::WARNING, message).await;
971 }
972
973 if !config_applied {
974 log::debug!("No configuration changes applied");
975 }
976
977 self.config_cache.write().await.clear();
979
980 if config_applied {
988 self.load_configuration(false).await;
989
990 if !self.queue_index_update(IndexUpdate::FullRescan).await {
994 log::warn!("Failed to request workspace rescan after configuration change");
995 }
996 }
997
998 let doc_list: Vec<_> = {
1003 let documents = self.documents.read().await;
1004 documents
1005 .iter()
1006 .filter(|(_, entry)| !entry.from_disk)
1007 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
1008 .collect()
1009 };
1010
1011 let tasks: Vec<_> = doc_list
1015 .into_iter()
1016 .map(|(uri, text)| {
1017 let server = self.clone();
1018 tokio::spawn(async move {
1019 server.update_diagnostics(uri, text, true).await;
1020 })
1021 })
1022 .collect();
1023
1024 for task in tasks {
1026 let _ = task.await;
1027 }
1028 }
1029
1030 async fn shutdown(&self) -> JsonRpcResult<()> {
1031 log::info!("Shutting down rumdl Language Server");
1032
1033 self.queue_index_update(IndexUpdate::Shutdown).await;
1035
1036 Ok(())
1037 }
1038
1039 async fn did_open(&self, params: DidOpenTextDocumentParams) {
1040 let uri = params.text_document.uri;
1041 let text = params.text_document.text;
1042 let version = params.text_document.version;
1043
1044 let entry = DocumentEntry {
1045 content: text.clone(),
1046 version: Some(version),
1047 from_disk: false,
1048 };
1049 self.documents.write().await.insert(uri.clone(), entry);
1050
1051 let resolved = super::resolve_uri_spelling(&uri);
1053 if resolved != uri {
1054 let mut aliases = self.document_aliases.write().await;
1055 let spellings = aliases.entry(resolved).or_default();
1056 if !spellings.contains(&uri) {
1057 spellings.push(uri.clone());
1058 }
1059 }
1060
1061 if let Some(path) = super::resolve_uri(&uri) {
1063 self.queue_index_update(IndexUpdate::FileChanged {
1064 path,
1065 content: text.clone(),
1066 })
1067 .await;
1068 }
1069
1070 self.update_diagnostics(uri, text, true).await;
1071 }
1072
1073 async fn did_change(&self, params: DidChangeTextDocumentParams) {
1074 let uri = params.text_document.uri;
1075 let version = params.text_document.version;
1076
1077 let base = if params.content_changes.iter().all(|change| change.range.is_some()) {
1080 match self.documents.read().await.get(&uri) {
1081 Some(entry) => Some(entry.content.clone()),
1082 None => {
1083 log::warn!("Ignoring an incremental change to {uri}, which is not open");
1084 return;
1085 }
1086 }
1087 } else {
1088 None
1089 };
1090
1091 if !params.content_changes.is_empty() {
1092 let text = super::position::apply_content_changes(base.unwrap_or_default(), params.content_changes);
1093
1094 let entry = DocumentEntry {
1095 content: text.clone(),
1096 version: Some(version),
1097 from_disk: false,
1098 };
1099 self.documents.write().await.insert(uri.clone(), entry);
1100
1101 if let Some(path) = super::resolve_uri(&uri) {
1103 self.queue_index_update(IndexUpdate::FileChanged {
1104 path,
1105 content: text.clone(),
1106 })
1107 .await;
1108 }
1109
1110 self.update_diagnostics(uri, text, false).await;
1111 }
1112 }
1113
1114 async fn will_save_wait_until(&self, params: WillSaveTextDocumentParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1115 if params.reason != TextDocumentSaveReason::MANUAL {
1118 return Ok(None);
1119 }
1120
1121 let config_guard = self.config.read().await;
1122 let enable_auto_fix = config_guard.enable_auto_fix;
1123 drop(config_guard);
1124
1125 if !enable_auto_fix {
1126 return Ok(None);
1127 }
1128
1129 let Some(text) = self.get_document_content(¶ms.text_document.uri).await else {
1131 return Ok(None);
1132 };
1133
1134 match self.apply_all_fixes(¶ms.text_document.uri, &text).await {
1136 Ok(Some(fixed_text)) => {
1137 Ok(Some(vec![TextEdit {
1139 range: Range {
1140 start: Position { line: 0, character: 0 },
1141 end: self.get_end_position(&text),
1142 },
1143 new_text: fixed_text,
1144 }]))
1145 }
1146 Ok(None) => Ok(None),
1147 Err(e) => {
1148 log::error!("Failed to generate fixes in will_save_wait_until: {e}");
1149 Ok(None)
1150 }
1151 }
1152 }
1153
1154 async fn did_save(&self, params: DidSaveTextDocumentParams) {
1155 if let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
1158 self.update_diagnostics(params.text_document.uri, entry.content.clone(), true)
1159 .await;
1160 }
1161 }
1162
1163 async fn did_close(&self, params: DidCloseTextDocumentParams) {
1164 self.documents.write().await.remove(¶ms.text_document.uri);
1166 let resolved = super::resolve_uri_spelling(¶ms.text_document.uri);
1169 if resolved != params.text_document.uri {
1170 let mut aliases = self.document_aliases.write().await;
1171 if let Some(spellings) = aliases.get_mut(&resolved) {
1172 spellings.retain(|u| u != ¶ms.text_document.uri);
1173 if spellings.is_empty() {
1174 aliases.remove(&resolved);
1175 }
1176 }
1177 }
1178
1179 if let Some(path) = super::resolve_uri(¶ms.text_document.uri) {
1183 match self.get_open_document_content(&resolved).await {
1184 Some(content) => {
1185 self.queue_index_update(IndexUpdate::FileChanged { path, content })
1186 .await;
1187 }
1188 None => self.reindex_from_disk(path).await,
1189 }
1190 }
1191
1192 self.client
1195 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
1196 .await;
1197 }
1198
1199 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
1200 const CONFIG_FILES: &[&str] = &[
1202 ".rumdl.toml",
1203 "rumdl.toml",
1204 "pyproject.toml",
1205 ".markdownlint.json",
1206 ".markdownlint-cli2.jsonc",
1207 ".markdownlint-cli2.yaml",
1208 ".markdownlint-cli2.yml",
1209 ];
1210
1211 let mut config_changed = false;
1212 let reads_editorconfig = self.reads_editorconfig().await;
1215
1216 for change in ¶ms.changes {
1217 if let Some(path) = super::resolve_uri(&change.uri) {
1221 let file_name = path.file_name().and_then(|f| f.to_str());
1222
1223 if let Some(name) = file_name
1225 && (CONFIG_FILES.contains(&name) || (reads_editorconfig && name == ".editorconfig"))
1226 && !config_changed
1227 {
1228 log::info!("Config file changed: {}, invalidating config cache", path.display());
1229
1230 let mut cache = self.config_cache.write().await;
1234 cache.clear();
1235
1236 drop(cache);
1238 self.reload_configuration().await;
1239 config_changed = true;
1240 }
1241
1242 if let Some(ext) = path.extension()
1244 && is_markdown_extension(ext)
1245 {
1246 match change.typ {
1247 FileChangeType::CREATED | FileChangeType::CHANGED => {
1248 if let Some(content) = self
1258 .get_open_document_content(&super::resolve_uri_spelling(&change.uri))
1259 .await
1260 {
1261 self.queue_index_update(IndexUpdate::FileChanged {
1262 path: path.clone(),
1263 content,
1264 })
1265 .await;
1266 continue;
1267 }
1268 self.reindex_from_disk(path.clone()).await;
1269 }
1270 FileChangeType::DELETED => {
1271 self.queue_index_update(IndexUpdate::FileRemoved { path: path.clone() })
1272 .await;
1273 }
1274 _ => {}
1275 }
1276 }
1277 }
1278 }
1279
1280 if config_changed {
1282 if !self.queue_index_update(IndexUpdate::FullRescan).await {
1286 log::warn!("Failed to request workspace rescan after config change");
1287 }
1288
1289 let docs_to_update: Vec<(Url, String)> = {
1290 let docs = self.documents.read().await;
1291 docs.iter()
1292 .filter(|(_, entry)| !entry.from_disk)
1293 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
1294 .collect()
1295 };
1296
1297 for (uri, text) in docs_to_update {
1298 self.update_diagnostics(uri, text, true).await;
1299 }
1300 }
1301 }
1302
1303 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
1304 let uri = params.text_document.uri;
1305 let range = params.range;
1306 let requested_kinds = params.context.only;
1307
1308 if let Some(text) = self.get_document_content(&uri).await {
1309 match self.get_code_actions(&uri, &text, range).await {
1310 Ok(actions) => {
1311 let filtered_actions = if let Some(ref kinds) = requested_kinds
1315 && !kinds.is_empty()
1316 {
1317 actions
1318 .into_iter()
1319 .filter(|action| {
1320 action.kind.as_ref().is_some_and(|action_kind| {
1321 let action_kind_str = action_kind.as_str();
1322 kinds.iter().any(|requested| {
1323 let requested = requested.as_str();
1328 requested.is_empty()
1329 || action_kind_str
1330 .strip_prefix(requested)
1331 .is_some_and(|rest| rest.is_empty() || rest.starts_with('.'))
1332 })
1333 })
1334 })
1335 .collect()
1336 } else {
1337 actions
1338 };
1339
1340 let response: Vec<CodeActionOrCommand> = filtered_actions
1341 .into_iter()
1342 .map(CodeActionOrCommand::CodeAction)
1343 .collect();
1344 Ok(Some(response))
1345 }
1346 Err(e) => {
1347 log::error!("Failed to get code actions: {e}");
1348 Ok(None)
1349 }
1350 }
1351 } else {
1352 Ok(None)
1353 }
1354 }
1355
1356 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1357 let uri = params.text_document.uri;
1358 log::debug!("Range formatting request for {uri} {:?}", params.range);
1359
1360 let Some((text, formatted)) = self.format_document(&uri, ¶ms.options).await else {
1361 log::warn!("Document not found: {uri}");
1362 return Ok(None);
1363 };
1364 Ok(Some(Self::range_edits(&text, &formatted, params.range)))
1365 }
1366
1367 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1368 let uri = params.text_document.uri;
1369 log::debug!("Formatting request for: {uri}");
1370
1371 let Some((text, formatted)) = self.format_document(&uri, ¶ms.options).await else {
1372 log::warn!("Document not found: {uri}");
1373 return Ok(None);
1374 };
1375 if formatted == text {
1376 return Ok(Some(Vec::new()));
1377 }
1378 let edit = TextEdit {
1379 range: Range {
1380 start: Position { line: 0, character: 0 },
1381 end: self.get_end_position(&text),
1382 },
1383 new_text: formatted,
1384 };
1385 Ok(Some(vec![edit]))
1386 }
1387
1388 async fn goto_definition(&self, params: GotoDefinitionParams) -> JsonRpcResult<Option<GotoDefinitionResponse>> {
1389 if !self.config.read().await.enable_link_navigation {
1390 return Ok(None);
1391 }
1392 let uri = params.text_document_position_params.text_document.uri;
1393 let position = params.text_document_position_params.position;
1394
1395 log::debug!("Go-to-definition at {uri} {}:{}", position.line, position.character);
1396
1397 Ok(self.handle_goto_definition(&uri, position).await)
1398 }
1399
1400 async fn references(&self, params: ReferenceParams) -> JsonRpcResult<Option<Vec<Location>>> {
1401 if !self.config.read().await.enable_link_navigation {
1402 return Ok(None);
1403 }
1404 let uri = params.text_document_position.text_document.uri;
1405 let position = params.text_document_position.position;
1406
1407 log::debug!("Find references at {uri} {}:{}", position.line, position.character);
1408
1409 Ok(self.handle_references(&uri, position).await)
1410 }
1411
1412 async fn hover(&self, params: HoverParams) -> JsonRpcResult<Option<Hover>> {
1413 if !self.config.read().await.enable_link_navigation {
1414 return Ok(None);
1415 }
1416 let uri = params.text_document_position_params.text_document.uri;
1417 let position = params.text_document_position_params.position;
1418
1419 log::debug!("Hover at {uri} {}:{}", position.line, position.character);
1420
1421 Ok(self.handle_hover(&uri, position).await)
1422 }
1423
1424 async fn prepare_rename(&self, params: TextDocumentPositionParams) -> JsonRpcResult<Option<PrepareRenameResponse>> {
1425 if !self.config.read().await.enable_link_navigation {
1426 return Ok(None);
1427 }
1428 let uri = params.text_document.uri;
1429 let position = params.position;
1430
1431 log::debug!("Prepare rename at {uri} {}:{}", position.line, position.character);
1432
1433 Ok(self.handle_prepare_rename(&uri, position).await)
1434 }
1435
1436 async fn rename(&self, params: RenameParams) -> JsonRpcResult<Option<WorkspaceEdit>> {
1437 if !self.config.read().await.enable_link_navigation {
1438 return Ok(None);
1439 }
1440 let uri = params.text_document_position.text_document.uri;
1441 let position = params.text_document_position.position;
1442 let new_name = params.new_name;
1443
1444 log::debug!("Rename at {uri} {}:{} → {new_name}", position.line, position.character);
1445
1446 Ok(self.handle_rename(&uri, position, &new_name).await)
1447 }
1448
1449 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
1450 let uri = params.text_document.uri;
1451
1452 if let Some(text) = self.get_open_document_content(&uri).await {
1453 match self.lint_document(&uri, &text, true).await {
1454 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1455 RelatedFullDocumentDiagnosticReport {
1456 related_documents: None,
1457 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1458 result_id: None,
1459 items: diagnostics,
1460 },
1461 },
1462 ))),
1463 Err(e) => {
1464 log::error!("Failed to get diagnostics: {e}");
1465 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1466 RelatedFullDocumentDiagnosticReport {
1467 related_documents: None,
1468 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1469 result_id: None,
1470 items: Vec::new(),
1471 },
1472 },
1473 )))
1474 }
1475 }
1476 } else {
1477 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1478 RelatedFullDocumentDiagnosticReport {
1479 related_documents: None,
1480 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1481 result_id: None,
1482 items: Vec::new(),
1483 },
1484 },
1485 )))
1486 }
1487 }
1488
1489 async fn document_symbol(&self, params: DocumentSymbolParams) -> JsonRpcResult<Option<DocumentSymbolResponse>> {
1490 if !self.config.read().await.enable_symbols {
1491 return Ok(None);
1492 }
1493
1494 let uri = params.text_document.uri;
1495 let Some(text) = self.get_document_content(&uri).await else {
1496 return Ok(None);
1497 };
1498
1499 let flavor = self.resolve_flavor_for_uri(&uri).await;
1500 let analysis = crate::utils::bom::strip_bom(&text);
1503 let mark_units = crate::utils::bom::utf16_len(&text);
1504 let ctx = crate::lint_context::LintContext::new(analysis, flavor, None);
1505
1506 if *self.client_supports_hierarchical_symbols.read().await {
1507 let symbols = super::symbols::document_symbols(&ctx, mark_units);
1508 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Nested(symbols)))
1509 } else {
1510 let symbols = super::symbols::document_symbols_flat(&ctx, &uri, mark_units);
1511 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Flat(symbols)))
1512 }
1513 }
1514
1515 async fn symbol(&self, params: WorkspaceSymbolParams) -> JsonRpcResult<Option<Vec<SymbolInformation>>> {
1516 if !self.config.read().await.enable_symbols {
1517 return Ok(None);
1518 }
1519
1520 let query = params.query.to_lowercase();
1521 let index = self.workspace_index.read().await;
1522 let symbols = super::symbols::workspace_symbols(&index, &query);
1523 Ok(if symbols.is_empty() { None } else { Some(symbols) })
1524 }
1525}
1526
1527#[cfg(test)]
1528#[path = "tests.rs"]
1529mod tests;