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::lsp::index_worker::IndexWorker;
18use crate::lsp::types::{IndexState, IndexUpdate, LspRuleSettings, RumdlLspConfig};
19use crate::rule::FixCapability;
20use crate::rules;
21use crate::workspace_index::WorkspaceIndex;
22
23const MARKDOWN_EXTENSIONS: &[&str] = &["md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd"];
25
26const MAX_RULE_LIST_SIZE: usize = 100;
28
29const MAX_LINE_LENGTH: usize = 10_000;
31
32#[inline]
34fn is_markdown_extension(ext: &str) -> bool {
35 MARKDOWN_EXTENSIONS.contains(&ext.to_lowercase().as_str())
36}
37
38#[derive(Clone, Debug, PartialEq)]
40pub(crate) struct DocumentEntry {
41 pub(crate) content: String,
43 pub(crate) version: Option<i32>,
45 pub(crate) from_disk: bool,
47}
48
49#[derive(Clone, Debug)]
51pub(crate) struct ConfigCacheEntry {
52 pub(crate) config: Config,
54 pub(crate) config_file: Option<PathBuf>,
56 pub(crate) from_global_fallback: bool,
58}
59
60#[derive(Clone)]
70pub struct RumdlLanguageServer {
71 pub(crate) client: Client,
72 pub(crate) config: Arc<RwLock<RumdlLspConfig>>,
74 pub(crate) rumdl_config: Arc<RwLock<Config>>,
76 pub(crate) documents: Arc<RwLock<HashMap<Url, DocumentEntry>>>,
78 pub(crate) workspace_roots: Arc<RwLock<Vec<PathBuf>>>,
80 pub(crate) config_cache: Arc<RwLock<HashMap<PathBuf, ConfigCacheEntry>>>,
83 pub(crate) workspace_index: Arc<RwLock<WorkspaceIndex>>,
85 pub(crate) index_state: Arc<RwLock<IndexState>>,
87 pub(crate) update_tx: mpsc::Sender<IndexUpdate>,
89 pub(crate) client_supports_pull_diagnostics: Arc<RwLock<bool>>,
92}
93
94impl RumdlLanguageServer {
95 pub fn new(client: Client, cli_config_path: Option<&str>) -> Self {
96 let mut initial_config = RumdlLspConfig::default();
98 if let Some(path) = cli_config_path {
99 initial_config.config_path = Some(path.to_string());
100 }
101
102 let workspace_index = Arc::new(RwLock::new(WorkspaceIndex::new()));
104 let index_state = Arc::new(RwLock::new(IndexState::default()));
105 let workspace_roots = Arc::new(RwLock::new(Vec::new()));
106
107 let (update_tx, update_rx) = mpsc::channel::<IndexUpdate>(100);
109 let (relint_tx, _relint_rx) = mpsc::channel::<PathBuf>(100);
110
111 let worker = IndexWorker::new(
113 update_rx,
114 workspace_index.clone(),
115 index_state.clone(),
116 client.clone(),
117 workspace_roots.clone(),
118 relint_tx,
119 );
120 tokio::spawn(worker.run());
121
122 Self {
123 client,
124 config: Arc::new(RwLock::new(initial_config)),
125 rumdl_config: Arc::new(RwLock::new(Config::default())),
126 documents: Arc::new(RwLock::new(HashMap::new())),
127 workspace_roots,
128 config_cache: Arc::new(RwLock::new(HashMap::new())),
129 workspace_index,
130 index_state,
131 update_tx,
132 client_supports_pull_diagnostics: Arc::new(RwLock::new(false)),
133 }
134 }
135
136 async fn get_document_content(&self, uri: &Url) -> Option<String> {
142 {
144 let docs = self.documents.read().await;
145 if let Some(entry) = docs.get(uri) {
146 return Some(entry.content.clone());
147 }
148 }
149
150 if let Ok(path) = uri.to_file_path() {
152 if let Ok(content) = tokio::fs::read_to_string(&path).await {
153 let entry = DocumentEntry {
155 content: content.clone(),
156 version: None,
157 from_disk: true,
158 };
159
160 let mut docs = self.documents.write().await;
161 docs.insert(uri.clone(), entry);
162
163 log::debug!("Loaded document from disk and cached: {uri}");
164 return Some(content);
165 } else {
166 log::debug!("Failed to read file from disk: {uri}");
167 }
168 }
169
170 None
171 }
172
173 async fn get_open_document_content(&self, uri: &Url) -> Option<String> {
179 let docs = self.documents.read().await;
180 docs.get(uri)
181 .and_then(|entry| (!entry.from_disk).then(|| entry.content.clone()))
182 }
183}
184
185#[tower_lsp::async_trait]
186impl LanguageServer for RumdlLanguageServer {
187 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
188 log::info!("Initializing rumdl Language Server");
189
190 if let Some(options) = params.initialization_options
192 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
193 {
194 *self.config.write().await = config;
195 }
196
197 let supports_pull = params
200 .capabilities
201 .text_document
202 .as_ref()
203 .and_then(|td| td.diagnostic.as_ref())
204 .is_some();
205
206 if supports_pull {
207 log::info!("Client supports pull diagnostics - disabling push to avoid duplicates");
208 *self.client_supports_pull_diagnostics.write().await = true;
209 } else {
210 log::info!("Client does not support pull diagnostics - using push model");
211 }
212
213 let mut roots = Vec::new();
215 if let Some(workspace_folders) = params.workspace_folders {
216 for folder in workspace_folders {
217 if let Ok(path) = folder.uri.to_file_path() {
218 log::info!("Workspace root: {}", path.display());
219 roots.push(path);
220 }
221 }
222 } else if let Some(root_uri) = params.root_uri
223 && let Ok(path) = root_uri.to_file_path()
224 {
225 log::info!("Workspace root: {}", path.display());
226 roots.push(path);
227 }
228 *self.workspace_roots.write().await = roots;
229
230 self.load_configuration(false).await;
232
233 Ok(InitializeResult {
234 capabilities: ServerCapabilities {
235 text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
236 open_close: Some(true),
237 change: Some(TextDocumentSyncKind::FULL),
238 will_save: Some(false),
239 will_save_wait_until: Some(true),
240 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
241 include_text: Some(false),
242 })),
243 })),
244 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
245 code_action_kinds: Some(vec![
246 CodeActionKind::QUICKFIX,
247 CodeActionKind::SOURCE_FIX_ALL,
248 CodeActionKind::new("source.fixAll.rumdl"),
249 ]),
250 work_done_progress_options: WorkDoneProgressOptions::default(),
251 resolve_provider: None,
252 })),
253 document_formatting_provider: Some(OneOf::Left(true)),
254 document_range_formatting_provider: Some(OneOf::Left(true)),
255 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
256 identifier: Some("rumdl".to_string()),
257 inter_file_dependencies: true,
258 workspace_diagnostics: false,
259 work_done_progress_options: WorkDoneProgressOptions::default(),
260 })),
261 completion_provider: Some(CompletionOptions {
262 trigger_characters: Some(vec!["`".to_string()]),
263 resolve_provider: Some(false),
264 work_done_progress_options: WorkDoneProgressOptions::default(),
265 all_commit_characters: None,
266 completion_item: None,
267 }),
268 workspace: Some(WorkspaceServerCapabilities {
269 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
270 supported: Some(true),
271 change_notifications: Some(OneOf::Left(true)),
272 }),
273 file_operations: None,
274 }),
275 ..Default::default()
276 },
277 server_info: Some(ServerInfo {
278 name: "rumdl".to_string(),
279 version: Some(env!("CARGO_PKG_VERSION").to_string()),
280 }),
281 })
282 }
283
284 async fn initialized(&self, _: InitializedParams) {
285 let version = env!("CARGO_PKG_VERSION");
286
287 let (binary_path, build_time) = std::env::current_exe()
289 .ok()
290 .map(|path| {
291 let path_str = path.to_str().unwrap_or("unknown").to_string();
292 let build_time = std::fs::metadata(&path)
293 .ok()
294 .and_then(|metadata| metadata.modified().ok())
295 .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
296 .and_then(|duration| {
297 let secs = duration.as_secs();
298 chrono::DateTime::from_timestamp(secs as i64, 0)
299 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
300 })
301 .unwrap_or_else(|| "unknown".to_string());
302 (path_str, build_time)
303 })
304 .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string()));
305
306 let working_dir = std::env::current_dir()
307 .ok()
308 .and_then(|p| p.to_str().map(|s| s.to_string()))
309 .unwrap_or_else(|| "unknown".to_string());
310
311 log::info!("rumdl Language Server v{version} initialized (built: {build_time}, binary: {binary_path})");
312 log::info!("Working directory: {working_dir}");
313
314 self.client
315 .log_message(MessageType::INFO, format!("rumdl v{version} Language Server started"))
316 .await;
317
318 if self.update_tx.send(IndexUpdate::FullRescan).await.is_err() {
320 log::warn!("Failed to trigger initial workspace indexing");
321 } else {
322 log::info!("Triggered initial workspace indexing for cross-file analysis");
323 }
324
325 let markdown_patterns = [
328 "**/*.md",
329 "**/*.markdown",
330 "**/*.mdx",
331 "**/*.mkd",
332 "**/*.mkdn",
333 "**/*.mdown",
334 "**/*.mdwn",
335 "**/*.qmd",
336 "**/*.rmd",
337 ];
338 let watchers: Vec<_> = markdown_patterns
339 .iter()
340 .map(|pattern| FileSystemWatcher {
341 glob_pattern: GlobPattern::String((*pattern).to_string()),
342 kind: Some(WatchKind::all()),
343 })
344 .collect();
345
346 let registration = Registration {
347 id: "markdown-watcher".to_string(),
348 method: "workspace/didChangeWatchedFiles".to_string(),
349 register_options: Some(
350 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { watchers }).unwrap(),
351 ),
352 };
353
354 if self.client.register_capability(vec![registration]).await.is_err() {
355 log::debug!("Client does not support file watching capability");
356 }
357 }
358
359 async fn completion(&self, params: CompletionParams) -> JsonRpcResult<Option<CompletionResponse>> {
360 let uri = params.text_document_position.text_document.uri;
361 let position = params.text_document_position.position;
362
363 let Some(text) = self.get_document_content(&uri).await else {
365 return Ok(None);
366 };
367
368 let Some((start_col, current_text)) = Self::detect_code_fence_language_position(&text, position) else {
370 return Ok(None);
371 };
372
373 log::debug!(
374 "Code fence completion triggered at {}:{}, current text: '{}'",
375 position.line,
376 position.character,
377 current_text
378 );
379
380 let items = self
382 .get_language_completions(&uri, ¤t_text, start_col, position)
383 .await;
384
385 if items.is_empty() {
386 Ok(None)
387 } else {
388 Ok(Some(CompletionResponse::Array(items)))
389 }
390 }
391
392 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
393 let mut roots = self.workspace_roots.write().await;
395
396 for removed in ¶ms.event.removed {
398 if let Ok(path) = removed.uri.to_file_path() {
399 roots.retain(|r| r != &path);
400 log::info!("Removed workspace root: {}", path.display());
401 }
402 }
403
404 for added in ¶ms.event.added {
406 if let Ok(path) = added.uri.to_file_path()
407 && !roots.contains(&path)
408 {
409 log::info!("Added workspace root: {}", path.display());
410 roots.push(path);
411 }
412 }
413 drop(roots);
414
415 self.config_cache.write().await.clear();
417
418 self.reload_configuration().await;
420
421 if self.update_tx.send(IndexUpdate::FullRescan).await.is_err() {
423 log::warn!("Failed to trigger workspace rescan after folder change");
424 }
425 }
426
427 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
428 log::debug!("Configuration changed: {:?}", params.settings);
429
430 let settings_value = params.settings;
434
435 let rumdl_settings = if let serde_json::Value::Object(ref obj) = settings_value {
437 obj.get("rumdl").cloned().unwrap_or(settings_value.clone())
438 } else {
439 settings_value
440 };
441
442 let mut config_applied = false;
444 let mut warnings: Vec<String> = Vec::new();
445
446 if let Ok(rule_settings) = serde_json::from_value::<LspRuleSettings>(rumdl_settings.clone())
450 && (rule_settings.disable.is_some()
451 || rule_settings.enable.is_some()
452 || rule_settings.line_length.is_some()
453 || !rule_settings.rules.is_empty())
454 {
455 if let Some(ref disable) = rule_settings.disable {
457 for rule in disable {
458 if !is_valid_rule_name(rule) {
459 warnings.push(format!("Unknown rule in disable list: {rule}"));
460 }
461 }
462 }
463 if let Some(ref enable) = rule_settings.enable {
464 for rule in enable {
465 if !is_valid_rule_name(rule) {
466 warnings.push(format!("Unknown rule in enable list: {rule}"));
467 }
468 }
469 }
470 for rule_name in rule_settings.rules.keys() {
472 if !is_valid_rule_name(rule_name) {
473 warnings.push(format!("Unknown rule in settings: {rule_name}"));
474 }
475 }
476
477 log::info!("Applied rule settings from configuration (Neovim style)");
478 let mut config = self.config.write().await;
479 config.settings = Some(rule_settings);
480 drop(config);
481 config_applied = true;
482 } else if let Ok(full_config) = serde_json::from_value::<RumdlLspConfig>(rumdl_settings.clone())
483 && (full_config.config_path.is_some()
484 || full_config.enable_rules.is_some()
485 || full_config.disable_rules.is_some()
486 || full_config.settings.is_some()
487 || !full_config.enable_linting
488 || full_config.enable_auto_fix)
489 {
490 if let Some(ref rules) = full_config.enable_rules {
492 for rule in rules {
493 if !is_valid_rule_name(rule) {
494 warnings.push(format!("Unknown rule in enableRules: {rule}"));
495 }
496 }
497 }
498 if let Some(ref rules) = full_config.disable_rules {
499 for rule in rules {
500 if !is_valid_rule_name(rule) {
501 warnings.push(format!("Unknown rule in disableRules: {rule}"));
502 }
503 }
504 }
505
506 log::info!("Applied full LSP configuration from settings");
507 *self.config.write().await = full_config;
508 config_applied = true;
509 } else if let serde_json::Value::Object(obj) = rumdl_settings {
510 let mut config = self.config.write().await;
513
514 let mut rules = std::collections::HashMap::new();
516 let mut disable = Vec::new();
517 let mut enable = Vec::new();
518 let mut line_length = None;
519
520 for (key, value) in obj {
521 match key.as_str() {
522 "disable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
523 Ok(d) => {
524 if d.len() > MAX_RULE_LIST_SIZE {
525 warnings.push(format!(
526 "Too many rules in 'disable' ({} > {}), truncating",
527 d.len(),
528 MAX_RULE_LIST_SIZE
529 ));
530 }
531 for rule in d.iter().take(MAX_RULE_LIST_SIZE) {
532 if !is_valid_rule_name(rule) {
533 warnings.push(format!("Unknown rule in disable: {rule}"));
534 }
535 }
536 disable = d.into_iter().take(MAX_RULE_LIST_SIZE).collect();
537 }
538 Err(_) => {
539 warnings.push(format!(
540 "Invalid 'disable' value: expected array of strings, got {value}"
541 ));
542 }
543 },
544 "enable" => match serde_json::from_value::<Vec<String>>(value.clone()) {
545 Ok(e) => {
546 if e.len() > MAX_RULE_LIST_SIZE {
547 warnings.push(format!(
548 "Too many rules in 'enable' ({} > {}), truncating",
549 e.len(),
550 MAX_RULE_LIST_SIZE
551 ));
552 }
553 for rule in e.iter().take(MAX_RULE_LIST_SIZE) {
554 if !is_valid_rule_name(rule) {
555 warnings.push(format!("Unknown rule in enable: {rule}"));
556 }
557 }
558 enable = e.into_iter().take(MAX_RULE_LIST_SIZE).collect();
559 }
560 Err(_) => {
561 warnings.push(format!(
562 "Invalid 'enable' value: expected array of strings, got {value}"
563 ));
564 }
565 },
566 "lineLength" | "line_length" | "line-length" => {
567 if let Some(l) = value.as_u64() {
568 match usize::try_from(l) {
569 Ok(len) if len <= MAX_LINE_LENGTH => line_length = Some(len),
570 Ok(len) => warnings.push(format!(
571 "Invalid 'lineLength' value: {len} exceeds maximum ({MAX_LINE_LENGTH})"
572 )),
573 Err(_) => warnings.push(format!("Invalid 'lineLength' value: {l} is too large")),
574 }
575 } else {
576 warnings.push(format!("Invalid 'lineLength' value: expected number, got {value}"));
577 }
578 }
579 _ if key.starts_with("MD") || key.starts_with("md") => {
581 let normalized = key.to_uppercase();
582 if !is_valid_rule_name(&normalized) {
583 warnings.push(format!("Unknown rule: {key}"));
584 }
585 rules.insert(normalized, value);
586 }
587 _ => {
588 warnings.push(format!("Unknown configuration key: {key}"));
590 }
591 }
592 }
593
594 let settings = LspRuleSettings {
595 line_length,
596 disable: if disable.is_empty() { None } else { Some(disable) },
597 enable: if enable.is_empty() { None } else { Some(enable) },
598 rules,
599 };
600
601 log::info!("Applied Neovim-style rule settings (manual parse)");
602 config.settings = Some(settings);
603 drop(config);
604 config_applied = true;
605 } else {
606 log::warn!("Could not parse configuration settings: {rumdl_settings:?}");
607 }
608
609 for warning in &warnings {
611 log::warn!("{warning}");
612 }
613
614 if !warnings.is_empty() {
616 let message = if warnings.len() == 1 {
617 format!("rumdl: {}", warnings[0])
618 } else {
619 format!("rumdl configuration warnings:\n{}", warnings.join("\n"))
620 };
621 self.client.log_message(MessageType::WARNING, message).await;
622 }
623
624 if !config_applied {
625 log::debug!("No configuration changes applied");
626 }
627
628 self.config_cache.write().await.clear();
630
631 let doc_list: Vec<_> = {
633 let documents = self.documents.read().await;
634 documents
635 .iter()
636 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
637 .collect()
638 };
639
640 let tasks = doc_list.into_iter().map(|(uri, text)| {
642 let server = self.clone();
643 tokio::spawn(async move {
644 server.update_diagnostics(uri, text).await;
645 })
646 });
647
648 let _ = join_all(tasks).await;
650 }
651
652 async fn shutdown(&self) -> JsonRpcResult<()> {
653 log::info!("Shutting down rumdl Language Server");
654
655 let _ = self.update_tx.send(IndexUpdate::Shutdown).await;
657
658 Ok(())
659 }
660
661 async fn did_open(&self, params: DidOpenTextDocumentParams) {
662 let uri = params.text_document.uri;
663 let text = params.text_document.text;
664 let version = params.text_document.version;
665
666 let entry = DocumentEntry {
667 content: text.clone(),
668 version: Some(version),
669 from_disk: false,
670 };
671 self.documents.write().await.insert(uri.clone(), entry);
672
673 if let Ok(path) = uri.to_file_path() {
675 let _ = self
676 .update_tx
677 .send(IndexUpdate::FileChanged {
678 path,
679 content: text.clone(),
680 })
681 .await;
682 }
683
684 self.update_diagnostics(uri, text).await;
685 }
686
687 async fn did_change(&self, params: DidChangeTextDocumentParams) {
688 let uri = params.text_document.uri;
689 let version = params.text_document.version;
690
691 if let Some(change) = params.content_changes.into_iter().next() {
692 let text = change.text;
693
694 let entry = DocumentEntry {
695 content: text.clone(),
696 version: Some(version),
697 from_disk: false,
698 };
699 self.documents.write().await.insert(uri.clone(), entry);
700
701 if let Ok(path) = uri.to_file_path() {
703 let _ = self
704 .update_tx
705 .send(IndexUpdate::FileChanged {
706 path,
707 content: text.clone(),
708 })
709 .await;
710 }
711
712 self.update_diagnostics(uri, text).await;
713 }
714 }
715
716 async fn will_save_wait_until(&self, params: WillSaveTextDocumentParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
717 if params.reason != TextDocumentSaveReason::MANUAL {
720 return Ok(None);
721 }
722
723 let config_guard = self.config.read().await;
724 let enable_auto_fix = config_guard.enable_auto_fix;
725 drop(config_guard);
726
727 if !enable_auto_fix {
728 return Ok(None);
729 }
730
731 let Some(text) = self.get_document_content(¶ms.text_document.uri).await else {
733 return Ok(None);
734 };
735
736 match self.apply_all_fixes(¶ms.text_document.uri, &text).await {
738 Ok(Some(fixed_text)) => {
739 Ok(Some(vec![TextEdit {
741 range: Range {
742 start: Position { line: 0, character: 0 },
743 end: self.get_end_position(&text),
744 },
745 new_text: fixed_text,
746 }]))
747 }
748 Ok(None) => Ok(None),
749 Err(e) => {
750 log::error!("Failed to generate fixes in will_save_wait_until: {e}");
751 Ok(None)
752 }
753 }
754 }
755
756 async fn did_save(&self, params: DidSaveTextDocumentParams) {
757 if let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
760 self.update_diagnostics(params.text_document.uri, entry.content.clone())
761 .await;
762 }
763 }
764
765 async fn did_close(&self, params: DidCloseTextDocumentParams) {
766 self.documents.write().await.remove(¶ms.text_document.uri);
768
769 self.client
772 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
773 .await;
774 }
775
776 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
777 const CONFIG_FILES: &[&str] = &[".rumdl.toml", "rumdl.toml", "pyproject.toml", ".markdownlint.json"];
779
780 let mut config_changed = false;
781
782 for change in ¶ms.changes {
783 if let Ok(path) = change.uri.to_file_path() {
784 let file_name = path.file_name().and_then(|f| f.to_str());
785 let extension = path.extension().and_then(|e| e.to_str());
786
787 if let Some(name) = file_name
789 && CONFIG_FILES.contains(&name)
790 && !config_changed
791 {
792 log::info!("Config file changed: {}, invalidating config cache", path.display());
793
794 let mut cache = self.config_cache.write().await;
796 cache.retain(|_, entry| {
797 if let Some(config_file) = &entry.config_file {
798 config_file != &path
799 } else {
800 true
801 }
802 });
803
804 drop(cache);
806 self.reload_configuration().await;
807 config_changed = true;
808 }
809
810 if let Some(ext) = extension
812 && is_markdown_extension(ext)
813 {
814 match change.typ {
815 FileChangeType::CREATED | FileChangeType::CHANGED => {
816 if let Ok(content) = tokio::fs::read_to_string(&path).await {
818 let _ = self
819 .update_tx
820 .send(IndexUpdate::FileChanged {
821 path: path.clone(),
822 content,
823 })
824 .await;
825 }
826 }
827 FileChangeType::DELETED => {
828 let _ = self
829 .update_tx
830 .send(IndexUpdate::FileDeleted { path: path.clone() })
831 .await;
832 }
833 _ => {}
834 }
835 }
836 }
837 }
838
839 if config_changed {
841 let docs_to_update: Vec<(Url, String)> = {
842 let docs = self.documents.read().await;
843 docs.iter()
844 .filter(|(_, entry)| !entry.from_disk)
845 .map(|(uri, entry)| (uri.clone(), entry.content.clone()))
846 .collect()
847 };
848
849 for (uri, text) in docs_to_update {
850 self.update_diagnostics(uri, text).await;
851 }
852 }
853 }
854
855 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
856 let uri = params.text_document.uri;
857 let range = params.range;
858 let requested_kinds = params.context.only;
859
860 if let Some(text) = self.get_document_content(&uri).await {
861 match self.get_code_actions(&uri, &text, range).await {
862 Ok(actions) => {
863 let filtered_actions = if let Some(ref kinds) = requested_kinds
867 && !kinds.is_empty()
868 {
869 actions
870 .into_iter()
871 .filter(|action| {
872 action.kind.as_ref().is_some_and(|action_kind| {
873 let action_kind_str = action_kind.as_str();
874 kinds.iter().any(|requested| {
875 let requested_str = requested.as_str();
876 action_kind_str.starts_with(requested_str)
879 })
880 })
881 })
882 .collect()
883 } else {
884 actions
885 };
886
887 let response: Vec<CodeActionOrCommand> = filtered_actions
888 .into_iter()
889 .map(CodeActionOrCommand::CodeAction)
890 .collect();
891 Ok(Some(response))
892 }
893 Err(e) => {
894 log::error!("Failed to get code actions: {e}");
895 Ok(None)
896 }
897 }
898 } else {
899 Ok(None)
900 }
901 }
902
903 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
904 log::debug!(
909 "Range formatting requested for {:?}, formatting entire document due to rule interdependencies",
910 params.range
911 );
912
913 let formatting_params = DocumentFormattingParams {
914 text_document: params.text_document,
915 options: params.options,
916 work_done_progress_params: params.work_done_progress_params,
917 };
918
919 self.formatting(formatting_params).await
920 }
921
922 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
923 let uri = params.text_document.uri;
924 let options = params.options;
925
926 log::debug!("Formatting request for: {uri}");
927 log::debug!(
928 "FormattingOptions: insert_final_newline={:?}, trim_final_newlines={:?}, trim_trailing_whitespace={:?}",
929 options.insert_final_newline,
930 options.trim_final_newlines,
931 options.trim_trailing_whitespace
932 );
933
934 if let Some(text) = self.get_document_content(&uri).await {
935 let config_guard = self.config.read().await;
937 let lsp_config = config_guard.clone();
938 drop(config_guard);
939
940 let file_path = uri.to_file_path().ok();
942 let file_config = if let Some(ref path) = file_path {
943 self.resolve_config_for_file(path).await
944 } else {
945 self.rumdl_config.read().await.clone()
947 };
948
949 let rumdl_config = self.merge_lsp_settings(file_config, &lsp_config);
951
952 let all_rules = rules::all_rules(&rumdl_config);
953 let flavor = if let Some(ref path) = file_path {
954 rumdl_config.get_flavor_for_file(path)
955 } else {
956 rumdl_config.markdown_flavor()
957 };
958
959 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
961
962 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
964
965 let mut result = text.clone();
967 match crate::lint(&text, &filtered_rules, false, flavor, Some(&rumdl_config)) {
968 Ok(warnings) => {
969 log::debug!(
970 "Found {} warnings, {} with fixes",
971 warnings.len(),
972 warnings.iter().filter(|w| w.fix.is_some()).count()
973 );
974
975 let has_fixes = warnings.iter().any(|w| w.fix.is_some());
976 if has_fixes {
977 let fixable_warnings: Vec<_> = warnings
979 .iter()
980 .filter(|w| {
981 if let Some(rule_name) = &w.rule_name {
982 filtered_rules
983 .iter()
984 .find(|r| r.name() == rule_name)
985 .map(|r| r.fix_capability() != FixCapability::Unfixable)
986 .unwrap_or(false)
987 } else {
988 false
989 }
990 })
991 .cloned()
992 .collect();
993
994 match crate::utils::fix_utils::apply_warning_fixes(&text, &fixable_warnings) {
995 Ok(fixed_content) => {
996 result = fixed_content;
997 }
998 Err(e) => {
999 log::error!("Failed to apply fixes: {e}");
1000 }
1001 }
1002 }
1003 }
1004 Err(e) => {
1005 log::error!("Failed to lint document: {e}");
1006 }
1007 }
1008
1009 result = Self::apply_formatting_options(result, &options);
1012
1013 if result != text {
1015 log::debug!("Returning formatting edits");
1016 let end_position = self.get_end_position(&text);
1017 let edit = TextEdit {
1018 range: Range {
1019 start: Position { line: 0, character: 0 },
1020 end: end_position,
1021 },
1022 new_text: result,
1023 };
1024 return Ok(Some(vec![edit]));
1025 }
1026
1027 Ok(Some(Vec::new()))
1028 } else {
1029 log::warn!("Document not found: {uri}");
1030 Ok(None)
1031 }
1032 }
1033
1034 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
1035 let uri = params.text_document.uri;
1036
1037 if let Some(text) = self.get_open_document_content(&uri).await {
1038 match self.lint_document(&uri, &text).await {
1039 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1040 RelatedFullDocumentDiagnosticReport {
1041 related_documents: None,
1042 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1043 result_id: None,
1044 items: diagnostics,
1045 },
1046 },
1047 ))),
1048 Err(e) => {
1049 log::error!("Failed to get diagnostics: {e}");
1050 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1051 RelatedFullDocumentDiagnosticReport {
1052 related_documents: None,
1053 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1054 result_id: None,
1055 items: Vec::new(),
1056 },
1057 },
1058 )))
1059 }
1060 }
1061 } else {
1062 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
1063 RelatedFullDocumentDiagnosticReport {
1064 related_documents: None,
1065 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1066 result_id: None,
1067 items: Vec::new(),
1068 },
1069 },
1070 )))
1071 }
1072 }
1073}
1074
1075#[cfg(test)]
1076#[path = "tests.rs"]
1077mod tests;