1use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use futures::future::join_all;
11use tokio::sync::{RwLock, mpsc};
12use tower_lsp::jsonrpc::Result as JsonRpcResult;
13use tower_lsp::lsp_types::*;
14use tower_lsp::{Client, LanguageServer};
15
16use crate::config::{Config, is_valid_rule_name};
17use crate::discovery::{ExcludeMatchers, is_markdown_extension};
18use crate::lsp::index_worker::IndexWorker;
19use crate::lsp::types::{IndexState, IndexUpdate, LspRuleSettings, RumdlLspConfig};
20use crate::workspace_index::WorkspaceIndex;
21
22const MAX_RULE_LIST_SIZE: usize = 100;
24
25const MAX_LINE_LENGTH: usize = 10_000;
27
28fn merge_lsp_config(current: &RumdlLspConfig, incoming: &serde_json::Value) -> Option<RumdlLspConfig> {
40 let serde_json::Value::Object(incoming) = incoming else {
41 return None;
42 };
43 let serde_json::Value::Object(mut base) = serde_json::to_value(current).ok()? else {
44 return None;
45 };
46 for (key, value) in incoming {
47 base.insert(key.clone(), value.clone());
48 }
49 serde_json::from_value(serde_json::Value::Object(base)).ok()
50}
51
52#[derive(Clone, Debug, PartialEq)]
54pub(crate) struct DocumentEntry {
55 pub(crate) content: String,
57 pub(crate) version: Option<i32>,
59 pub(crate) from_disk: bool,
61}
62
63#[derive(Clone, Debug)]
65pub(crate) struct ConfigCacheEntry {
66 pub(crate) config: Config,
68 pub(crate) config_file: Option<PathBuf>,
70 pub(crate) from_global_fallback: bool,
72}
73
74#[derive(Clone)]
84pub struct RumdlLanguageServer {
85 pub(crate) client: Client,
86 pub(crate) config: Arc<RwLock<RumdlLspConfig>>,
88 pub(crate) rumdl_config: Arc<RwLock<Config>>,
90 pub(crate) documents: Arc<RwLock<HashMap<Url, DocumentEntry>>>,
92 pub(crate) workspace_roots: Arc<RwLock<Vec<PathBuf>>>,
94 pub(crate) config_cache: Arc<RwLock<HashMap<PathBuf, ConfigCacheEntry>>>,
97 pub(crate) workspace_index: Arc<RwLock<WorkspaceIndex>>,
99 pub(crate) index_state: Arc<RwLock<IndexState>>,
101 pub(crate) update_tx: mpsc::Sender<IndexUpdate>,
103 pub(crate) client_supports_pull_diagnostics: Arc<RwLock<bool>>,
106 pub(crate) client_supports_hierarchical_symbols: Arc<RwLock<bool>>,
110 pub(crate) cli_config_path: Option<String>,
118}
119
120impl RumdlLanguageServer {
121 pub fn new(client: Client, cli_config_path: Option<&str>) -> Self {
122 let initial_config = RumdlLspConfig::default();
123 let cli_config_path = cli_config_path.map(str::to_string);
124
125 let workspace_index = Arc::new(RwLock::new(WorkspaceIndex::new()));
127 let index_state = Arc::new(RwLock::new(IndexState::default()));
128 let workspace_roots = Arc::new(RwLock::new(Vec::new()));
129 let rumdl_config = Arc::new(RwLock::new(Config::default()));
130
131 let (update_tx, update_rx) = mpsc::channel::<IndexUpdate>(100);
133 let (relint_tx, _relint_rx) = mpsc::channel::<PathBuf>(100);
134
135 let worker = IndexWorker::new(
137 update_rx,
138 workspace_index.clone(),
139 index_state.clone(),
140 client.clone(),
141 workspace_roots.clone(),
142 relint_tx,
143 rumdl_config.clone(),
144 );
145 tokio::spawn(worker.run());
146
147 Self {
148 client,
149 config: Arc::new(RwLock::new(initial_config)),
150 rumdl_config,
151 documents: Arc::new(RwLock::new(HashMap::new())),
152 workspace_roots,
153 config_cache: Arc::new(RwLock::new(HashMap::new())),
154 workspace_index,
155 index_state,
156 update_tx,
157 client_supports_pull_diagnostics: Arc::new(RwLock::new(false)),
158 client_supports_hierarchical_symbols: Arc::new(RwLock::new(false)),
159 cli_config_path,
160 }
161 }
162
163 pub(super) async fn get_document_content(&self, uri: &Url) -> Option<String> {
169 {
171 let docs = self.documents.read().await;
172 if let Some(entry) = docs.get(uri) {
173 return Some(entry.content.clone());
174 }
175 }
176
177 if let Ok(path) = uri.to_file_path() {
179 if let Ok(content) = tokio::fs::read_to_string(&path).await {
180 let entry = DocumentEntry {
182 content: content.clone(),
183 version: None,
184 from_disk: true,
185 };
186
187 let mut docs = self.documents.write().await;
188 docs.insert(uri.clone(), entry);
189
190 log::debug!("Loaded document from disk and cached: {uri}");
191 return Some(content);
192 } else {
193 log::debug!("Failed to read file from disk: {uri}");
194 }
195 }
196
197 None
198 }
199
200 async fn get_open_document_content(&self, uri: &Url) -> Option<String> {
206 let docs = self.documents.read().await;
207 docs.get(uri)
208 .and_then(|entry| (!entry.from_disk).then(|| entry.content.clone()))
209 }
210
211 pub(super) async fn resolve_flavor_for_uri(&self, uri: &Url) -> crate::config::MarkdownFlavor {
214 match uri.to_file_path() {
215 Ok(path) => self.resolve_config_for_file(&path).await.get_flavor_for_file(&path),
216 Err(_) => self.rumdl_config.read().await.markdown_flavor(),
217 }
218 }
219}
220
221#[tower_lsp::async_trait]
222impl LanguageServer for RumdlLanguageServer {
223 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
224 log::info!("Initializing rumdl Language Server");
225
226 if let Some(options) = params.initialization_options
228 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
229 {
230 *self.config.write().await = config;
231 }
232
233 let supports_pull = params
236 .capabilities
237 .text_document
238 .as_ref()
239 .and_then(|td| td.diagnostic.as_ref())
240 .is_some();
241
242 if supports_pull {
243 log::info!("Client supports pull diagnostics - disabling push to avoid duplicates");
244 *self.client_supports_pull_diagnostics.write().await = true;
245 } else {
246 log::info!("Client does not support pull diagnostics - using push model");
247 }
248
249 let supports_hierarchical_symbols = params
252 .capabilities
253 .text_document
254 .as_ref()
255 .and_then(|td| td.document_symbol.as_ref())
256 .and_then(|ds| ds.hierarchical_document_symbol_support)
257 .unwrap_or(false);
258 *self.client_supports_hierarchical_symbols.write().await = supports_hierarchical_symbols;
259
260 let mut roots = Vec::new();
262 if let Some(workspace_folders) = params.workspace_folders {
263 for folder in workspace_folders {
264 if let Ok(path) = folder.uri.to_file_path() {
265 let path = path.canonicalize().unwrap_or(path);
266 log::info!("Workspace root: {}", path.display());
267 roots.push(path);
268 }
269 }
270 } else if let Some(root_uri) = params.root_uri
271 && let Ok(path) = root_uri.to_file_path()
272 {
273 let path = path.canonicalize().unwrap_or(path);
274 log::info!("Workspace root: {}", path.display());
275 roots.push(path);
276 }
277 *self.workspace_roots.write().await = roots;
278
279 self.load_configuration(false).await;
281
282 let (enable_link_navigation, enable_link_completions, enable_symbols) = {
283 let config = self.config.read().await;
284 (
285 config.enable_link_navigation,
286 config.enable_link_completions,
287 config.enable_symbols,
288 )
289 };
290
291 Ok(InitializeResult {
292 capabilities: ServerCapabilities {
293 text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
294 open_close: Some(true),
295 change: Some(TextDocumentSyncKind::FULL),
296 will_save: Some(false),
297 will_save_wait_until: Some(true),
298 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
299 include_text: Some(false),
300 })),
301 })),
302 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
303 code_action_kinds: Some(vec![
304 CodeActionKind::QUICKFIX,
305 CodeActionKind::SOURCE_FIX_ALL,
306 CodeActionKind::new("source.fixAll.rumdl"),
307 ]),
308 work_done_progress_options: WorkDoneProgressOptions::default(),
309 resolve_provider: None,
310 })),
311 document_formatting_provider: Some(OneOf::Left(true)),
312 document_range_formatting_provider: Some(OneOf::Left(true)),
313 document_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
314 workspace_symbol_provider: enable_symbols.then_some(OneOf::Left(true)),
315 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
316 identifier: Some("rumdl".to_string()),
317 inter_file_dependencies: true,
318 workspace_diagnostics: false,
319 work_done_progress_options: WorkDoneProgressOptions::default(),
320 })),
321 completion_provider: Some(CompletionOptions {
327 trigger_characters: Some(if enable_link_completions {
328 vec![
329 "`".to_string(),
330 "(".to_string(),
331 "#".to_string(),
332 "/".to_string(),
333 ".".to_string(),
334 "-".to_string(),
335 ]
336 } else {
337 vec!["`".to_string()]
338 }),
339 resolve_provider: Some(false),
340 work_done_progress_options: WorkDoneProgressOptions::default(),
341 all_commit_characters: None,
342 completion_item: None,
343 }),
344 definition_provider: enable_link_navigation.then_some(OneOf::Left(true)),
345 references_provider: enable_link_navigation.then_some(OneOf::Left(true)),
346 hover_provider: enable_link_navigation.then_some(HoverProviderCapability::Simple(true)),
347 rename_provider: enable_link_navigation.then_some(OneOf::Right(RenameOptions {
348 prepare_provider: Some(true),
349 work_done_progress_options: WorkDoneProgressOptions::default(),
350 })),
351 workspace: Some(WorkspaceServerCapabilities {
352 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
353 supported: Some(true),
354 change_notifications: Some(OneOf::Left(true)),
355 }),
356 file_operations: None,
357 }),
358 ..Default::default()
359 },
360 server_info: Some(ServerInfo {
361 name: "rumdl".to_string(),
362 version: Some(env!("CARGO_PKG_VERSION").to_string()),
363 }),
364 })
365 }
366
367 async fn initialized(&self, _: InitializedParams) {
368 let version = env!("CARGO_PKG_VERSION");
369
370 let (binary_path, build_time) = std::env::current_exe().ok().map_or_else(
372 || ("unknown".to_string(), "unknown".to_string()),
373 |path| {
374 let path_str = path.to_str().unwrap_or("unknown").to_string();
375 let build_time = std::fs::metadata(&path)
376 .ok()
377 .and_then(|metadata| metadata.modified().ok())
378 .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
379 .and_then(|duration| {
380 let secs = duration.as_secs();
381 chrono::DateTime::from_timestamp(secs as i64, 0)
382 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
383 })
384 .unwrap_or_else(|| "unknown".to_string());
385 (path_str, build_time)
386 },
387 );
388
389 let working_dir = std::env::current_dir()
390 .ok()
391 .and_then(|p| p.to_str().map(std::string::ToString::to_string))
392 .unwrap_or_else(|| "unknown".to_string());
393
394 log::info!("rumdl Language Server v{version} initialized (built: {build_time}, binary: {binary_path})");
395 log::info!("Working directory: {working_dir}");
396
397 self.client
398 .log_message(MessageType::INFO, format!("rumdl v{version} Language Server started"))
399 .await;
400
401 if self.update_tx.send(IndexUpdate::FullRescan).await.is_err() {
403 log::warn!("Failed to trigger initial workspace indexing");
404 } else {
405 log::info!("Triggered initial workspace indexing for cross-file analysis");
406 }
407
408 let markdown_patterns = [
410 "**/*.md",
411 "**/*.markdown",
412 "**/*.mdx",
413 "**/*.mkd",
414 "**/*.mkdn",
415 "**/*.mdown",
416 "**/*.mdwn",
417 "**/*.qmd",
418 "**/*.rmd",
419 ];
420 let config_patterns = [
421 "**/.rumdl.toml",
422 "**/rumdl.toml",
423 "**/pyproject.toml",
424 "**/.markdownlint.json",
425 "**/.markdownlint-cli2.yaml",
426 "**/.markdownlint-cli2.jsonc",
427 ];
428 let watchers: Vec<_> = markdown_patterns
429 .iter()
430 .chain(config_patterns.iter())
431 .map(|pattern| FileSystemWatcher {
432 glob_pattern: GlobPattern::String((*pattern).to_string()),
433 kind: Some(WatchKind::all()),
434 })
435 .collect();
436
437 let registration = Registration {
438 id: "markdown-watcher".to_string(),
439 method: "workspace/didChangeWatchedFiles".to_string(),
440 register_options: Some(
441 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { watchers }).unwrap(),
442 ),
443 };
444
445 if self.client.register_capability(vec![registration]).await.is_err() {
446 log::debug!("Client does not support file watching capability");
447 }
448 }
449
450 async fn completion(&self, params: CompletionParams) -> JsonRpcResult<Option<CompletionResponse>> {
451 let uri = params.text_document_position.text_document.uri;
452 let position = params.text_document_position.position;
453
454 let Some(text) = self.get_document_content(&uri).await else {
456 return Ok(None);
457 };
458
459 if let Some((start_col, current_text)) = Self::detect_code_fence_language_position(&text, position) {
461 log::debug!(
462 "Code fence completion triggered at {}:{}, current text: '{}'",
463 position.line,
464 position.character,
465 current_text
466 );
467 let items = self
468 .get_language_completions(&uri, ¤t_text, start_col, position)
469 .await;
470 if !items.is_empty() {
471 return Ok(Some(CompletionResponse::Array(items)));
472 }
473 }
474
475 if self.config.read().await.enable_link_completions {
477 let trigger = params.context.as_ref().and_then(|c| c.trigger_character.as_deref());
481 let skip_link_check = matches!(trigger, Some("." | "-")) && {
482 let line_num = position.line as usize;
483 !text.lines().nth(line_num).is_some_and(|line| line.contains("]("))
486 };
487
488 if !skip_link_check && let Some(link_info) = Self::detect_link_target_position(&text, position) {
489 if let Some((partial_anchor, anchor_start_col)) = link_info.anchor {
490 log::debug!(
491 "Anchor completion triggered at {}:{}, file: '{}', partial: '{}'",
492 position.line,
493 position.character,
494 link_info.file_path,
495 partial_anchor
496 );
497 let items = self
498 .get_anchor_completions(&uri, &link_info.file_path, &partial_anchor, anchor_start_col, position)
499 .await;
500 if !items.is_empty() {
501 return Ok(Some(CompletionResponse::Array(items)));
502 }
503 } else {
504 log::debug!(
505 "File path completion triggered at {}:{}, partial: '{}'",
506 position.line,
507 position.character,
508 link_info.file_path
509 );
510 let list = self
511 .get_file_completions(&uri, &link_info.file_path, link_info.path_start_col, position)
512 .await;
513 if !list.items.is_empty() {
514 return Ok(Some(CompletionResponse::List(list)));
515 }
516 }
517 }
518 }
519
520 Ok(None)
521 }
522
523 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
524 let mut roots = self.workspace_roots.write().await;
526
527 for removed in ¶ms.event.removed {
529 if let Ok(path) = removed.uri.to_file_path() {
530 roots.retain(|r| r != &path);
531 log::info!("Removed workspace root: {}", path.display());
532 }
533 }
534
535 for added in ¶ms.event.added {
537 if let Ok(path) = added.uri.to_file_path()
538 && !roots.contains(&path)
539 {
540 log::info!("Added workspace root: {}", path.display());
541 roots.push(path);
542 }
543 }
544 drop(roots);
545
546 self.config_cache.write().await.clear();
548
549 self.reload_configuration().await;
551
552 if self.update_tx.send(IndexUpdate::FullRescan).await.is_err() {
554 log::warn!("Failed to trigger workspace rescan after folder change");
555 }
556 }
557
558 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
559 log::debug!("Configuration changed: {:?}", params.settings);
560
561 let settings_value = params.settings;
565
566 let rumdl_settings = if let serde_json::Value::Object(ref obj) = settings_value {
568 obj.get("rumdl").cloned().unwrap_or(settings_value.clone())
569 } else {
570 settings_value
571 };
572
573 let has_content_roots_key = matches!(
577 &rumdl_settings,
578 serde_json::Value::Object(obj) if obj.contains_key("linkCompletionContentRoots")
579 );
580
581 let has_symbols_key = matches!(
586 &rumdl_settings,
587 serde_json::Value::Object(obj) if obj.contains_key("enableSymbols")
588 );
589
590 let mut config_applied = false;
592 let mut warnings: Vec<String> = Vec::new();
593
594 if let Ok(rule_settings) = serde_json::from_value::<LspRuleSettings>(rumdl_settings.clone())
598 && (rule_settings.disable.is_some()
599 || rule_settings.enable.is_some()
600 || rule_settings.line_length.is_some()
601 || (!rule_settings.rules.is_empty() && rule_settings.rules.keys().all(|k| is_valid_rule_name(k))))
602 {
603 if let Some(ref disable) = rule_settings.disable {
605 for rule in disable {
606 if !is_valid_rule_name(rule) {
607 warnings.push(format!("Unknown rule in disable list: {rule}"));
608 }
609 }
610 }
611 if let Some(ref enable) = rule_settings.enable {
612 for rule in enable {
613 if !is_valid_rule_name(rule) {
614 warnings.push(format!("Unknown rule in enable list: {rule}"));
615 }
616 }
617 }
618 for rule_name in rule_settings.rules.keys() {
620 if !is_valid_rule_name(rule_name) {
621 warnings.push(format!("Unknown rule in settings: {rule_name}"));
622 }
623 }
624
625 log::info!("Applied rule settings from configuration (Neovim style)");
626 let mut config = self.config.write().await;
627 config.settings = Some(rule_settings);
628 drop(config);
629 config_applied = true;
630 } else if let Ok(full_config) = serde_json::from_value::<RumdlLspConfig>(rumdl_settings.clone())
631 && (full_config.config_path.is_some()
632 || full_config.enable_rules.is_some()
633 || full_config.disable_rules.is_some()
634 || full_config.settings.is_some()
635 || !full_config.enable_linting
636 || full_config.enable_auto_fix
637 || !full_config.enable_link_completions
638 || !full_config.enable_link_navigation
639 || has_symbols_key
640 || has_content_roots_key)
641 {
642 if let Some(ref rules) = full_config.enable_rules {
644 for rule in rules {
645 if !is_valid_rule_name(rule) {
646 warnings.push(format!("Unknown rule in enableRules: {rule}"));
647 }
648 }
649 }
650 if let Some(ref rules) = full_config.disable_rules {
651 for rule in rules {
652 if !is_valid_rule_name(rule) {
653 warnings.push(format!("Unknown rule in disableRules: {rule}"));
654 }
655 }
656 }
657
658 {
666 let mut config = self.config.write().await;
667 if let Some(merged) = merge_lsp_config(&config, &rumdl_settings) {
668 *config = merged;
669 drop(config);
670 log::info!("Merged LSP configuration from client settings");
671 config_applied = true;
672 } else {
673 drop(config);
674 warnings.push("Could not merge LSP configuration update; keeping current settings".to_string());
675 }
676 }
677 } else if let serde_json::Value::Object(obj) = rumdl_settings {
678 let mut config = self.config.write().await;
681
682 let mut rules = std::collections::HashMap::new();
684 let mut disable = Vec::new();
685 let mut enable = Vec::new();
686 let mut line_length = None;
687
688 for (key, value) in obj {
689 match key.as_str() {
690 "disable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
691 Ok(d) => {
692 if d.len() > MAX_RULE_LIST_SIZE {
693 warnings.push(format!(
694 "Too many rules in 'disable' ({} > {}), truncating",
695 d.len(),
696 MAX_RULE_LIST_SIZE
697 ));
698 }
699 for rule in d.iter().take(MAX_RULE_LIST_SIZE) {
700 if !is_valid_rule_name(rule) {
701 warnings.push(format!("Unknown rule in disable: {rule}"));
702 }
703 }
704 disable = d.into_iter().take(MAX_RULE_LIST_SIZE).collect();
705 }
706 Err(_) => {
707 warnings.push(format!(
708 "Invalid 'disable' value: expected array of strings, got {value}"
709 ));
710 }
711 },
712 "enable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
713 Ok(e) => {
714 if e.len() > MAX_RULE_LIST_SIZE {
715 warnings.push(format!(
716 "Too many rules in 'enable' ({} > {}), truncating",
717 e.len(),
718 MAX_RULE_LIST_SIZE
719 ));
720 }
721 for rule in e.iter().take(MAX_RULE_LIST_SIZE) {
722 if !is_valid_rule_name(rule) {
723 warnings.push(format!("Unknown rule in enable: {rule}"));
724 }
725 }
726 enable = e.into_iter().take(MAX_RULE_LIST_SIZE).collect();
727 }
728 Err(_) => {
729 warnings.push(format!(
730 "Invalid 'enable' value: expected array of strings, got {value}"
731 ));
732 }
733 },
734 "lineLength" | "line_length" | "line-length" => {
735 if let Some(l) = value.as_u64() {
736 match usize::try_from(l) {
737 Ok(len) if len <= MAX_LINE_LENGTH => line_length = Some(len),
738 Ok(len) => warnings.push(format!(
739 "Invalid 'lineLength' value: {len} exceeds maximum ({MAX_LINE_LENGTH})"
740 )),
741 Err(_) => warnings.push(format!("Invalid 'lineLength' value: {l} is too large")),
742 }
743 } else {
744 warnings.push(format!("Invalid 'lineLength' value: expected number, got {value}"));
745 }
746 }
747 _ if key.starts_with("MD") || key.starts_with("md") => {
749 let normalized = key.to_uppercase();
750 if !is_valid_rule_name(&normalized) {
751 warnings.push(format!("Unknown rule: {key}"));
752 }
753 rules.insert(normalized, value);
754 }
755 _ => {
756 warnings.push(format!("Unknown configuration key: {key}"));
758 }
759 }
760 }
761
762 let settings = LspRuleSettings {
763 line_length,
764 disable: if disable.is_empty() { None } else { Some(disable) },
765 enable: if enable.is_empty() { None } else { Some(enable) },
766 rules,
767 };
768
769 log::info!("Applied Neovim-style rule settings (manual parse)");
770 config.settings = Some(settings);
771 drop(config);
772 config_applied = true;
773 } else {
774 log::warn!("Could not parse configuration settings: {rumdl_settings:?}");
775 }
776
777 for warning in &warnings {
779 log::warn!("{warning}");
780 }
781
782 if !warnings.is_empty() {
784 let message = if warnings.len() == 1 {
785 format!("rumdl: {}", warnings[0])
786 } else {
787 format!("rumdl configuration warnings:\n{}", warnings.join("\n"))
788 };
789 self.client.log_message(MessageType::WARNING, message).await;
790 }
791
792 if !config_applied {
793 log::debug!("No configuration changes applied");
794 }
795
796 self.config_cache.write().await.clear();
798
799 if config_applied {
807 self.load_configuration(false).await;
808
809 if self.update_tx.send(IndexUpdate::FullRescan).await.is_err() {
813 log::warn!("Failed to request workspace rescan after configuration change");
814 }
815 }
816
817 let doc_list: Vec<_> = {
819 let documents = self.documents.read().await;
820 documents
821 .iter()
822 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
823 .collect()
824 };
825
826 let tasks = doc_list.into_iter().map(|(uri, text)| {
828 let server = self.clone();
829 tokio::spawn(async move {
830 server.update_diagnostics(uri, text, true).await;
831 })
832 });
833
834 let _ = join_all(tasks).await;
836 }
837
838 async fn shutdown(&self) -> JsonRpcResult<()> {
839 log::info!("Shutting down rumdl Language Server");
840
841 let _ = self.update_tx.send(IndexUpdate::Shutdown).await;
843
844 Ok(())
845 }
846
847 async fn did_open(&self, params: DidOpenTextDocumentParams) {
848 let uri = params.text_document.uri;
849 let text = params.text_document.text;
850 let version = params.text_document.version;
851
852 let entry = DocumentEntry {
853 content: text.clone(),
854 version: Some(version),
855 from_disk: false,
856 };
857 self.documents.write().await.insert(uri.clone(), entry);
858
859 if let Ok(path) = uri.to_file_path() {
861 let _ = self
862 .update_tx
863 .send(IndexUpdate::FileChanged {
864 path,
865 content: text.clone(),
866 })
867 .await;
868 }
869
870 self.update_diagnostics(uri, text, true).await;
871 }
872
873 async fn did_change(&self, params: DidChangeTextDocumentParams) {
874 let uri = params.text_document.uri;
875 let version = params.text_document.version;
876
877 if let Some(change) = params.content_changes.into_iter().next() {
878 let text = change.text;
879
880 let entry = DocumentEntry {
881 content: text.clone(),
882 version: Some(version),
883 from_disk: false,
884 };
885 self.documents.write().await.insert(uri.clone(), entry);
886
887 if let Ok(path) = uri.to_file_path() {
889 let _ = self
890 .update_tx
891 .send(IndexUpdate::FileChanged {
892 path,
893 content: text.clone(),
894 })
895 .await;
896 }
897
898 self.update_diagnostics(uri, text, false).await;
899 }
900 }
901
902 async fn will_save_wait_until(&self, params: WillSaveTextDocumentParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
903 if params.reason != TextDocumentSaveReason::MANUAL {
906 return Ok(None);
907 }
908
909 let config_guard = self.config.read().await;
910 let enable_auto_fix = config_guard.enable_auto_fix;
911 drop(config_guard);
912
913 if !enable_auto_fix {
914 return Ok(None);
915 }
916
917 let Some(text) = self.get_document_content(¶ms.text_document.uri).await else {
919 return Ok(None);
920 };
921
922 match self.apply_all_fixes(¶ms.text_document.uri, &text).await {
924 Ok(Some(fixed_text)) => {
925 Ok(Some(vec![TextEdit {
927 range: Range {
928 start: Position { line: 0, character: 0 },
929 end: self.get_end_position(&text),
930 },
931 new_text: fixed_text,
932 }]))
933 }
934 Ok(None) => Ok(None),
935 Err(e) => {
936 log::error!("Failed to generate fixes in will_save_wait_until: {e}");
937 Ok(None)
938 }
939 }
940 }
941
942 async fn did_save(&self, params: DidSaveTextDocumentParams) {
943 if let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
946 self.update_diagnostics(params.text_document.uri, entry.content.clone(), true)
947 .await;
948 }
949 }
950
951 async fn did_close(&self, params: DidCloseTextDocumentParams) {
952 self.documents.write().await.remove(¶ms.text_document.uri);
954
955 self.client
958 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
959 .await;
960 }
961
962 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
963 const CONFIG_FILES: &[&str] = &[
965 ".rumdl.toml",
966 "rumdl.toml",
967 "pyproject.toml",
968 ".markdownlint.json",
969 ".markdownlint-cli2.jsonc",
970 ".markdownlint-cli2.yaml",
971 ".markdownlint-cli2.yml",
972 ];
973
974 let mut config_changed = false;
975
976 for change in ¶ms.changes {
977 if let Ok(path) = change.uri.to_file_path() {
978 let file_name = path.file_name().and_then(|f| f.to_str());
979
980 if let Some(name) = file_name
982 && CONFIG_FILES.contains(&name)
983 && !config_changed
984 {
985 log::info!("Config file changed: {}, invalidating config cache", path.display());
986
987 let mut cache = self.config_cache.write().await;
991 cache.clear();
992
993 drop(cache);
995 self.reload_configuration().await;
996 config_changed = true;
997 }
998
999 if let Some(ext) = path.extension()
1001 && is_markdown_extension(ext)
1002 {
1003 match change.typ {
1004 FileChangeType::CREATED | FileChangeType::CHANGED => {
1005 let roots = self.workspace_roots.read().await.clone();
1010 let (options, excludes) = {
1011 let config = self.rumdl_config.read().await;
1012 (
1013 crate::lsp::index_worker::index_walk_options(&config),
1014 ExcludeMatchers::new(&config.global.exclude),
1015 )
1016 };
1017 if crate::lsp::index_worker::path_is_ignored_for_index(&roots, &path, &options, &excludes) {
1018 let _ = self
1023 .update_tx
1024 .send(IndexUpdate::FileDeleted { path: path.clone() })
1025 .await;
1026 continue;
1027 }
1028 if let Ok(content) = tokio::fs::read_to_string(&path).await {
1030 let _ = self
1031 .update_tx
1032 .send(IndexUpdate::FileChanged {
1033 path: path.clone(),
1034 content,
1035 })
1036 .await;
1037 }
1038 }
1039 FileChangeType::DELETED => {
1040 let _ = self
1041 .update_tx
1042 .send(IndexUpdate::FileDeleted { path: path.clone() })
1043 .await;
1044 }
1045 _ => {}
1046 }
1047 }
1048 }
1049 }
1050
1051 if config_changed {
1053 if self.update_tx.send(IndexUpdate::FullRescan).await.is_err() {
1057 log::warn!("Failed to request workspace rescan after config change");
1058 }
1059
1060 let docs_to_update: Vec<(Url, String)> = {
1061 let docs = self.documents.read().await;
1062 docs.iter()
1063 .filter(|(_, entry)| !entry.from_disk)
1064 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
1065 .collect()
1066 };
1067
1068 for (uri, text) in docs_to_update {
1069 self.update_diagnostics(uri, text, true).await;
1070 }
1071 }
1072 }
1073
1074 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
1075 let uri = params.text_document.uri;
1076 let range = params.range;
1077 let requested_kinds = params.context.only;
1078
1079 if let Some(text) = self.get_document_content(&uri).await {
1080 match self.get_code_actions(&uri, &text, range).await {
1081 Ok(actions) => {
1082 let filtered_actions = if let Some(ref kinds) = requested_kinds
1086 && !kinds.is_empty()
1087 {
1088 actions
1089 .into_iter()
1090 .filter(|action| {
1091 action.kind.as_ref().is_some_and(|action_kind| {
1092 let action_kind_str = action_kind.as_str();
1093 kinds.iter().any(|requested| {
1094 let requested_str = requested.as_str();
1095 action_kind_str.starts_with(requested_str)
1098 })
1099 })
1100 })
1101 .collect()
1102 } else {
1103 actions
1104 };
1105
1106 let response: Vec<CodeActionOrCommand> = filtered_actions
1107 .into_iter()
1108 .map(CodeActionOrCommand::CodeAction)
1109 .collect();
1110 Ok(Some(response))
1111 }
1112 Err(e) => {
1113 log::error!("Failed to get code actions: {e}");
1114 Ok(None)
1115 }
1116 }
1117 } else {
1118 Ok(None)
1119 }
1120 }
1121
1122 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1123 log::debug!(
1128 "Range formatting requested for {:?}, formatting entire document due to rule interdependencies",
1129 params.range
1130 );
1131
1132 let formatting_params = DocumentFormattingParams {
1133 text_document: params.text_document,
1134 options: params.options,
1135 work_done_progress_params: params.work_done_progress_params,
1136 };
1137
1138 self.formatting(formatting_params).await
1139 }
1140
1141 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
1142 let uri = params.text_document.uri;
1143 let options = params.options;
1144
1145 log::debug!("Formatting request for: {uri}");
1146 log::debug!(
1147 "FormattingOptions: insert_final_newline={:?}, trim_final_newlines={:?}, trim_trailing_whitespace={:?}",
1148 options.insert_final_newline,
1149 options.trim_final_newlines,
1150 options.trim_trailing_whitespace
1151 );
1152
1153 if let Some(text) = self.get_document_content(&uri).await {
1154 let mut result = match self.apply_all_fixes(&uri, &text).await {
1163 Ok(Some(fixed)) => fixed,
1164 Ok(None) => text.clone(),
1165 Err(e) => {
1166 log::error!("Failed to apply fixes during formatting: {e}");
1167 text.clone()
1168 }
1169 };
1170
1171 result = Self::apply_formatting_options(result, &options);
1174
1175 if result != text {
1177 log::debug!("Returning formatting edits");
1178 let end_position = self.get_end_position(&text);
1179 let edit = TextEdit {
1180 range: Range {
1181 start: Position { line: 0, character: 0 },
1182 end: end_position,
1183 },
1184 new_text: result,
1185 };
1186 return Ok(Some(vec![edit]));
1187 }
1188
1189 Ok(Some(Vec::new()))
1190 } else {
1191 log::warn!("Document not found: {uri}");
1192 Ok(None)
1193 }
1194 }
1195
1196 async fn goto_definition(&self, params: GotoDefinitionParams) -> JsonRpcResult<Option<GotoDefinitionResponse>> {
1197 if !self.config.read().await.enable_link_navigation {
1198 return Ok(None);
1199 }
1200 let uri = params.text_document_position_params.text_document.uri;
1201 let position = params.text_document_position_params.position;
1202
1203 log::debug!("Go-to-definition at {uri} {}:{}", position.line, position.character);
1204
1205 Ok(self.handle_goto_definition(&uri, position).await)
1206 }
1207
1208 async fn references(&self, params: ReferenceParams) -> JsonRpcResult<Option<Vec<Location>>> {
1209 if !self.config.read().await.enable_link_navigation {
1210 return Ok(None);
1211 }
1212 let uri = params.text_document_position.text_document.uri;
1213 let position = params.text_document_position.position;
1214
1215 log::debug!("Find references at {uri} {}:{}", position.line, position.character);
1216
1217 Ok(self.handle_references(&uri, position).await)
1218 }
1219
1220 async fn hover(&self, params: HoverParams) -> JsonRpcResult<Option<Hover>> {
1221 if !self.config.read().await.enable_link_navigation {
1222 return Ok(None);
1223 }
1224 let uri = params.text_document_position_params.text_document.uri;
1225 let position = params.text_document_position_params.position;
1226
1227 log::debug!("Hover at {uri} {}:{}", position.line, position.character);
1228
1229 Ok(self.handle_hover(&uri, position).await)
1230 }
1231
1232 async fn prepare_rename(&self, params: TextDocumentPositionParams) -> JsonRpcResult<Option<PrepareRenameResponse>> {
1233 if !self.config.read().await.enable_link_navigation {
1234 return Ok(None);
1235 }
1236 let uri = params.text_document.uri;
1237 let position = params.position;
1238
1239 log::debug!("Prepare rename at {uri} {}:{}", position.line, position.character);
1240
1241 Ok(self.handle_prepare_rename(&uri, position).await)
1242 }
1243
1244 async fn rename(&self, params: RenameParams) -> JsonRpcResult<Option<WorkspaceEdit>> {
1245 if !self.config.read().await.enable_link_navigation {
1246 return Ok(None);
1247 }
1248 let uri = params.text_document_position.text_document.uri;
1249 let position = params.text_document_position.position;
1250 let new_name = params.new_name;
1251
1252 log::debug!("Rename at {uri} {}:{} → {new_name}", position.line, position.character);
1253
1254 Ok(self.handle_rename(&uri, position, &new_name).await)
1255 }
1256
1257 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
1258 let uri = params.text_document.uri;
1259
1260 if let Some(text) = self.get_open_document_content(&uri).await {
1261 match self.lint_document(&uri, &text, true).await {
1262 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1263 RelatedFullDocumentDiagnosticReport {
1264 related_documents: None,
1265 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1266 result_id: None,
1267 items: diagnostics,
1268 },
1269 },
1270 ))),
1271 Err(e) => {
1272 log::error!("Failed to get diagnostics: {e}");
1273 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1274 RelatedFullDocumentDiagnosticReport {
1275 related_documents: None,
1276 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1277 result_id: None,
1278 items: Vec::new(),
1279 },
1280 },
1281 )))
1282 }
1283 }
1284 } else {
1285 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1286 RelatedFullDocumentDiagnosticReport {
1287 related_documents: None,
1288 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1289 result_id: None,
1290 items: Vec::new(),
1291 },
1292 },
1293 )))
1294 }
1295 }
1296
1297 async fn document_symbol(&self, params: DocumentSymbolParams) -> JsonRpcResult<Option<DocumentSymbolResponse>> {
1298 if !self.config.read().await.enable_symbols {
1299 return Ok(None);
1300 }
1301
1302 let uri = params.text_document.uri;
1303 let Some(text) = self.get_document_content(&uri).await else {
1304 return Ok(None);
1305 };
1306
1307 let flavor = self.resolve_flavor_for_uri(&uri).await;
1308 let ctx = crate::lint_context::LintContext::new(&text, flavor, None);
1309
1310 if *self.client_supports_hierarchical_symbols.read().await {
1311 let symbols = super::symbols::document_symbols(&ctx);
1312 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Nested(symbols)))
1313 } else {
1314 let symbols = super::symbols::document_symbols_flat(&ctx, &uri);
1315 Ok((!symbols.is_empty()).then_some(DocumentSymbolResponse::Flat(symbols)))
1316 }
1317 }
1318
1319 async fn symbol(&self, params: WorkspaceSymbolParams) -> JsonRpcResult<Option<Vec<SymbolInformation>>> {
1320 if !self.config.read().await.enable_symbols {
1321 return Ok(None);
1322 }
1323
1324 let query = params.query.to_lowercase();
1325 let index = self.workspace_index.read().await;
1326 let symbols = super::symbols::workspace_symbols(&index, &query);
1327 Ok(if symbols.is_empty() { None } else { Some(symbols) })
1328 }
1329}
1330
1331#[cfg(test)]
1332#[path = "tests.rs"]
1333mod tests;