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 update_tx.send(update).await.is_ok()
277 }
278
279 pub(super) async fn get_document_content(&self, uri: &Url) -> Option<String> {
285 let uri = &self.store_uri(uri).await;
286
287 {
289 let docs = self.documents.read().await;
290 if let Some(entry) = docs.get(uri) {
291 return Some(entry.content.clone());
292 }
293 }
294
295 if let Ok(path) = uri.to_file_path() {
297 if let Ok(content) = tokio::fs::read_to_string(&path).await {
298 let entry = DocumentEntry {
300 content: content.clone(),
301 version: None,
302 from_disk: true,
303 };
304
305 let mut docs = self.documents.write().await;
306 docs.insert(uri.clone(), entry);
307
308 log::debug!("Loaded document from disk and cached: {uri}");
309 return Some(content);
310 } else {
311 log::debug!("Failed to read file from disk: {uri}");
312 }
313 }
314
315 None
316 }
317
318 async fn get_open_document_content(&self, uri: &Url) -> Option<String> {
324 let uri = self.store_uri(uri).await;
325 let docs = self.documents.read().await;
326 docs.get(&uri)
327 .and_then(|entry| (!entry.from_disk).then(|| entry.content.clone()))
328 }
329
330 async fn reindex_from_disk(&self, path: PathBuf) {
339 let roots = self.workspace_roots.read().await.clone();
340 let (options, includes, excludes) = {
341 let config = self.rumdl_config.read().await;
342 (
343 crate::lsp::index_worker::index_walk_options(&config),
344 config.global.include.clone(),
345 ExcludeMatchers::new(&config.global.exclude),
346 )
347 };
348 let scanned = roots.iter().any(|root| path.starts_with(root))
349 && path.extension().is_some_and(is_markdown_extension)
350 && !crate::lsp::index_worker::path_is_ignored_for_index(&roots, &path, &options, &includes, &excludes);
351 let content = if scanned {
352 crate::lsp::read_markdown_lossy(&path).await.ok()
353 } else {
354 None
355 };
356 let update = match content {
357 Some(content) => IndexUpdate::FileChanged { path, content },
358 None => IndexUpdate::FileRemoved { path },
359 };
360 self.queue_index_update(update).await;
361 }
362
363 async fn store_uri(&self, uri: &Url) -> Url {
375 let Some(spellings) = self.document_aliases.read().await.get(uri).cloned() else {
376 return uri.clone();
377 };
378 let docs = self.documents.read().await;
379 let is_open = |u: &Url| matches!(docs.get(u), Some(entry) if !entry.from_disk);
380 if is_open(uri) {
381 return uri.clone();
382 }
383 spellings
385 .iter()
386 .rev()
387 .find(|u| is_open(u))
388 .cloned()
389 .unwrap_or_else(|| uri.clone())
390 }
391
392 pub(super) async fn resolve_flavor_for_uri(&self, uri: &Url) -> crate::config::MarkdownFlavor {
395 match super::resolve_uri(uri) {
396 Some(path) => self.resolve_config_for_file(&path).await.get_flavor_for_file(&path),
397 None => self.rumdl_config.read().await.markdown_flavor(),
398 }
399 }
400}
401
402#[tower_lsp::async_trait]
403impl LanguageServer for RumdlLanguageServer {
404 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
405 log::info!("Initializing rumdl Language Server");
406
407 if let Some(options) = params.initialization_options
409 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
410 {
411 *self.config.write().await = config;
412 }
413
414 let supports_pull = params
417 .capabilities
418 .text_document
419 .as_ref()
420 .and_then(|td| td.diagnostic.as_ref())
421 .is_some();
422
423 if supports_pull {
424 log::info!("Client supports pull diagnostics - disabling push to avoid duplicates");
425 *self.client_supports_pull_diagnostics.write().await = true;
426 } else {
427 log::info!("Client does not support pull diagnostics - using push model");
428 }
429
430 let supports_hierarchical_symbols = params
433 .capabilities
434 .text_document
435 .as_ref()
436 .and_then(|td| td.document_symbol.as_ref())
437 .and_then(|ds| ds.hierarchical_document_symbol_support)
438 .unwrap_or(false);
439 *self.client_supports_hierarchical_symbols.write().await = supports_hierarchical_symbols;
440
441 let mut roots = Vec::new();
443 if let Some(workspace_folders) = params.workspace_folders {
444 for folder in workspace_folders {
445 if let Ok(path) = folder.uri.to_file_path() {
446 let path = super::resolve_workspace_root(&path);
447 log::info!("Workspace root: {}", path.display());
448 roots.push(path);
449 }
450 }
451 } else if let Some(root_uri) = params.root_uri
452 && let Ok(path) = root_uri.to_file_path()
453 {
454 let path = super::resolve_workspace_root(&path);
455 log::info!("Workspace root: {}", path.display());
456 roots.push(path);
457 }
458 *self.workspace_roots.write().await = roots;
459
460 self.load_configuration(false).await;
462
463 let (enable_link_navigation, enable_link_completions, enable_symbols) = {
464 let config = self.config.read().await;
465 (
466 config.enable_link_navigation,
467 config.enable_link_completions,
468 config.enable_symbols,
469 )
470 };
471
472 Ok(InitializeResult {
473 capabilities: ServerCapabilities {
474 text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
475 open_close: Some(true),
476 change: Some(TextDocumentSyncKind::FULL),
477 will_save: Some(false),
478 will_save_wait_until: Some(true),
479 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
480 include_text: Some(false),
481 })),
482 })),
483 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
484 code_action_kinds: Some(vec![
485 CodeActionKind::QUICKFIX,
486 CodeActionKind::SOURCE_FIX_ALL,
487 CodeActionKind::new("source.fixAll.rumdl"),
488 ]),
489 work_done_progress_options: WorkDoneProgressOptions::default(),
490 resolve_provider: None,
491 })),
492 document_formatting_provider: Some(OneOf::Left(true)),
493 document_range_formatting_provider: Some(OneOf::Left(true)),
494 document_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
495 workspace_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
496 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
497 identifier: Some("rumdl".to_string()),
498 inter_file_dependencies: true,
499 workspace_diagnostics: false,
500 work_done_progress_options: WorkDoneProgressOptions::default(),
501 })),
502 completion_provider: Some(CompletionOptions {
508 trigger_characters: Some(if enable_link_completions {
509 vec![
510 "`".to_string(),
511 "(".to_string(),
512 "#".to_string(),
513 "/".to_string(),
514 ".".to_string(),
515 "-".to_string(),
516 ]
517 } else {
518 vec!["`".to_string()]
519 }),
520 resolve_provider: Some(false),
521 work_done_progress_options: WorkDoneProgressOptions::default(),
522 all_commit_characters: None,
523 completion_item: None,
524 }),
525 definition_provider: enable_link_navigation.then_some(OneOf::Left(true)),
526 references_provider: enable_link_navigation.then_some(OneOf::Left(true)),
527 hover_provider: enable_link_navigation.then_some(HoverProviderCapability::Simple(true)),
528 rename_provider: enable_link_navigation.then_some(OneOf::Right(RenameOptions {
529 prepare_provider: Some(true),
530 work_done_progress_options: WorkDoneProgressOptions::default(),
531 })),
532 workspace: Some(WorkspaceServerCapabilities {
533 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
534 supported: Some(true),
535 change_notifications: Some(OneOf::Left(true)),
536 }),
537 file_operations: None,
538 }),
539 ..Default::default()
540 },
541 server_info: Some(ServerInfo {
542 name: "rumdl".to_string(),
543 version: Some(env!("CARGO_PKG_VERSION").to_string()),
544 }),
545 })
546 }
547
548 async fn initialized(&self, _: InitializedParams) {
549 let version = env!("CARGO_PKG_VERSION");
550
551 let (binary_path, build_time) = std::env::current_exe().ok().map_or_else(
553 || ("unknown".to_string(), "unknown".to_string()),
554 |path| {
555 let path_str = path.to_str().unwrap_or("unknown").to_string();
556 let build_time = std::fs::metadata(&path)
557 .ok()
558 .and_then(|metadata| metadata.modified().ok())
559 .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
560 .and_then(|duration| {
561 let secs = duration.as_secs();
562 chrono::DateTime::from_timestamp(secs as i64, 0)
563 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
564 })
565 .unwrap_or_else(|| "unknown".to_string());
566 (path_str, build_time)
567 },
568 );
569
570 let working_dir = std::env::current_dir()
571 .ok()
572 .and_then(|p| p.to_str().map(std::string::ToString::to_string))
573 .unwrap_or_else(|| "unknown".to_string());
574
575 log::info!("rumdl Language Server v{version} initialized (built: {build_time}, binary: {binary_path})");
576 log::info!("Working directory: {working_dir}");
577
578 self.client
579 .log_message(MessageType::INFO, format!("rumdl v{version} Language Server started"))
580 .await;
581
582 if !self.queue_index_update(IndexUpdate::FullRescan).await {
584 log::warn!("Failed to trigger initial workspace indexing");
585 } else {
586 log::info!("Triggered initial workspace indexing for cross-file analysis");
587 }
588
589 let markdown_patterns = [
591 "**/*.md",
592 "**/*.markdown",
593 "**/*.mdx",
594 "**/*.mkd",
595 "**/*.mkdn",
596 "**/*.mdown",
597 "**/*.mdwn",
598 "**/*.qmd",
599 "**/*.rmd",
600 ];
601 let config_patterns = [
605 "**/.rumdl.toml",
606 "**/rumdl.toml",
607 "**/pyproject.toml",
608 "**/.markdownlint.json",
609 "**/.markdownlint-cli2.yaml",
610 "**/.markdownlint-cli2.jsonc",
611 "**/.editorconfig",
612 ];
613 let watchers: Vec<_> = markdown_patterns
614 .iter()
615 .chain(config_patterns.iter())
616 .map(|pattern| FileSystemWatcher {
617 glob_pattern: GlobPattern::String((*pattern).to_string()),
618 kind: Some(WatchKind::all()),
619 })
620 .collect();
621
622 let registration = Registration {
623 id: "markdown-watcher".to_string(),
624 method: "workspace/didChangeWatchedFiles".to_string(),
625 register_options: Some(
626 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { watchers }).unwrap(),
627 ),
628 };
629
630 if self.client.register_capability(vec![registration]).await.is_err() {
631 log::debug!("Client does not support file watching capability");
632 }
633 }
634
635 async fn completion(&self, params: CompletionParams) -> JsonRpcResult<Option<CompletionResponse>> {
636 let uri = params.text_document_position.text_document.uri;
637 let position = params.text_document_position.position;
638
639 let Some(text) = self.get_document_content(&uri).await else {
641 return Ok(None);
642 };
643
644 if let Some((start_col, current_text)) = Self::detect_code_fence_language_position(&text, position) {
646 log::debug!(
647 "Code fence completion triggered at {}:{}, current text: '{}'",
648 position.line,
649 position.character,
650 current_text
651 );
652 let items = self
653 .get_language_completions(&uri, ¤t_text, start_col, position)
654 .await;
655 if !items.is_empty() {
656 return Ok(Some(CompletionResponse::Array(items)));
657 }
658 }
659
660 if self.config.read().await.enable_link_completions {
662 let trigger = params.context.as_ref().and_then(|c| c.trigger_character.as_deref());
666 let skip_link_check = matches!(trigger, Some("." | "-")) && {
667 let line_num = position.line as usize;
668 !text.lines().nth(line_num).is_some_and(|line| line.contains("]("))
671 };
672
673 if !skip_link_check && let Some(link_info) = Self::detect_link_target_position(&text, position) {
674 if let Some((partial_anchor, anchor_start_col)) = link_info.anchor {
675 log::debug!(
676 "Anchor completion triggered at {}:{}, file: '{}', partial: '{}'",
677 position.line,
678 position.character,
679 link_info.file_path,
680 partial_anchor
681 );
682 let items = self
683 .get_anchor_completions(&uri, &link_info.file_path, &partial_anchor, anchor_start_col, position)
684 .await;
685 if !items.is_empty() {
686 return Ok(Some(CompletionResponse::Array(items)));
687 }
688 } else {
689 log::debug!(
690 "File path completion triggered at {}:{}, partial: '{}'",
691 position.line,
692 position.character,
693 link_info.file_path
694 );
695 let list = self
696 .get_file_completions(&uri, &link_info.file_path, link_info.path_start_col, position)
697 .await;
698 if !list.items.is_empty() {
699 return Ok(Some(CompletionResponse::List(list)));
700 }
701 }
702 }
703 }
704
705 Ok(None)
706 }
707
708 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
709 let mut roots = self.workspace_roots.write().await;
711
712 for removed in ¶ms.event.removed {
716 if let Ok(path) = removed.uri.to_file_path() {
717 let path = super::resolve_workspace_root(&path);
718 roots.retain(|r| r != &path);
719 log::info!("Removed workspace root: {}", path.display());
720 }
721 }
722
723 for added in ¶ms.event.added {
725 if let Ok(path) = added.uri.to_file_path()
726 && let path = super::resolve_workspace_root(&path)
727 && !roots.contains(&path)
728 {
729 log::info!("Added workspace root: {}", path.display());
730 roots.push(path);
731 }
732 }
733 drop(roots);
734
735 self.config_cache.write().await.clear();
737
738 self.reload_configuration().await;
740
741 if !self.queue_index_update(IndexUpdate::FullRescan).await {
743 log::warn!("Failed to trigger workspace rescan after folder change");
744 }
745 }
746
747 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
748 log::debug!("Configuration changed: {:?}", params.settings);
749
750 let settings_value = params.settings;
754
755 let rumdl_settings = if let serde_json::Value::Object(ref obj) = settings_value {
757 obj.get("rumdl").cloned().unwrap_or(settings_value.clone())
758 } else {
759 settings_value
760 };
761
762 let mut config_applied = false;
764 let mut warnings: Vec<String> = Vec::new();
765
766 if let Ok(rule_settings) = serde_json::from_value::<LspRuleSettings>(rumdl_settings.clone())
770 && (rule_settings.disable.is_some()
771 || rule_settings.enable.is_some()
772 || rule_settings.line_length.is_some()
773 || (!rule_settings.rules.is_empty() && rule_settings.rules.keys().all(|k| is_valid_rule_name(k))))
774 {
775 if let Some(ref disable) = rule_settings.disable {
777 for rule in disable {
778 if !is_valid_rule_name(rule) {
779 warnings.push(format!("Unknown rule in disable list: {rule}"));
780 }
781 }
782 }
783 if let Some(ref enable) = rule_settings.enable {
784 for rule in enable {
785 if !is_valid_rule_name(rule) {
786 warnings.push(format!("Unknown rule in enable list: {rule}"));
787 }
788 }
789 }
790 for rule_name in rule_settings.rules.keys() {
792 if !is_valid_rule_name(rule_name) {
793 warnings.push(format!("Unknown rule in settings: {rule_name}"));
794 }
795 }
796
797 log::info!("Applied rule settings from configuration (Neovim style)");
798 let mut config = self.config.write().await;
799 config.settings = Some(rule_settings);
800 drop(config);
801 config_applied = true;
802 } else if names_lsp_config_field(&rumdl_settings)
803 && let Ok(full_config) = serde_json::from_value::<RumdlLspConfig>(rumdl_settings.clone())
804 {
805 if let Some(ref rules) = full_config.enable_rules {
807 for rule in rules {
808 if !is_valid_rule_name(rule) {
809 warnings.push(format!("Unknown rule in enableRules: {rule}"));
810 }
811 }
812 }
813 if let Some(ref rules) = full_config.disable_rules {
814 for rule in rules {
815 if !is_valid_rule_name(rule) {
816 warnings.push(format!("Unknown rule in disableRules: {rule}"));
817 }
818 }
819 }
820
821 {
829 let mut config = self.config.write().await;
830 if let Some(merged) = merge_lsp_config(&config, &rumdl_settings) {
831 *config = merged;
832 drop(config);
833 log::info!("Merged LSP configuration from client settings");
834 config_applied = true;
835 } else {
836 drop(config);
837 warnings.push("Could not merge LSP configuration update; keeping current settings".to_string());
838 }
839 }
840 } else if let serde_json::Value::Object(obj) = rumdl_settings {
841 let mut config = self.config.write().await;
844
845 let mut rules = std::collections::HashMap::new();
847 let mut disable = Vec::new();
848 let mut enable = Vec::new();
849 let mut line_length = None;
850 let mut names_a_rule_setting = false;
854
855 for (key, value) in obj {
856 match key.as_str() {
857 "disable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
858 Ok(d) => {
859 if d.len() > MAX_RULE_LIST_SIZE {
860 warnings.push(format!(
861 "Too many rules in 'disable' ({} > {}), truncating",
862 d.len(),
863 MAX_RULE_LIST_SIZE
864 ));
865 }
866 for rule in d.iter().take(MAX_RULE_LIST_SIZE) {
867 if !is_valid_rule_name(rule) {
868 warnings.push(format!("Unknown rule in disable: {rule}"));
869 }
870 }
871 disable = d.into_iter().take(MAX_RULE_LIST_SIZE).collect();
872 names_a_rule_setting = true;
873 }
874 Err(_) => {
875 warnings.push(format!(
876 "Invalid 'disable' value: expected array of strings, got {value}"
877 ));
878 }
879 },
880 "enable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
881 Ok(e) => {
882 if e.len() > MAX_RULE_LIST_SIZE {
883 warnings.push(format!(
884 "Too many rules in 'enable' ({} > {}), truncating",
885 e.len(),
886 MAX_RULE_LIST_SIZE
887 ));
888 }
889 for rule in e.iter().take(MAX_RULE_LIST_SIZE) {
890 if !is_valid_rule_name(rule) {
891 warnings.push(format!("Unknown rule in enable: {rule}"));
892 }
893 }
894 enable = e.into_iter().take(MAX_RULE_LIST_SIZE).collect();
895 names_a_rule_setting = true;
896 }
897 Err(_) => {
898 warnings.push(format!(
899 "Invalid 'enable' value: expected array of strings, got {value}"
900 ));
901 }
902 },
903 "lineLength" | "line_length" | "line-length" => {
904 if let Some(l) = value.as_u64() {
905 match usize::try_from(l) {
906 Ok(len) if len <= MAX_LINE_LENGTH => {
907 line_length = Some(len);
908 names_a_rule_setting = true;
909 }
910 Ok(len) => warnings.push(format!(
911 "Invalid 'lineLength' value: {len} exceeds maximum ({MAX_LINE_LENGTH})"
912 )),
913 Err(_) => warnings.push(format!("Invalid 'lineLength' value: {l} is too large")),
914 }
915 } else {
916 warnings.push(format!("Invalid 'lineLength' value: expected number, got {value}"));
917 }
918 }
919 _ if key.starts_with("MD") || key.starts_with("md") => {
921 let normalized = key.to_uppercase();
922 if !is_valid_rule_name(&normalized) {
923 warnings.push(format!("Unknown rule: {key}"));
924 }
925 rules.insert(normalized, value);
926 names_a_rule_setting = true;
927 }
928 _ => {
929 warnings.push(format!("Unknown configuration key: {key}"));
931 }
932 }
933 }
934
935 let settings = LspRuleSettings {
936 line_length,
937 disable: if disable.is_empty() { None } else { Some(disable) },
938 enable: if enable.is_empty() { None } else { Some(enable) },
939 rules,
940 };
941
942 if names_a_rule_setting {
943 log::info!("Applied Neovim-style rule settings (manual parse)");
944 config.settings = Some(settings);
945 config_applied = true;
946 }
947 drop(config);
948 } else {
949 log::warn!("Could not parse configuration settings: {rumdl_settings:?}");
950 }
951
952 for warning in &warnings {
954 log::warn!("{warning}");
955 }
956
957 if !warnings.is_empty() {
959 let message = if warnings.len() == 1 {
960 format!("rumdl: {}", warnings[0])
961 } else {
962 format!("rumdl configuration warnings:\n{}", warnings.join("\n"))
963 };
964 self.client.log_message(MessageType::WARNING, message).await;
965 }
966
967 if !config_applied {
968 log::debug!("No configuration changes applied");
969 }
970
971 self.config_cache.write().await.clear();
973
974 if config_applied {
982 self.load_configuration(false).await;
983
984 if !self.queue_index_update(IndexUpdate::FullRescan).await {
988 log::warn!("Failed to request workspace rescan after configuration change");
989 }
990 }
991
992 let doc_list: Vec<_> = {
997 let documents = self.documents.read().await;
998 documents
999 .iter()
1000 .filter(|(_, entry)| !entry.from_disk)
1001 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
1002 .collect()
1003 };
1004
1005 let tasks: Vec<_> = doc_list
1009 .into_iter()
1010 .map(|(uri, text)| {
1011 let server = self.clone();
1012 tokio::spawn(async move {
1013 server.update_diagnostics(uri, text, true).await;
1014 })
1015 })
1016 .collect();
1017
1018 for task in tasks {
1020 let _ = task.await;
1021 }
1022 }
1023
1024 async fn shutdown(&self) -> JsonRpcResult<()> {
1025 log::info!("Shutting down rumdl Language Server");
1026
1027 self.queue_index_update(IndexUpdate::Shutdown).await;
1029
1030 Ok(())
1031 }
1032
1033 async fn did_open(&self, params: DidOpenTextDocumentParams) {
1034 let uri = params.text_document.uri;
1035 let text = params.text_document.text;
1036 let version = params.text_document.version;
1037
1038 let entry = DocumentEntry {
1039 content: text.clone(),
1040 version: Some(version),
1041 from_disk: false,
1042 };
1043 self.documents.write().await.insert(uri.clone(), entry);
1044
1045 let resolved = super::resolve_uri_spelling(&uri);
1047 if resolved != uri {
1048 let mut aliases = self.document_aliases.write().await;
1049 let spellings = aliases.entry(resolved).or_default();
1050 if !spellings.contains(&uri) {
1051 spellings.push(uri.clone());
1052 }
1053 }
1054
1055 if let Some(path) = super::resolve_uri(&uri) {
1057 self.queue_index_update(IndexUpdate::FileChanged {
1058 path,
1059 content: text.clone(),
1060 })
1061 .await;
1062 }
1063
1064 self.update_diagnostics(uri, text, true).await;
1065 }
1066
1067 async fn did_change(&self, params: DidChangeTextDocumentParams) {
1068 let uri = params.text_document.uri;
1069 let version = params.text_document.version;
1070
1071 let base = if params.content_changes.iter().all(|change| change.range.is_some()) {
1074 match self.documents.read().await.get(&uri) {
1075 Some(entry) => Some(entry.content.clone()),
1076 None => {
1077 log::warn!("Ignoring an incremental change to {uri}, which is not open");
1078 return;
1079 }
1080 }
1081 } else {
1082 None
1083 };
1084
1085 if !params.content_changes.is_empty() {
1086 let text = super::position::apply_content_changes(base.unwrap_or_default(), params.content_changes);
1087
1088 let entry = DocumentEntry {
1089 content: text.clone(),
1090 version: Some(version),
1091 from_disk: false,
1092 };
1093 self.documents.write().await.insert(uri.clone(), entry);
1094
1095 if let Some(path) = super::resolve_uri(&uri) {
1097 self.queue_index_update(IndexUpdate::FileChanged {
1098 path,
1099 content: text.clone(),
1100 })
1101 .await;
1102 }
1103
1104 self.update_diagnostics(uri, text, false).await;
1105 }
1106 }
1107
1108 async fn will_save_wait_until(&self, params: WillSaveTextDocumentParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1109 if params.reason != TextDocumentSaveReason::MANUAL {
1112 return Ok(None);
1113 }
1114
1115 let config_guard = self.config.read().await;
1116 let enable_auto_fix = config_guard.enable_auto_fix;
1117 drop(config_guard);
1118
1119 if !enable_auto_fix {
1120 return Ok(None);
1121 }
1122
1123 let Some(text) = self.get_document_content(¶ms.text_document.uri).await else {
1125 return Ok(None);
1126 };
1127
1128 match self.apply_all_fixes(¶ms.text_document.uri, &text).await {
1130 Ok(Some(fixed_text)) => {
1131 Ok(Some(vec![TextEdit {
1133 range: Range {
1134 start: Position { line: 0, character: 0 },
1135 end: self.get_end_position(&text),
1136 },
1137 new_text: fixed_text,
1138 }]))
1139 }
1140 Ok(None) => Ok(None),
1141 Err(e) => {
1142 log::error!("Failed to generate fixes in will_save_wait_until: {e}");
1143 Ok(None)
1144 }
1145 }
1146 }
1147
1148 async fn did_save(&self, params: DidSaveTextDocumentParams) {
1149 if let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
1152 self.update_diagnostics(params.text_document.uri, entry.content.clone(), true)
1153 .await;
1154 }
1155 }
1156
1157 async fn did_close(&self, params: DidCloseTextDocumentParams) {
1158 self.documents.write().await.remove(¶ms.text_document.uri);
1160 let resolved = super::resolve_uri_spelling(¶ms.text_document.uri);
1163 if resolved != params.text_document.uri {
1164 let mut aliases = self.document_aliases.write().await;
1165 if let Some(spellings) = aliases.get_mut(&resolved) {
1166 spellings.retain(|u| u != ¶ms.text_document.uri);
1167 if spellings.is_empty() {
1168 aliases.remove(&resolved);
1169 }
1170 }
1171 }
1172
1173 if let Some(path) = super::resolve_uri(¶ms.text_document.uri) {
1177 match self.get_open_document_content(&resolved).await {
1178 Some(content) => {
1179 self.queue_index_update(IndexUpdate::FileChanged { path, content })
1180 .await;
1181 }
1182 None => self.reindex_from_disk(path).await,
1183 }
1184 }
1185
1186 self.client
1189 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
1190 .await;
1191 }
1192
1193 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
1194 const CONFIG_FILES: &[&str] = &[
1196 ".rumdl.toml",
1197 "rumdl.toml",
1198 "pyproject.toml",
1199 ".markdownlint.json",
1200 ".markdownlint-cli2.jsonc",
1201 ".markdownlint-cli2.yaml",
1202 ".markdownlint-cli2.yml",
1203 ];
1204
1205 let mut config_changed = false;
1206 let reads_editorconfig = self.reads_editorconfig().await;
1209
1210 for change in ¶ms.changes {
1211 if let Some(path) = super::resolve_uri(&change.uri) {
1215 let file_name = path.file_name().and_then(|f| f.to_str());
1216
1217 if let Some(name) = file_name
1219 && (CONFIG_FILES.contains(&name) || (reads_editorconfig && name == ".editorconfig"))
1220 && !config_changed
1221 {
1222 log::info!("Config file changed: {}, invalidating config cache", path.display());
1223
1224 let mut cache = self.config_cache.write().await;
1228 cache.clear();
1229
1230 drop(cache);
1232 self.reload_configuration().await;
1233 config_changed = true;
1234 }
1235
1236 if let Some(ext) = path.extension()
1238 && is_markdown_extension(ext)
1239 {
1240 match change.typ {
1241 FileChangeType::CREATED | FileChangeType::CHANGED => {
1242 if let Some(content) = self
1252 .get_open_document_content(&super::resolve_uri_spelling(&change.uri))
1253 .await
1254 {
1255 self.queue_index_update(IndexUpdate::FileChanged {
1256 path: path.clone(),
1257 content,
1258 })
1259 .await;
1260 continue;
1261 }
1262 self.reindex_from_disk(path.clone()).await;
1263 }
1264 FileChangeType::DELETED => {
1265 self.queue_index_update(IndexUpdate::FileRemoved { path: path.clone() })
1266 .await;
1267 }
1268 _ => {}
1269 }
1270 }
1271 }
1272 }
1273
1274 if config_changed {
1276 if !self.queue_index_update(IndexUpdate::FullRescan).await {
1280 log::warn!("Failed to request workspace rescan after config change");
1281 }
1282
1283 let docs_to_update: Vec<(Url, String)> = {
1284 let docs = self.documents.read().await;
1285 docs.iter()
1286 .filter(|(_, entry)| !entry.from_disk)
1287 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
1288 .collect()
1289 };
1290
1291 for (uri, text) in docs_to_update {
1292 self.update_diagnostics(uri, text, true).await;
1293 }
1294 }
1295 }
1296
1297 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
1298 let uri = params.text_document.uri;
1299 let range = params.range;
1300 let requested_kinds = params.context.only;
1301
1302 if let Some(text) = self.get_document_content(&uri).await {
1303 match self.get_code_actions(&uri, &text, range).await {
1304 Ok(actions) => {
1305 let filtered_actions = if let Some(ref kinds) = requested_kinds
1309 && !kinds.is_empty()
1310 {
1311 actions
1312 .into_iter()
1313 .filter(|action| {
1314 action.kind.as_ref().is_some_and(|action_kind| {
1315 let action_kind_str = action_kind.as_str();
1316 kinds.iter().any(|requested| {
1317 let requested = requested.as_str();
1322 requested.is_empty()
1323 || action_kind_str
1324 .strip_prefix(requested)
1325 .is_some_and(|rest| rest.is_empty() || rest.starts_with('.'))
1326 })
1327 })
1328 })
1329 .collect()
1330 } else {
1331 actions
1332 };
1333
1334 let response: Vec<CodeActionOrCommand> = filtered_actions
1335 .into_iter()
1336 .map(CodeActionOrCommand::CodeAction)
1337 .collect();
1338 Ok(Some(response))
1339 }
1340 Err(e) => {
1341 log::error!("Failed to get code actions: {e}");
1342 Ok(None)
1343 }
1344 }
1345 } else {
1346 Ok(None)
1347 }
1348 }
1349
1350 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1351 let uri = params.text_document.uri;
1352 log::debug!("Range formatting request for {uri} {:?}", params.range);
1353
1354 let Some((text, formatted)) = self.format_document(&uri, ¶ms.options).await else {
1355 log::warn!("Document not found: {uri}");
1356 return Ok(None);
1357 };
1358 Ok(Some(Self::range_edits(&text, &formatted, params.range)))
1359 }
1360
1361 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1362 let uri = params.text_document.uri;
1363 log::debug!("Formatting request for: {uri}");
1364
1365 let Some((text, formatted)) = self.format_document(&uri, ¶ms.options).await else {
1366 log::warn!("Document not found: {uri}");
1367 return Ok(None);
1368 };
1369 if formatted == text {
1370 return Ok(Some(Vec::new()));
1371 }
1372 let edit = TextEdit {
1373 range: Range {
1374 start: Position { line: 0, character: 0 },
1375 end: self.get_end_position(&text),
1376 },
1377 new_text: formatted,
1378 };
1379 Ok(Some(vec![edit]))
1380 }
1381
1382 async fn goto_definition(&self, params: GotoDefinitionParams) -> JsonRpcResult<Option<GotoDefinitionResponse>> {
1383 if !self.config.read().await.enable_link_navigation {
1384 return Ok(None);
1385 }
1386 let uri = params.text_document_position_params.text_document.uri;
1387 let position = params.text_document_position_params.position;
1388
1389 log::debug!("Go-to-definition at {uri} {}:{}", position.line, position.character);
1390
1391 Ok(self.handle_goto_definition(&uri, position).await)
1392 }
1393
1394 async fn references(&self, params: ReferenceParams) -> JsonRpcResult<Option<Vec<Location>>> {
1395 if !self.config.read().await.enable_link_navigation {
1396 return Ok(None);
1397 }
1398 let uri = params.text_document_position.text_document.uri;
1399 let position = params.text_document_position.position;
1400
1401 log::debug!("Find references at {uri} {}:{}", position.line, position.character);
1402
1403 Ok(self.handle_references(&uri, position).await)
1404 }
1405
1406 async fn hover(&self, params: HoverParams) -> JsonRpcResult<Option<Hover>> {
1407 if !self.config.read().await.enable_link_navigation {
1408 return Ok(None);
1409 }
1410 let uri = params.text_document_position_params.text_document.uri;
1411 let position = params.text_document_position_params.position;
1412
1413 log::debug!("Hover at {uri} {}:{}", position.line, position.character);
1414
1415 Ok(self.handle_hover(&uri, position).await)
1416 }
1417
1418 async fn prepare_rename(&self, params: TextDocumentPositionParams) -> JsonRpcResult<Option<PrepareRenameResponse>> {
1419 if !self.config.read().await.enable_link_navigation {
1420 return Ok(None);
1421 }
1422 let uri = params.text_document.uri;
1423 let position = params.position;
1424
1425 log::debug!("Prepare rename at {uri} {}:{}", position.line, position.character);
1426
1427 Ok(self.handle_prepare_rename(&uri, position).await)
1428 }
1429
1430 async fn rename(&self, params: RenameParams) -> JsonRpcResult<Option<WorkspaceEdit>> {
1431 if !self.config.read().await.enable_link_navigation {
1432 return Ok(None);
1433 }
1434 let uri = params.text_document_position.text_document.uri;
1435 let position = params.text_document_position.position;
1436 let new_name = params.new_name;
1437
1438 log::debug!("Rename at {uri} {}:{} → {new_name}", position.line, position.character);
1439
1440 Ok(self.handle_rename(&uri, position, &new_name).await)
1441 }
1442
1443 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
1444 let uri = params.text_document.uri;
1445
1446 if let Some(text) = self.get_open_document_content(&uri).await {
1447 match self.lint_document(&uri, &text, true).await {
1448 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1449 RelatedFullDocumentDiagnosticReport {
1450 related_documents: None,
1451 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1452 result_id: None,
1453 items: diagnostics,
1454 },
1455 },
1456 ))),
1457 Err(e) => {
1458 log::error!("Failed to get diagnostics: {e}");
1459 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1460 RelatedFullDocumentDiagnosticReport {
1461 related_documents: None,
1462 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1463 result_id: None,
1464 items: Vec::new(),
1465 },
1466 },
1467 )))
1468 }
1469 }
1470 } else {
1471 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1472 RelatedFullDocumentDiagnosticReport {
1473 related_documents: None,
1474 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1475 result_id: None,
1476 items: Vec::new(),
1477 },
1478 },
1479 )))
1480 }
1481 }
1482
1483 async fn document_symbol(&self, params: DocumentSymbolParams) -> JsonRpcResult<Option<DocumentSymbolResponse>> {
1484 if !self.config.read().await.enable_symbols {
1485 return Ok(None);
1486 }
1487
1488 let uri = params.text_document.uri;
1489 let Some(text) = self.get_document_content(&uri).await else {
1490 return Ok(None);
1491 };
1492
1493 let flavor = self.resolve_flavor_for_uri(&uri).await;
1494 let ctx = crate::lint_context::LintContext::new(&text, flavor, None);
1495
1496 if *self.client_supports_hierarchical_symbols.read().await {
1497 let symbols = super::symbols::document_symbols(&ctx);
1498 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Nested(symbols)))
1499 } else {
1500 let symbols = super::symbols::document_symbols_flat(&ctx, &uri);
1501 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Flat(symbols)))
1502 }
1503 }
1504
1505 async fn symbol(&self, params: WorkspaceSymbolParams) -> JsonRpcResult<Option<Vec<SymbolInformation>>> {
1506 if !self.config.read().await.enable_symbols {
1507 return Ok(None);
1508 }
1509
1510 let query = params.query.to_lowercase();
1511 let index = self.workspace_index.read().await;
1512 let symbols = super::symbols::workspace_symbols(&index, &query);
1513 Ok(if symbols.is_empty() { None } else { Some(symbols) })
1514 }
1515}
1516
1517#[cfg(test)]
1518#[path = "tests.rs"]
1519mod tests;