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 merge_lsp_config(current: &RumdlLspConfig, incoming: &serde_json::Value) -> Option<RumdlLspConfig> {
39 let serde_json::Value::Object(incoming) = incoming else {
40 return None;
41 };
42 let serde_json::Value::Object(mut base) = serde_json::to_value(current).ok()? else {
43 return None;
44 };
45 for (key, value) in incoming {
46 base.insert(key.clone(), value.clone());
47 }
48 serde_json::from_value(serde_json::Value::Object(base)).ok()
49}
50
51#[derive(Clone, Debug, PartialEq)]
53pub(crate) struct DocumentEntry {
54 pub(crate) content: String,
56 pub(crate) version: Option<i32>,
58 pub(crate) from_disk: bool,
60}
61
62#[derive(Clone, Debug)]
64pub(crate) struct ConfigCacheEntry {
65 pub(crate) config: Config,
67 pub(crate) sourced: Option<Arc<SourcedConfig<ConfigValidated>>>,
72 pub(crate) config_file: Option<PathBuf>,
74 pub(crate) from_global_fallback: bool,
76}
77
78#[derive(Clone)]
85pub(crate) struct ConfigResolver {
86 pub(super) config: Arc<RwLock<RumdlLspConfig>>,
87 pub(super) rumdl_config: Arc<RwLock<Config>>,
88 pub(super) rumdl_sourced: Arc<RwLock<Option<Arc<SourcedConfig<ConfigValidated>>>>>,
89 pub(super) workspace_roots: Arc<RwLock<Vec<PathBuf>>>,
90 pub(super) config_cache: Arc<RwLock<HashMap<PathBuf, ConfigCacheEntry>>>,
91 pub(super) cli_config_path: Option<String>,
92}
93
94#[derive(Clone)]
104pub struct RumdlLanguageServer {
105 pub(crate) client: Client,
106 pub(crate) config: Arc<RwLock<RumdlLspConfig>>,
108 pub(crate) rumdl_config: Arc<RwLock<Config>>,
110 pub(crate) rumdl_sourced: Arc<RwLock<Option<Arc<SourcedConfig<ConfigValidated>>>>>,
113 pub(crate) documents: Arc<RwLock<HashMap<Url, DocumentEntry>>>,
115 pub(crate) document_aliases: Arc<RwLock<HashMap<Url, Vec<Url>>>>,
128 pub(crate) workspace_roots: Arc<RwLock<Vec<PathBuf>>>,
130 pub(crate) config_cache: Arc<RwLock<HashMap<PathBuf, ConfigCacheEntry>>>,
133 pub(crate) config_resolver: ConfigResolver,
135 pub(crate) workspace_index: Arc<RwLock<WorkspaceIndex>>,
137 pub(crate) index_state: Arc<RwLock<IndexState>>,
139 update_tx: Option<mpsc::Sender<IndexUpdate>>,
147 pub(crate) client_supports_pull_diagnostics: Arc<RwLock<bool>>,
150 pub(crate) client_supports_hierarchical_symbols: Arc<RwLock<bool>>,
154 pub(crate) cli_config_path: Option<String>,
162}
163
164impl RumdlLanguageServer {
165 pub fn new(client: Client, cli_config_path: Option<&str>) -> Self {
166 let initial_config = RumdlLspConfig::default();
167 let cli_config_path = cli_config_path.map(str::to_string);
168
169 let workspace_index = Arc::new(RwLock::new(WorkspaceIndex::new()));
171 let index_state = Arc::new(RwLock::new(IndexState::default()));
172 let workspace_roots = Arc::new(RwLock::new(Vec::new()));
173 let config = Arc::new(RwLock::new(initial_config));
174 let rumdl_config = Arc::new(RwLock::new(Config::default()));
175 let rumdl_sourced = Arc::new(RwLock::new(None));
176 let config_cache = Arc::new(RwLock::new(HashMap::new()));
177 let documents = Arc::new(RwLock::new(HashMap::new()));
178
179 let config_resolver = ConfigResolver {
180 config: config.clone(),
181 rumdl_config: rumdl_config.clone(),
182 rumdl_sourced: rumdl_sourced.clone(),
183 workspace_roots: workspace_roots.clone(),
184 config_cache: config_cache.clone(),
185 cli_config_path: cli_config_path.clone(),
186 };
187
188 let (update_tx, update_rx) = mpsc::channel::<IndexUpdate>(100);
190 let (relint_tx, relint_rx) = mpsc::channel::<RelintRequest>(100);
191
192 let server = Self {
193 client,
194 config,
195 rumdl_config,
196 rumdl_sourced,
197 documents,
198 document_aliases: Arc::new(RwLock::new(HashMap::new())),
199 workspace_roots,
200 config_cache,
201 config_resolver: config_resolver.clone(),
202 workspace_index,
203 index_state,
204 update_tx: Some(update_tx),
205 client_supports_pull_diagnostics: Arc::new(RwLock::new(false)),
206 client_supports_hierarchical_symbols: Arc::new(RwLock::new(false)),
207 cli_config_path,
208 };
209
210 let worker = IndexWorker::new(
214 update_rx,
215 server.client.clone(),
216 relint_tx,
217 SharedIndexState {
218 workspace_index: server.workspace_index.clone(),
219 index_state: server.index_state.clone(),
220 workspace_roots: server.workspace_roots.clone(),
221 config_resolver,
222 documents: server.documents.clone(),
223 },
224 );
225 tokio::spawn(worker.run());
226
227 tokio::spawn(server.detached_for_background().run_relint_worker(relint_rx));
232
233 server
234 }
235
236 fn detached_for_background(&self) -> Self {
247 Self {
248 update_tx: None,
249 ..self.clone()
250 }
251 }
252
253 pub(crate) async fn queue_index_update(&self, update: IndexUpdate) -> bool {
259 let Some(update_tx) = &self.update_tx else {
260 return false;
261 };
262 update_tx.send(update).await.is_ok()
263 }
264
265 pub(super) async fn get_document_content(&self, uri: &Url) -> Option<String> {
271 let uri = &self.store_uri(uri).await;
272
273 {
275 let docs = self.documents.read().await;
276 if let Some(entry) = docs.get(uri) {
277 return Some(entry.content.clone());
278 }
279 }
280
281 if let Ok(path) = uri.to_file_path() {
283 if let Ok(content) = tokio::fs::read_to_string(&path).await {
284 let entry = DocumentEntry {
286 content: content.clone(),
287 version: None,
288 from_disk: true,
289 };
290
291 let mut docs = self.documents.write().await;
292 docs.insert(uri.clone(), entry);
293
294 log::debug!("Loaded document from disk and cached: {uri}");
295 return Some(content);
296 } else {
297 log::debug!("Failed to read file from disk: {uri}");
298 }
299 }
300
301 None
302 }
303
304 async fn get_open_document_content(&self, uri: &Url) -> Option<String> {
310 let uri = self.store_uri(uri).await;
311 let docs = self.documents.read().await;
312 docs.get(&uri)
313 .and_then(|entry| (!entry.from_disk).then(|| entry.content.clone()))
314 }
315
316 async fn store_uri(&self, uri: &Url) -> Url {
328 let Some(spellings) = self.document_aliases.read().await.get(uri).cloned() else {
329 return uri.clone();
330 };
331 let docs = self.documents.read().await;
332 let is_open = |u: &Url| matches!(docs.get(u), Some(entry) if !entry.from_disk);
333 if is_open(uri) {
334 return uri.clone();
335 }
336 spellings
338 .iter()
339 .rev()
340 .find(|u| is_open(u))
341 .cloned()
342 .unwrap_or_else(|| uri.clone())
343 }
344
345 pub(super) async fn resolve_flavor_for_uri(&self, uri: &Url) -> crate::config::MarkdownFlavor {
348 match super::resolve_uri(uri) {
349 Some(path) => self.resolve_config_for_file(&path).await.get_flavor_for_file(&path),
350 None => self.rumdl_config.read().await.markdown_flavor(),
351 }
352 }
353}
354
355#[tower_lsp::async_trait]
356impl LanguageServer for RumdlLanguageServer {
357 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
358 log::info!("Initializing rumdl Language Server");
359
360 if let Some(options) = params.initialization_options
362 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
363 {
364 *self.config.write().await = config;
365 }
366
367 let supports_pull = params
370 .capabilities
371 .text_document
372 .as_ref()
373 .and_then(|td| td.diagnostic.as_ref())
374 .is_some();
375
376 if supports_pull {
377 log::info!("Client supports pull diagnostics - disabling push to avoid duplicates");
378 *self.client_supports_pull_diagnostics.write().await = true;
379 } else {
380 log::info!("Client does not support pull diagnostics - using push model");
381 }
382
383 let supports_hierarchical_symbols = params
386 .capabilities
387 .text_document
388 .as_ref()
389 .and_then(|td| td.document_symbol.as_ref())
390 .and_then(|ds| ds.hierarchical_document_symbol_support)
391 .unwrap_or(false);
392 *self.client_supports_hierarchical_symbols.write().await = supports_hierarchical_symbols;
393
394 let mut roots = Vec::new();
396 if let Some(workspace_folders) = params.workspace_folders {
397 for folder in workspace_folders {
398 if let Ok(path) = folder.uri.to_file_path() {
399 let path = super::resolve_workspace_root(&path);
400 log::info!("Workspace root: {}", path.display());
401 roots.push(path);
402 }
403 }
404 } else if let Some(root_uri) = params.root_uri
405 && let Ok(path) = root_uri.to_file_path()
406 {
407 let path = super::resolve_workspace_root(&path);
408 log::info!("Workspace root: {}", path.display());
409 roots.push(path);
410 }
411 *self.workspace_roots.write().await = roots;
412
413 self.load_configuration(false).await;
415
416 let (enable_link_navigation, enable_link_completions, enable_symbols) = {
417 let config = self.config.read().await;
418 (
419 config.enable_link_navigation,
420 config.enable_link_completions,
421 config.enable_symbols,
422 )
423 };
424
425 Ok(InitializeResult {
426 capabilities: ServerCapabilities {
427 text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
428 open_close: Some(true),
429 change: Some(TextDocumentSyncKind::FULL),
430 will_save: Some(false),
431 will_save_wait_until: Some(true),
432 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
433 include_text: Some(false),
434 })),
435 })),
436 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
437 code_action_kinds: Some(vec![
438 CodeActionKind::QUICKFIX,
439 CodeActionKind::SOURCE_FIX_ALL,
440 CodeActionKind::new("source.fixAll.rumdl"),
441 ]),
442 work_done_progress_options: WorkDoneProgressOptions::default(),
443 resolve_provider: None,
444 })),
445 document_formatting_provider: Some(OneOf::Left(true)),
446 document_range_formatting_provider: Some(OneOf::Left(true)),
447 document_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
448 workspace_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
449 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
450 identifier: Some("rumdl".to_string()),
451 inter_file_dependencies: true,
452 workspace_diagnostics: false,
453 work_done_progress_options: WorkDoneProgressOptions::default(),
454 })),
455 completion_provider: Some(CompletionOptions {
461 trigger_characters: Some(if enable_link_completions {
462 vec![
463 "`".to_string(),
464 "(".to_string(),
465 "#".to_string(),
466 "/".to_string(),
467 ".".to_string(),
468 "-".to_string(),
469 ]
470 } else {
471 vec!["`".to_string()]
472 }),
473 resolve_provider: Some(false),
474 work_done_progress_options: WorkDoneProgressOptions::default(),
475 all_commit_characters: None,
476 completion_item: None,
477 }),
478 definition_provider: enable_link_navigation.then_some(OneOf::Left(true)),
479 references_provider: enable_link_navigation.then_some(OneOf::Left(true)),
480 hover_provider: enable_link_navigation.then_some(HoverProviderCapability::Simple(true)),
481 rename_provider: enable_link_navigation.then_some(OneOf::Right(RenameOptions {
482 prepare_provider: Some(true),
483 work_done_progress_options: WorkDoneProgressOptions::default(),
484 })),
485 workspace: Some(WorkspaceServerCapabilities {
486 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
487 supported: Some(true),
488 change_notifications: Some(OneOf::Left(true)),
489 }),
490 file_operations: None,
491 }),
492 ..Default::default()
493 },
494 server_info: Some(ServerInfo {
495 name: "rumdl".to_string(),
496 version: Some(env!("CARGO_PKG_VERSION").to_string()),
497 }),
498 })
499 }
500
501 async fn initialized(&self, _: InitializedParams) {
502 let version = env!("CARGO_PKG_VERSION");
503
504 let (binary_path, build_time) = std::env::current_exe().ok().map_or_else(
506 || ("unknown".to_string(), "unknown".to_string()),
507 |path| {
508 let path_str = path.to_str().unwrap_or("unknown").to_string();
509 let build_time = std::fs::metadata(&path)
510 .ok()
511 .and_then(|metadata| metadata.modified().ok())
512 .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
513 .and_then(|duration| {
514 let secs = duration.as_secs();
515 chrono::DateTime::from_timestamp(secs as i64, 0)
516 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
517 })
518 .unwrap_or_else(|| "unknown".to_string());
519 (path_str, build_time)
520 },
521 );
522
523 let working_dir = std::env::current_dir()
524 .ok()
525 .and_then(|p| p.to_str().map(std::string::ToString::to_string))
526 .unwrap_or_else(|| "unknown".to_string());
527
528 log::info!("rumdl Language Server v{version} initialized (built: {build_time}, binary: {binary_path})");
529 log::info!("Working directory: {working_dir}");
530
531 self.client
532 .log_message(MessageType::INFO, format!("rumdl v{version} Language Server started"))
533 .await;
534
535 if !self.queue_index_update(IndexUpdate::FullRescan).await {
537 log::warn!("Failed to trigger initial workspace indexing");
538 } else {
539 log::info!("Triggered initial workspace indexing for cross-file analysis");
540 }
541
542 let markdown_patterns = [
544 "**/*.md",
545 "**/*.markdown",
546 "**/*.mdx",
547 "**/*.mkd",
548 "**/*.mkdn",
549 "**/*.mdown",
550 "**/*.mdwn",
551 "**/*.qmd",
552 "**/*.rmd",
553 ];
554 let config_patterns = [
558 "**/.rumdl.toml",
559 "**/rumdl.toml",
560 "**/pyproject.toml",
561 "**/.markdownlint.json",
562 "**/.markdownlint-cli2.yaml",
563 "**/.markdownlint-cli2.jsonc",
564 "**/.editorconfig",
565 ];
566 let watchers: Vec<_> = markdown_patterns
567 .iter()
568 .chain(config_patterns.iter())
569 .map(|pattern| FileSystemWatcher {
570 glob_pattern: GlobPattern::String((*pattern).to_string()),
571 kind: Some(WatchKind::all()),
572 })
573 .collect();
574
575 let registration = Registration {
576 id: "markdown-watcher".to_string(),
577 method: "workspace/didChangeWatchedFiles".to_string(),
578 register_options: Some(
579 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { watchers }).unwrap(),
580 ),
581 };
582
583 if self.client.register_capability(vec![registration]).await.is_err() {
584 log::debug!("Client does not support file watching capability");
585 }
586 }
587
588 async fn completion(&self, params: CompletionParams) -> JsonRpcResult<Option<CompletionResponse>> {
589 let uri = params.text_document_position.text_document.uri;
590 let position = params.text_document_position.position;
591
592 let Some(text) = self.get_document_content(&uri).await else {
594 return Ok(None);
595 };
596
597 if let Some((start_col, current_text)) = Self::detect_code_fence_language_position(&text, position) {
599 log::debug!(
600 "Code fence completion triggered at {}:{}, current text: '{}'",
601 position.line,
602 position.character,
603 current_text
604 );
605 let items = self
606 .get_language_completions(&uri, ¤t_text, start_col, position)
607 .await;
608 if !items.is_empty() {
609 return Ok(Some(CompletionResponse::Array(items)));
610 }
611 }
612
613 if self.config.read().await.enable_link_completions {
615 let trigger = params.context.as_ref().and_then(|c| c.trigger_character.as_deref());
619 let skip_link_check = matches!(trigger, Some("." | "-")) && {
620 let line_num = position.line as usize;
621 !text.lines().nth(line_num).is_some_and(|line| line.contains("]("))
624 };
625
626 if !skip_link_check && let Some(link_info) = Self::detect_link_target_position(&text, position) {
627 if let Some((partial_anchor, anchor_start_col)) = link_info.anchor {
628 log::debug!(
629 "Anchor completion triggered at {}:{}, file: '{}', partial: '{}'",
630 position.line,
631 position.character,
632 link_info.file_path,
633 partial_anchor
634 );
635 let items = self
636 .get_anchor_completions(&uri, &link_info.file_path, &partial_anchor, anchor_start_col, position)
637 .await;
638 if !items.is_empty() {
639 return Ok(Some(CompletionResponse::Array(items)));
640 }
641 } else {
642 log::debug!(
643 "File path completion triggered at {}:{}, partial: '{}'",
644 position.line,
645 position.character,
646 link_info.file_path
647 );
648 let list = self
649 .get_file_completions(&uri, &link_info.file_path, link_info.path_start_col, position)
650 .await;
651 if !list.items.is_empty() {
652 return Ok(Some(CompletionResponse::List(list)));
653 }
654 }
655 }
656 }
657
658 Ok(None)
659 }
660
661 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
662 let mut roots = self.workspace_roots.write().await;
664
665 for removed in ¶ms.event.removed {
669 if let Ok(path) = removed.uri.to_file_path() {
670 let path = super::resolve_workspace_root(&path);
671 roots.retain(|r| r != &path);
672 log::info!("Removed workspace root: {}", path.display());
673 }
674 }
675
676 for added in ¶ms.event.added {
678 if let Ok(path) = added.uri.to_file_path()
679 && let path = super::resolve_workspace_root(&path)
680 && !roots.contains(&path)
681 {
682 log::info!("Added workspace root: {}", path.display());
683 roots.push(path);
684 }
685 }
686 drop(roots);
687
688 self.config_cache.write().await.clear();
690
691 self.reload_configuration().await;
693
694 if !self.queue_index_update(IndexUpdate::FullRescan).await {
696 log::warn!("Failed to trigger workspace rescan after folder change");
697 }
698 }
699
700 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
701 log::debug!("Configuration changed: {:?}", params.settings);
702
703 let settings_value = params.settings;
707
708 let rumdl_settings = if let serde_json::Value::Object(ref obj) = settings_value {
710 obj.get("rumdl").cloned().unwrap_or(settings_value.clone())
711 } else {
712 settings_value
713 };
714
715 let has_content_roots_key = matches!(
719 &rumdl_settings,
720 serde_json::Value::Object(obj) if obj.contains_key("linkCompletionContentRoots")
721 );
722
723 let has_symbols_key = matches!(
728 &rumdl_settings,
729 serde_json::Value::Object(obj) if obj.contains_key("enableSymbols")
730 );
731
732 let mut config_applied = false;
734 let mut warnings: Vec<String> = Vec::new();
735
736 if let Ok(rule_settings) = serde_json::from_value::<LspRuleSettings>(rumdl_settings.clone())
740 && (rule_settings.disable.is_some()
741 || rule_settings.enable.is_some()
742 || rule_settings.line_length.is_some()
743 || (!rule_settings.rules.is_empty() && rule_settings.rules.keys().all(|k| is_valid_rule_name(k))))
744 {
745 if let Some(ref disable) = rule_settings.disable {
747 for rule in disable {
748 if !is_valid_rule_name(rule) {
749 warnings.push(format!("Unknown rule in disable list: {rule}"));
750 }
751 }
752 }
753 if let Some(ref enable) = rule_settings.enable {
754 for rule in enable {
755 if !is_valid_rule_name(rule) {
756 warnings.push(format!("Unknown rule in enable list: {rule}"));
757 }
758 }
759 }
760 for rule_name in rule_settings.rules.keys() {
762 if !is_valid_rule_name(rule_name) {
763 warnings.push(format!("Unknown rule in settings: {rule_name}"));
764 }
765 }
766
767 log::info!("Applied rule settings from configuration (Neovim style)");
768 let mut config = self.config.write().await;
769 config.settings = Some(rule_settings);
770 drop(config);
771 config_applied = true;
772 } else if let Ok(full_config) = serde_json::from_value::<RumdlLspConfig>(rumdl_settings.clone())
773 && (full_config.config_path.is_some()
774 || full_config.enable_rules.is_some()
775 || full_config.disable_rules.is_some()
776 || full_config.settings.is_some()
777 || !full_config.enable_linting
778 || full_config.enable_auto_fix
779 || !full_config.enable_link_completions
780 || !full_config.enable_link_navigation
781 || has_symbols_key
782 || has_content_roots_key)
783 {
784 if let Some(ref rules) = full_config.enable_rules {
786 for rule in rules {
787 if !is_valid_rule_name(rule) {
788 warnings.push(format!("Unknown rule in enableRules: {rule}"));
789 }
790 }
791 }
792 if let Some(ref rules) = full_config.disable_rules {
793 for rule in rules {
794 if !is_valid_rule_name(rule) {
795 warnings.push(format!("Unknown rule in disableRules: {rule}"));
796 }
797 }
798 }
799
800 {
808 let mut config = self.config.write().await;
809 if let Some(merged) = merge_lsp_config(&config, &rumdl_settings) {
810 *config = merged;
811 drop(config);
812 log::info!("Merged LSP configuration from client settings");
813 config_applied = true;
814 } else {
815 drop(config);
816 warnings.push("Could not merge LSP configuration update; keeping current settings".to_string());
817 }
818 }
819 } else if let serde_json::Value::Object(obj) = rumdl_settings {
820 let mut config = self.config.write().await;
823
824 let mut rules = std::collections::HashMap::new();
826 let mut disable = Vec::new();
827 let mut enable = Vec::new();
828 let mut line_length = None;
829
830 for (key, value) in obj {
831 match key.as_str() {
832 "disable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
833 Ok(d) => {
834 if d.len() > MAX_RULE_LIST_SIZE {
835 warnings.push(format!(
836 "Too many rules in 'disable' ({} > {}), truncating",
837 d.len(),
838 MAX_RULE_LIST_SIZE
839 ));
840 }
841 for rule in d.iter().take(MAX_RULE_LIST_SIZE) {
842 if !is_valid_rule_name(rule) {
843 warnings.push(format!("Unknown rule in disable: {rule}"));
844 }
845 }
846 disable = d.into_iter().take(MAX_RULE_LIST_SIZE).collect();
847 }
848 Err(_) => {
849 warnings.push(format!(
850 "Invalid 'disable' value: expected array of strings, got {value}"
851 ));
852 }
853 },
854 "enable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
855 Ok(e) => {
856 if e.len() > MAX_RULE_LIST_SIZE {
857 warnings.push(format!(
858 "Too many rules in 'enable' ({} > {}), truncating",
859 e.len(),
860 MAX_RULE_LIST_SIZE
861 ));
862 }
863 for rule in e.iter().take(MAX_RULE_LIST_SIZE) {
864 if !is_valid_rule_name(rule) {
865 warnings.push(format!("Unknown rule in enable: {rule}"));
866 }
867 }
868 enable = e.into_iter().take(MAX_RULE_LIST_SIZE).collect();
869 }
870 Err(_) => {
871 warnings.push(format!(
872 "Invalid 'enable' value: expected array of strings, got {value}"
873 ));
874 }
875 },
876 "lineLength" | "line_length" | "line-length" => {
877 if let Some(l) = value.as_u64() {
878 match usize::try_from(l) {
879 Ok(len) if len <= MAX_LINE_LENGTH => line_length = Some(len),
880 Ok(len) => warnings.push(format!(
881 "Invalid 'lineLength' value: {len} exceeds maximum ({MAX_LINE_LENGTH})"
882 )),
883 Err(_) => warnings.push(format!("Invalid 'lineLength' value: {l} is too large")),
884 }
885 } else {
886 warnings.push(format!("Invalid 'lineLength' value: expected number, got {value}"));
887 }
888 }
889 _ if key.starts_with("MD") || key.starts_with("md") => {
891 let normalized = key.to_uppercase();
892 if !is_valid_rule_name(&normalized) {
893 warnings.push(format!("Unknown rule: {key}"));
894 }
895 rules.insert(normalized, value);
896 }
897 _ => {
898 warnings.push(format!("Unknown configuration key: {key}"));
900 }
901 }
902 }
903
904 let settings = LspRuleSettings {
905 line_length,
906 disable: if disable.is_empty() { None } else { Some(disable) },
907 enable: if enable.is_empty() { None } else { Some(enable) },
908 rules,
909 };
910
911 log::info!("Applied Neovim-style rule settings (manual parse)");
912 config.settings = Some(settings);
913 drop(config);
914 config_applied = true;
915 } else {
916 log::warn!("Could not parse configuration settings: {rumdl_settings:?}");
917 }
918
919 for warning in &warnings {
921 log::warn!("{warning}");
922 }
923
924 if !warnings.is_empty() {
926 let message = if warnings.len() == 1 {
927 format!("rumdl: {}", warnings[0])
928 } else {
929 format!("rumdl configuration warnings:\n{}", warnings.join("\n"))
930 };
931 self.client.log_message(MessageType::WARNING, message).await;
932 }
933
934 if !config_applied {
935 log::debug!("No configuration changes applied");
936 }
937
938 self.config_cache.write().await.clear();
940
941 if config_applied {
949 self.load_configuration(false).await;
950
951 if !self.queue_index_update(IndexUpdate::FullRescan).await {
955 log::warn!("Failed to request workspace rescan after configuration change");
956 }
957 }
958
959 let doc_list: Vec<_> = {
964 let documents = self.documents.read().await;
965 documents
966 .iter()
967 .filter(|(_, entry)| !entry.from_disk)
968 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
969 .collect()
970 };
971
972 let tasks: Vec<_> = doc_list
976 .into_iter()
977 .map(|(uri, text)| {
978 let server = self.clone();
979 tokio::spawn(async move {
980 server.update_diagnostics(uri, text, true).await;
981 })
982 })
983 .collect();
984
985 for task in tasks {
987 let _ = task.await;
988 }
989 }
990
991 async fn shutdown(&self) -> JsonRpcResult<()> {
992 log::info!("Shutting down rumdl Language Server");
993
994 self.queue_index_update(IndexUpdate::Shutdown).await;
996
997 Ok(())
998 }
999
1000 async fn did_open(&self, params: DidOpenTextDocumentParams) {
1001 let uri = params.text_document.uri;
1002 let text = params.text_document.text;
1003 let version = params.text_document.version;
1004
1005 let entry = DocumentEntry {
1006 content: text.clone(),
1007 version: Some(version),
1008 from_disk: false,
1009 };
1010 self.documents.write().await.insert(uri.clone(), entry);
1011
1012 let resolved = super::resolve_uri_spelling(&uri);
1014 if resolved != uri {
1015 let mut aliases = self.document_aliases.write().await;
1016 let spellings = aliases.entry(resolved).or_default();
1017 if !spellings.contains(&uri) {
1018 spellings.push(uri.clone());
1019 }
1020 }
1021
1022 if let Some(path) = super::resolve_uri(&uri) {
1024 self.queue_index_update(IndexUpdate::FileChanged {
1025 path,
1026 content: text.clone(),
1027 })
1028 .await;
1029 }
1030
1031 self.update_diagnostics(uri, text, true).await;
1032 }
1033
1034 async fn did_change(&self, params: DidChangeTextDocumentParams) {
1035 let uri = params.text_document.uri;
1036 let version = params.text_document.version;
1037
1038 if let Some(change) = params.content_changes.into_iter().next() {
1039 let text = change.text;
1040
1041 let entry = DocumentEntry {
1042 content: text.clone(),
1043 version: Some(version),
1044 from_disk: false,
1045 };
1046 self.documents.write().await.insert(uri.clone(), entry);
1047
1048 if let Some(path) = super::resolve_uri(&uri) {
1050 self.queue_index_update(IndexUpdate::FileChanged {
1051 path,
1052 content: text.clone(),
1053 })
1054 .await;
1055 }
1056
1057 self.update_diagnostics(uri, text, false).await;
1058 }
1059 }
1060
1061 async fn will_save_wait_until(&self, params: WillSaveTextDocumentParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1062 if params.reason != TextDocumentSaveReason::MANUAL {
1065 return Ok(None);
1066 }
1067
1068 let config_guard = self.config.read().await;
1069 let enable_auto_fix = config_guard.enable_auto_fix;
1070 drop(config_guard);
1071
1072 if !enable_auto_fix {
1073 return Ok(None);
1074 }
1075
1076 let Some(text) = self.get_document_content(¶ms.text_document.uri).await else {
1078 return Ok(None);
1079 };
1080
1081 match self.apply_all_fixes(¶ms.text_document.uri, &text).await {
1083 Ok(Some(fixed_text)) => {
1084 Ok(Some(vec![TextEdit {
1086 range: Range {
1087 start: Position { line: 0, character: 0 },
1088 end: self.get_end_position(&text),
1089 },
1090 new_text: fixed_text,
1091 }]))
1092 }
1093 Ok(None) => Ok(None),
1094 Err(e) => {
1095 log::error!("Failed to generate fixes in will_save_wait_until: {e}");
1096 Ok(None)
1097 }
1098 }
1099 }
1100
1101 async fn did_save(&self, params: DidSaveTextDocumentParams) {
1102 if let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
1105 self.update_diagnostics(params.text_document.uri, entry.content.clone(), true)
1106 .await;
1107 }
1108 }
1109
1110 async fn did_close(&self, params: DidCloseTextDocumentParams) {
1111 self.documents.write().await.remove(¶ms.text_document.uri);
1113 let resolved = super::resolve_uri_spelling(¶ms.text_document.uri);
1116 if resolved != params.text_document.uri {
1117 let mut aliases = self.document_aliases.write().await;
1118 if let Some(spellings) = aliases.get_mut(&resolved) {
1119 spellings.retain(|u| u != ¶ms.text_document.uri);
1120 if spellings.is_empty() {
1121 aliases.remove(&resolved);
1122 }
1123 }
1124 }
1125
1126 self.client
1129 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
1130 .await;
1131 }
1132
1133 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
1134 const CONFIG_FILES: &[&str] = &[
1136 ".rumdl.toml",
1137 "rumdl.toml",
1138 "pyproject.toml",
1139 ".markdownlint.json",
1140 ".markdownlint-cli2.jsonc",
1141 ".markdownlint-cli2.yaml",
1142 ".markdownlint-cli2.yml",
1143 ];
1144
1145 let mut config_changed = false;
1146 let reads_editorconfig = self.reads_editorconfig().await;
1149
1150 for change in ¶ms.changes {
1151 if let Some(path) = super::resolve_uri(&change.uri) {
1155 let file_name = path.file_name().and_then(|f| f.to_str());
1156
1157 if let Some(name) = file_name
1159 && (CONFIG_FILES.contains(&name) || (reads_editorconfig && name == ".editorconfig"))
1160 && !config_changed
1161 {
1162 log::info!("Config file changed: {}, invalidating config cache", path.display());
1163
1164 let mut cache = self.config_cache.write().await;
1168 cache.clear();
1169
1170 drop(cache);
1172 self.reload_configuration().await;
1173 config_changed = true;
1174 }
1175
1176 if let Some(ext) = path.extension()
1178 && is_markdown_extension(ext)
1179 {
1180 match change.typ {
1181 FileChangeType::CREATED | FileChangeType::CHANGED => {
1182 if let Some(content) = self
1192 .get_open_document_content(&super::resolve_uri_spelling(&change.uri))
1193 .await
1194 {
1195 self.queue_index_update(IndexUpdate::FileChanged {
1196 path: path.clone(),
1197 content,
1198 })
1199 .await;
1200 continue;
1201 }
1202 let roots = self.workspace_roots.read().await.clone();
1206 let (options, includes, excludes) = {
1207 let config = self.rumdl_config.read().await;
1208 (
1209 crate::lsp::index_worker::index_walk_options(&config),
1210 config.global.include.clone(),
1211 ExcludeMatchers::new(&config.global.exclude),
1212 )
1213 };
1214 if crate::lsp::index_worker::path_is_ignored_for_index(
1215 &roots, &path, &options, &includes, &excludes,
1216 ) {
1217 self.queue_index_update(IndexUpdate::FileRemoved { path: path.clone() })
1222 .await;
1223 continue;
1224 }
1225 if let Ok(content) = tokio::fs::read_to_string(&path).await {
1227 self.queue_index_update(IndexUpdate::FileChanged {
1228 path: path.clone(),
1229 content,
1230 })
1231 .await;
1232 }
1233 }
1234 FileChangeType::DELETED => {
1235 self.queue_index_update(IndexUpdate::FileRemoved { path: path.clone() })
1236 .await;
1237 }
1238 _ => {}
1239 }
1240 }
1241 }
1242 }
1243
1244 if config_changed {
1246 if !self.queue_index_update(IndexUpdate::FullRescan).await {
1250 log::warn!("Failed to request workspace rescan after config change");
1251 }
1252
1253 let docs_to_update: Vec<(Url, String)> = {
1254 let docs = self.documents.read().await;
1255 docs.iter()
1256 .filter(|(_, entry)| !entry.from_disk)
1257 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
1258 .collect()
1259 };
1260
1261 for (uri, text) in docs_to_update {
1262 self.update_diagnostics(uri, text, true).await;
1263 }
1264 }
1265 }
1266
1267 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
1268 let uri = params.text_document.uri;
1269 let range = params.range;
1270 let requested_kinds = params.context.only;
1271
1272 if let Some(text) = self.get_document_content(&uri).await {
1273 match self.get_code_actions(&uri, &text, range).await {
1274 Ok(actions) => {
1275 let filtered_actions = if let Some(ref kinds) = requested_kinds
1279 && !kinds.is_empty()
1280 {
1281 actions
1282 .into_iter()
1283 .filter(|action| {
1284 action.kind.as_ref().is_some_and(|action_kind| {
1285 let action_kind_str = action_kind.as_str();
1286 kinds.iter().any(|requested| {
1287 let requested_str = requested.as_str();
1288 action_kind_str.starts_with(requested_str)
1291 })
1292 })
1293 })
1294 .collect()
1295 } else {
1296 actions
1297 };
1298
1299 let response: Vec<CodeActionOrCommand> = filtered_actions
1300 .into_iter()
1301 .map(CodeActionOrCommand::CodeAction)
1302 .collect();
1303 Ok(Some(response))
1304 }
1305 Err(e) => {
1306 log::error!("Failed to get code actions: {e}");
1307 Ok(None)
1308 }
1309 }
1310 } else {
1311 Ok(None)
1312 }
1313 }
1314
1315 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1316 log::debug!(
1321 "Range formatting requested for {:?}, formatting entire document due to rule interdependencies",
1322 params.range
1323 );
1324
1325 let formatting_params = DocumentFormattingParams {
1326 text_document: params.text_document,
1327 options: params.options,
1328 work_done_progress_params: params.work_done_progress_params,
1329 };
1330
1331 self.formatting(formatting_params).await
1332 }
1333
1334 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1335 let uri = params.text_document.uri;
1336 let options = params.options;
1337
1338 log::debug!("Formatting request for: {uri}");
1339 log::debug!(
1340 "FormattingOptions: insert_final_newline={:?}, trim_final_newlines={:?}, trim_trailing_whitespace={:?}",
1341 options.insert_final_newline,
1342 options.trim_final_newlines,
1343 options.trim_trailing_whitespace
1344 );
1345
1346 if let Some(text) = self.get_document_content(&uri).await {
1347 let mut result = match self.apply_all_fixes(&uri, &text).await {
1356 Ok(Some(fixed)) => fixed,
1357 Ok(None) => text.clone(),
1358 Err(e) => {
1359 log::error!("Failed to apply fixes during formatting: {e}");
1360 text.clone()
1361 }
1362 };
1363
1364 result = Self::apply_formatting_options(result, &options);
1367
1368 if result != text {
1370 log::debug!("Returning formatting edits");
1371 let end_position = self.get_end_position(&text);
1372 let edit = TextEdit {
1373 range: Range {
1374 start: Position { line: 0, character: 0 },
1375 end: end_position,
1376 },
1377 new_text: result,
1378 };
1379 return Ok(Some(vec![edit]));
1380 }
1381
1382 Ok(Some(Vec::new()))
1383 } else {
1384 log::warn!("Document not found: {uri}");
1385 Ok(None)
1386 }
1387 }
1388
1389 async fn goto_definition(&self, params: GotoDefinitionParams) -> JsonRpcResult<Option<GotoDefinitionResponse>> {
1390 if !self.config.read().await.enable_link_navigation {
1391 return Ok(None);
1392 }
1393 let uri = params.text_document_position_params.text_document.uri;
1394 let position = params.text_document_position_params.position;
1395
1396 log::debug!("Go-to-definition at {uri} {}:{}", position.line, position.character);
1397
1398 Ok(self.handle_goto_definition(&uri, position).await)
1399 }
1400
1401 async fn references(&self, params: ReferenceParams) -> JsonRpcResult<Option<Vec<Location>>> {
1402 if !self.config.read().await.enable_link_navigation {
1403 return Ok(None);
1404 }
1405 let uri = params.text_document_position.text_document.uri;
1406 let position = params.text_document_position.position;
1407
1408 log::debug!("Find references at {uri} {}:{}", position.line, position.character);
1409
1410 Ok(self.handle_references(&uri, position).await)
1411 }
1412
1413 async fn hover(&self, params: HoverParams) -> JsonRpcResult<Option<Hover>> {
1414 if !self.config.read().await.enable_link_navigation {
1415 return Ok(None);
1416 }
1417 let uri = params.text_document_position_params.text_document.uri;
1418 let position = params.text_document_position_params.position;
1419
1420 log::debug!("Hover at {uri} {}:{}", position.line, position.character);
1421
1422 Ok(self.handle_hover(&uri, position).await)
1423 }
1424
1425 async fn prepare_rename(&self, params: TextDocumentPositionParams) -> JsonRpcResult<Option<PrepareRenameResponse>> {
1426 if !self.config.read().await.enable_link_navigation {
1427 return Ok(None);
1428 }
1429 let uri = params.text_document.uri;
1430 let position = params.position;
1431
1432 log::debug!("Prepare rename at {uri} {}:{}", position.line, position.character);
1433
1434 Ok(self.handle_prepare_rename(&uri, position).await)
1435 }
1436
1437 async fn rename(&self, params: RenameParams) -> JsonRpcResult<Option<WorkspaceEdit>> {
1438 if !self.config.read().await.enable_link_navigation {
1439 return Ok(None);
1440 }
1441 let uri = params.text_document_position.text_document.uri;
1442 let position = params.text_document_position.position;
1443 let new_name = params.new_name;
1444
1445 log::debug!("Rename at {uri} {}:{} → {new_name}", position.line, position.character);
1446
1447 Ok(self.handle_rename(&uri, position, &new_name).await)
1448 }
1449
1450 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
1451 let uri = params.text_document.uri;
1452
1453 if let Some(text) = self.get_open_document_content(&uri).await {
1454 match self.lint_document(&uri, &text, true).await {
1455 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1456 RelatedFullDocumentDiagnosticReport {
1457 related_documents: None,
1458 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1459 result_id: None,
1460 items: diagnostics,
1461 },
1462 },
1463 ))),
1464 Err(e) => {
1465 log::error!("Failed to get diagnostics: {e}");
1466 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1467 RelatedFullDocumentDiagnosticReport {
1468 related_documents: None,
1469 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1470 result_id: None,
1471 items: Vec::new(),
1472 },
1473 },
1474 )))
1475 }
1476 }
1477 } else {
1478 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1479 RelatedFullDocumentDiagnosticReport {
1480 related_documents: None,
1481 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1482 result_id: None,
1483 items: Vec::new(),
1484 },
1485 },
1486 )))
1487 }
1488 }
1489
1490 async fn document_symbol(&self, params: DocumentSymbolParams) -> JsonRpcResult<Option<DocumentSymbolResponse>> {
1491 if !self.config.read().await.enable_symbols {
1492 return Ok(None);
1493 }
1494
1495 let uri = params.text_document.uri;
1496 let Some(text) = self.get_document_content(&uri).await else {
1497 return Ok(None);
1498 };
1499
1500 let flavor = self.resolve_flavor_for_uri(&uri).await;
1501 let ctx = crate::lint_context::LintContext::new(&text, flavor, None);
1502
1503 if *self.client_supports_hierarchical_symbols.read().await {
1504 let symbols = super::symbols::document_symbols(&ctx);
1505 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Nested(symbols)))
1506 } else {
1507 let symbols = super::symbols::document_symbols_flat(&ctx, &uri);
1508 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Flat(symbols)))
1509 }
1510 }
1511
1512 async fn symbol(&self, params: WorkspaceSymbolParams) -> JsonRpcResult<Option<Vec<SymbolInformation>>> {
1513 if !self.config.read().await.enable_symbols {
1514 return Ok(None);
1515 }
1516
1517 let query = params.query.to_lowercase();
1518 let index = self.workspace_index.read().await;
1519 let symbols = super::symbols::workspace_symbols(&index, &query);
1520 Ok(if symbols.is_empty() { None } else { Some(symbols) })
1521 }
1522}
1523
1524#[cfg(test)]
1525#[path = "tests.rs"]
1526mod tests;