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