1use std::collections::HashMap;
7use std::sync::Arc;
8
9use anyhow::Result;
10use tokio::sync::RwLock;
11use tower_lsp::jsonrpc::Result as JsonRpcResult;
12use tower_lsp::lsp_types::*;
13use tower_lsp::{Client, LanguageServer};
14
15use crate::config::Config;
16use crate::lsp::types::{RumdlLspConfig, warning_to_code_action, warning_to_diagnostic};
17use crate::rule::Rule;
18use crate::rules;
19
20#[derive(Clone, Debug, PartialEq)]
22struct DocumentEntry {
23 content: String,
25 version: Option<i32>,
27 from_disk: bool,
29}
30
31#[derive(Clone)]
39pub struct RumdlLanguageServer {
40 client: Client,
41 config: Arc<RwLock<RumdlLspConfig>>,
43 #[cfg_attr(test, allow(dead_code))]
45 pub(crate) rumdl_config: Arc<RwLock<Config>>,
46 documents: Arc<RwLock<HashMap<Url, DocumentEntry>>>,
48}
49
50impl RumdlLanguageServer {
51 pub fn new(client: Client) -> Self {
52 Self {
53 client,
54 config: Arc::new(RwLock::new(RumdlLspConfig::default())),
55 rumdl_config: Arc::new(RwLock::new(Config::default())),
56 documents: Arc::new(RwLock::new(HashMap::new())),
57 }
58 }
59
60 async fn get_document_content(&self, uri: &Url) -> Option<String> {
66 {
68 let docs = self.documents.read().await;
69 if let Some(entry) = docs.get(uri) {
70 return Some(entry.content.clone());
71 }
72 }
73
74 if let Ok(path) = uri.to_file_path() {
76 if let Ok(content) = tokio::fs::read_to_string(&path).await {
77 let entry = DocumentEntry {
79 content: content.clone(),
80 version: None,
81 from_disk: true,
82 };
83
84 let mut docs = self.documents.write().await;
85 docs.insert(uri.clone(), entry);
86
87 log::debug!("Loaded document from disk and cached: {uri}");
88 return Some(content);
89 } else {
90 log::debug!("Failed to read file from disk: {uri}");
91 }
92 }
93
94 None
95 }
96
97 fn apply_lsp_config_overrides(
99 &self,
100 mut filtered_rules: Vec<Box<dyn Rule>>,
101 lsp_config: &RumdlLspConfig,
102 ) -> Vec<Box<dyn Rule>> {
103 if let Some(enable) = &lsp_config.enable_rules
105 && !enable.is_empty()
106 {
107 let enable_set: std::collections::HashSet<String> = enable.iter().cloned().collect();
108 filtered_rules.retain(|rule| enable_set.contains(rule.name()));
109 }
110
111 if let Some(disable) = &lsp_config.disable_rules
113 && !disable.is_empty()
114 {
115 let disable_set: std::collections::HashSet<String> = disable.iter().cloned().collect();
116 filtered_rules.retain(|rule| !disable_set.contains(rule.name()));
117 }
118
119 filtered_rules
120 }
121
122 async fn should_exclude_uri(&self, uri: &Url) -> bool {
124 let file_path = match uri.to_file_path() {
126 Ok(path) => path,
127 Err(_) => return false, };
129
130 let rumdl_config = self.rumdl_config.read().await;
131 let exclude_patterns = &rumdl_config.global.exclude;
132
133 if exclude_patterns.is_empty() {
135 return false;
136 }
137
138 let path_to_check = if file_path.is_absolute() {
141 if let Ok(cwd) = std::env::current_dir() {
143 if let (Ok(canonical_cwd), Ok(canonical_path)) = (cwd.canonicalize(), file_path.canonicalize()) {
145 if let Ok(relative) = canonical_path.strip_prefix(&canonical_cwd) {
146 relative.to_string_lossy().to_string()
147 } else {
148 file_path.to_string_lossy().to_string()
150 }
151 } else {
152 file_path.to_string_lossy().to_string()
154 }
155 } else {
156 file_path.to_string_lossy().to_string()
157 }
158 } else {
159 file_path.to_string_lossy().to_string()
161 };
162
163 for pattern in exclude_patterns {
165 if let Ok(glob) = globset::Glob::new(pattern) {
166 let matcher = glob.compile_matcher();
167 if matcher.is_match(&path_to_check) {
168 log::debug!("Excluding file from LSP linting: {path_to_check}");
169 return true;
170 }
171 }
172 }
173
174 false
175 }
176
177 pub(crate) async fn lint_document(&self, uri: &Url, text: &str) -> Result<Vec<Diagnostic>> {
179 let config_guard = self.config.read().await;
180
181 if !config_guard.enable_linting {
183 return Ok(Vec::new());
184 }
185
186 let lsp_config = config_guard.clone();
187 drop(config_guard); if self.should_exclude_uri(uri).await {
191 return Ok(Vec::new());
192 }
193
194 let rumdl_config = self.rumdl_config.read().await;
196 let all_rules = rules::all_rules(&rumdl_config);
197 let flavor = rumdl_config.markdown_flavor();
198
199 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
201 drop(rumdl_config); filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
205
206 match crate::lint(text, &filtered_rules, false, flavor) {
208 Ok(warnings) => {
209 let diagnostics = warnings.iter().map(warning_to_diagnostic).collect();
210 Ok(diagnostics)
211 }
212 Err(e) => {
213 log::error!("Failed to lint document {uri}: {e}");
214 Ok(Vec::new())
215 }
216 }
217 }
218
219 async fn update_diagnostics(&self, uri: Url, text: String) {
221 match self.lint_document(&uri, &text).await {
222 Ok(diagnostics) => {
223 self.client.publish_diagnostics(uri, diagnostics, None).await;
224 }
225 Err(e) => {
226 log::error!("Failed to update diagnostics: {e}");
227 }
228 }
229 }
230
231 async fn apply_all_fixes(&self, uri: &Url, text: &str) -> Result<Option<String>> {
233 if self.should_exclude_uri(uri).await {
235 return Ok(None);
236 }
237
238 let config_guard = self.config.read().await;
239 let lsp_config = config_guard.clone();
240 drop(config_guard);
241
242 let rumdl_config = self.rumdl_config.read().await;
243 let all_rules = rules::all_rules(&rumdl_config);
244 let flavor = rumdl_config.markdown_flavor();
245
246 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
248 drop(rumdl_config);
249
250 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
252
253 let mut fixed_text = text.to_string();
255 let mut any_changes = false;
256
257 for rule in &filtered_rules {
258 let ctx = crate::lint_context::LintContext::new(&fixed_text, flavor);
259 match rule.fix(&ctx) {
260 Ok(new_text) => {
261 if new_text != fixed_text {
262 fixed_text = new_text;
263 any_changes = true;
264 }
265 }
266 Err(e) => {
267 log::warn!("Failed to apply fix for rule {}: {}", rule.name(), e);
268 }
269 }
270 }
271
272 if any_changes { Ok(Some(fixed_text)) } else { Ok(None) }
273 }
274
275 fn get_end_position(&self, text: &str) -> Position {
277 let mut line = 0u32;
278 let mut character = 0u32;
279
280 for ch in text.chars() {
281 if ch == '\n' {
282 line += 1;
283 character = 0;
284 } else {
285 character += 1;
286 }
287 }
288
289 Position { line, character }
290 }
291
292 async fn get_code_actions(&self, uri: &Url, text: &str, range: Range) -> Result<Vec<CodeAction>> {
294 let config_guard = self.config.read().await;
295 let lsp_config = config_guard.clone();
296 drop(config_guard);
297
298 let rumdl_config = self.rumdl_config.read().await;
299 let all_rules = rules::all_rules(&rumdl_config);
300 let flavor = rumdl_config.markdown_flavor();
301
302 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
304 drop(rumdl_config);
305
306 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
308
309 match crate::lint(text, &filtered_rules, false, flavor) {
310 Ok(warnings) => {
311 let mut actions = Vec::new();
312 let mut fixable_count = 0;
313
314 for warning in &warnings {
315 let warning_line = (warning.line.saturating_sub(1)) as u32;
317 if warning_line >= range.start.line
318 && warning_line <= range.end.line
319 && let Some(action) = warning_to_code_action(warning, uri, text)
320 {
321 actions.push(action);
322 if warning.fix.is_some() {
323 fixable_count += 1;
324 }
325 }
326 }
327
328 if fixable_count > 1 {
330 let total_fixable = warnings.iter().filter(|w| w.fix.is_some()).count();
332
333 if let Ok(fixed_content) = crate::utils::fix_utils::apply_warning_fixes(text, &warnings)
334 && fixed_content != text
335 {
336 let mut line = 0u32;
338 let mut character = 0u32;
339 for ch in text.chars() {
340 if ch == '\n' {
341 line += 1;
342 character = 0;
343 } else {
344 character += 1;
345 }
346 }
347
348 let fix_all_action = CodeAction {
349 title: format!("Fix all rumdl issues ({total_fixable} fixable)"),
350 kind: Some(CodeActionKind::QUICKFIX),
351 diagnostics: Some(Vec::new()),
352 edit: Some(WorkspaceEdit {
353 changes: Some(
354 [(
355 uri.clone(),
356 vec![TextEdit {
357 range: Range {
358 start: Position { line: 0, character: 0 },
359 end: Position { line, character },
360 },
361 new_text: fixed_content,
362 }],
363 )]
364 .into_iter()
365 .collect(),
366 ),
367 ..Default::default()
368 }),
369 command: None,
370 is_preferred: Some(true),
371 disabled: None,
372 data: None,
373 };
374
375 actions.insert(0, fix_all_action);
377 }
378 }
379
380 Ok(actions)
381 }
382 Err(e) => {
383 log::error!("Failed to get code actions: {e}");
384 Ok(Vec::new())
385 }
386 }
387 }
388
389 async fn load_configuration(&self, notify_client: bool) {
391 let config_guard = self.config.read().await;
392 let explicit_config_path = config_guard.config_path.clone();
393 drop(config_guard);
394
395 match Self::load_config_for_lsp(explicit_config_path.as_deref()) {
397 Ok(sourced_config) => {
398 let loaded_files = sourced_config.loaded_files.clone();
399 *self.rumdl_config.write().await = sourced_config.into();
400
401 if !loaded_files.is_empty() {
402 let message = format!("Loaded rumdl config from: {}", loaded_files.join(", "));
403 log::info!("{message}");
404 if notify_client {
405 self.client.log_message(MessageType::INFO, &message).await;
406 }
407 } else {
408 log::info!("Using default rumdl configuration (no config files found)");
409 }
410 }
411 Err(e) => {
412 let message = format!("Failed to load rumdl config: {e}");
413 log::warn!("{message}");
414 if notify_client {
415 self.client.log_message(MessageType::WARNING, &message).await;
416 }
417 *self.rumdl_config.write().await = crate::config::Config::default();
419 }
420 }
421 }
422
423 async fn reload_configuration(&self) {
425 self.load_configuration(true).await;
426 }
427
428 fn load_config_for_lsp(
430 config_path: Option<&str>,
431 ) -> Result<crate::config::SourcedConfig, crate::config::ConfigError> {
432 crate::config::SourcedConfig::load_with_discovery(config_path, None, false)
434 }
435}
436
437#[tower_lsp::async_trait]
438impl LanguageServer for RumdlLanguageServer {
439 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
440 log::info!("Initializing rumdl Language Server");
441
442 if let Some(options) = params.initialization_options
444 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
445 {
446 *self.config.write().await = config;
447 }
448
449 self.load_configuration(false).await;
451
452 Ok(InitializeResult {
453 capabilities: ServerCapabilities {
454 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
455 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
456 document_formatting_provider: Some(OneOf::Left(true)),
457 document_range_formatting_provider: Some(OneOf::Left(true)),
458 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
459 identifier: Some("rumdl".to_string()),
460 inter_file_dependencies: false,
461 workspace_diagnostics: false,
462 work_done_progress_options: WorkDoneProgressOptions::default(),
463 })),
464 workspace: Some(WorkspaceServerCapabilities {
465 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
466 supported: Some(true),
467 change_notifications: Some(OneOf::Left(true)),
468 }),
469 file_operations: None,
470 }),
471 ..Default::default()
472 },
473 server_info: Some(ServerInfo {
474 name: "rumdl".to_string(),
475 version: Some(env!("CARGO_PKG_VERSION").to_string()),
476 }),
477 })
478 }
479
480 async fn initialized(&self, _: InitializedParams) {
481 log::info!("rumdl Language Server initialized");
482
483 self.client
484 .log_message(MessageType::INFO, "rumdl Language Server started")
485 .await;
486 }
487
488 async fn did_change_workspace_folders(&self, _params: DidChangeWorkspaceFoldersParams) {
489 self.reload_configuration().await;
491 }
492
493 async fn shutdown(&self) -> JsonRpcResult<()> {
494 log::info!("Shutting down rumdl Language Server");
495 Ok(())
496 }
497
498 async fn did_open(&self, params: DidOpenTextDocumentParams) {
499 let uri = params.text_document.uri;
500 let text = params.text_document.text;
501 let version = params.text_document.version;
502
503 let entry = DocumentEntry {
505 content: text.clone(),
506 version: Some(version),
507 from_disk: false,
508 };
509 self.documents.write().await.insert(uri.clone(), entry);
510
511 self.update_diagnostics(uri, text).await;
513 }
514
515 async fn did_change(&self, params: DidChangeTextDocumentParams) {
516 let uri = params.text_document.uri;
517 let version = params.text_document.version;
518
519 if let Some(change) = params.content_changes.into_iter().next() {
521 let text = change.text;
522
523 let entry = DocumentEntry {
525 content: text.clone(),
526 version: Some(version),
527 from_disk: false,
528 };
529 self.documents.write().await.insert(uri.clone(), entry);
530
531 self.update_diagnostics(uri, text).await;
533 }
534 }
535
536 async fn did_save(&self, params: DidSaveTextDocumentParams) {
537 let config_guard = self.config.read().await;
538 let enable_auto_fix = config_guard.enable_auto_fix;
539 drop(config_guard);
540
541 if enable_auto_fix && let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
543 let text = &entry.content;
544 match self.apply_all_fixes(¶ms.text_document.uri, text).await {
545 Ok(Some(fixed_text)) => {
546 let edit = TextEdit {
548 range: Range {
549 start: Position { line: 0, character: 0 },
550 end: self.get_end_position(text),
551 },
552 new_text: fixed_text.clone(),
553 };
554
555 let mut changes = std::collections::HashMap::new();
556 changes.insert(params.text_document.uri.clone(), vec![edit]);
557
558 let workspace_edit = WorkspaceEdit {
559 changes: Some(changes),
560 document_changes: None,
561 change_annotations: None,
562 };
563
564 match self.client.apply_edit(workspace_edit).await {
566 Ok(response) => {
567 if response.applied {
568 log::info!("Auto-fix applied successfully");
569 let entry = DocumentEntry {
571 content: fixed_text,
572 version: None, from_disk: false,
574 };
575 self.documents
576 .write()
577 .await
578 .insert(params.text_document.uri.clone(), entry);
579 } else {
580 log::warn!("Auto-fix was not applied: {:?}", response.failure_reason);
581 }
582 }
583 Err(e) => {
584 log::error!("Failed to apply auto-fix: {e}");
585 }
586 }
587 }
588 Ok(None) => {
589 log::debug!("No fixes to apply");
590 }
591 Err(e) => {
592 log::error!("Failed to generate fixes: {e}");
593 }
594 }
595 }
596
597 if let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
599 self.update_diagnostics(params.text_document.uri, entry.content.clone())
600 .await;
601 }
602 }
603
604 async fn did_close(&self, params: DidCloseTextDocumentParams) {
605 self.documents.write().await.remove(¶ms.text_document.uri);
607
608 self.client
610 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
611 .await;
612 }
613
614 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
615 let uri = params.text_document.uri;
616 let range = params.range;
617
618 if let Some(text) = self.get_document_content(&uri).await {
619 match self.get_code_actions(&uri, &text, range).await {
620 Ok(actions) => {
621 let response: Vec<CodeActionOrCommand> =
622 actions.into_iter().map(CodeActionOrCommand::CodeAction).collect();
623 Ok(Some(response))
624 }
625 Err(e) => {
626 log::error!("Failed to get code actions: {e}");
627 Ok(None)
628 }
629 }
630 } else {
631 Ok(None)
632 }
633 }
634
635 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
636 log::debug!(
641 "Range formatting requested for {:?}, formatting entire document due to rule interdependencies",
642 params.range
643 );
644
645 let formatting_params = DocumentFormattingParams {
646 text_document: params.text_document,
647 options: params.options,
648 work_done_progress_params: params.work_done_progress_params,
649 };
650
651 self.formatting(formatting_params).await
652 }
653
654 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
655 let uri = params.text_document.uri;
656
657 log::debug!("Formatting request for: {uri}");
658
659 if let Some(text) = self.get_document_content(&uri).await {
660 let config_guard = self.config.read().await;
662 let lsp_config = config_guard.clone();
663 drop(config_guard);
664
665 let rumdl_config = self.rumdl_config.read().await;
667 let all_rules = rules::all_rules(&rumdl_config);
668 let flavor = rumdl_config.markdown_flavor();
669
670 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
672
673 drop(rumdl_config);
674
675 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
677
678 match crate::lint(&text, &filtered_rules, false, flavor) {
680 Ok(warnings) => {
681 log::debug!(
682 "Found {} warnings, {} with fixes",
683 warnings.len(),
684 warnings.iter().filter(|w| w.fix.is_some()).count()
685 );
686
687 let has_fixes = warnings.iter().any(|w| w.fix.is_some());
688 if has_fixes {
689 match crate::utils::fix_utils::apply_warning_fixes(&text, &warnings) {
690 Ok(fixed_content) => {
691 if fixed_content != text {
692 log::debug!("Returning formatting edits");
693 let end_position = self.get_end_position(&text);
694 let edit = TextEdit {
695 range: Range {
696 start: Position { line: 0, character: 0 },
697 end: end_position,
698 },
699 new_text: fixed_content,
700 };
701 return Ok(Some(vec![edit]));
702 }
703 }
704 Err(e) => {
705 log::error!("Failed to apply fixes: {e}");
706 }
707 }
708 }
709 Ok(Some(Vec::new()))
710 }
711 Err(e) => {
712 log::error!("Failed to format document: {e}");
713 Ok(Some(Vec::new()))
714 }
715 }
716 } else {
717 log::warn!("Document not found: {uri}");
718 Ok(None)
719 }
720 }
721
722 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
723 let uri = params.text_document.uri;
724
725 if let Some(text) = self.get_document_content(&uri).await {
726 match self.lint_document(&uri, &text).await {
727 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
728 RelatedFullDocumentDiagnosticReport {
729 related_documents: None,
730 full_document_diagnostic_report: FullDocumentDiagnosticReport {
731 result_id: None,
732 items: diagnostics,
733 },
734 },
735 ))),
736 Err(e) => {
737 log::error!("Failed to get diagnostics: {e}");
738 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
739 RelatedFullDocumentDiagnosticReport {
740 related_documents: None,
741 full_document_diagnostic_report: FullDocumentDiagnosticReport {
742 result_id: None,
743 items: Vec::new(),
744 },
745 },
746 )))
747 }
748 }
749 } else {
750 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
751 RelatedFullDocumentDiagnosticReport {
752 related_documents: None,
753 full_document_diagnostic_report: FullDocumentDiagnosticReport {
754 result_id: None,
755 items: Vec::new(),
756 },
757 },
758 )))
759 }
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use crate::rule::LintWarning;
767 use tower_lsp::LspService;
768
769 fn create_test_server() -> RumdlLanguageServer {
770 let (service, _socket) = LspService::new(RumdlLanguageServer::new);
771 service.inner().clone()
772 }
773
774 #[tokio::test]
775 async fn test_server_creation() {
776 let server = create_test_server();
777
778 let config = server.config.read().await;
780 assert!(config.enable_linting);
781 assert!(!config.enable_auto_fix);
782 }
783
784 #[tokio::test]
785 async fn test_lint_document() {
786 let server = create_test_server();
787
788 let uri = Url::parse("file:///test.md").unwrap();
790 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
791
792 let diagnostics = server.lint_document(&uri, text).await.unwrap();
793
794 assert!(!diagnostics.is_empty());
796 assert!(diagnostics.iter().any(|d| d.message.contains("trailing")));
797 }
798
799 #[tokio::test]
800 async fn test_lint_document_disabled() {
801 let server = create_test_server();
802
803 server.config.write().await.enable_linting = false;
805
806 let uri = Url::parse("file:///test.md").unwrap();
807 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
808
809 let diagnostics = server.lint_document(&uri, text).await.unwrap();
810
811 assert!(diagnostics.is_empty());
813 }
814
815 #[tokio::test]
816 async fn test_get_code_actions() {
817 let server = create_test_server();
818
819 let uri = Url::parse("file:///test.md").unwrap();
820 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
821
822 let range = Range {
824 start: Position { line: 0, character: 0 },
825 end: Position { line: 3, character: 21 },
826 };
827
828 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
829
830 assert!(!actions.is_empty());
832 assert!(actions.iter().any(|a| a.title.contains("trailing")));
833 }
834
835 #[tokio::test]
836 async fn test_get_code_actions_outside_range() {
837 let server = create_test_server();
838
839 let uri = Url::parse("file:///test.md").unwrap();
840 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
841
842 let range = Range {
844 start: Position { line: 0, character: 0 },
845 end: Position { line: 0, character: 6 },
846 };
847
848 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
849
850 assert!(actions.is_empty());
852 }
853
854 #[tokio::test]
855 async fn test_document_storage() {
856 let server = create_test_server();
857
858 let uri = Url::parse("file:///test.md").unwrap();
859 let text = "# Test Document";
860
861 let entry = DocumentEntry {
863 content: text.to_string(),
864 version: Some(1),
865 from_disk: false,
866 };
867 server.documents.write().await.insert(uri.clone(), entry);
868
869 let stored = server.documents.read().await.get(&uri).map(|e| e.content.clone());
871 assert_eq!(stored, Some(text.to_string()));
872
873 server.documents.write().await.remove(&uri);
875
876 let stored = server.documents.read().await.get(&uri).cloned();
878 assert_eq!(stored, None);
879 }
880
881 #[tokio::test]
882 async fn test_configuration_loading() {
883 let server = create_test_server();
884
885 server.load_configuration(false).await;
887
888 let rumdl_config = server.rumdl_config.read().await;
891 drop(rumdl_config); }
894
895 #[tokio::test]
896 async fn test_load_config_for_lsp() {
897 let result = RumdlLanguageServer::load_config_for_lsp(None);
899 assert!(result.is_ok());
900
901 let result = RumdlLanguageServer::load_config_for_lsp(Some("/nonexistent/config.toml"));
903 assert!(result.is_err());
904 }
905
906 #[tokio::test]
907 async fn test_warning_conversion() {
908 let warning = LintWarning {
909 message: "Test warning".to_string(),
910 line: 1,
911 column: 1,
912 end_line: 1,
913 end_column: 10,
914 severity: crate::rule::Severity::Warning,
915 fix: None,
916 rule_name: Some("MD001"),
917 };
918
919 let diagnostic = warning_to_diagnostic(&warning);
921 assert_eq!(diagnostic.message, "Test warning");
922 assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::WARNING));
923 assert_eq!(diagnostic.code, Some(NumberOrString::String("MD001".to_string())));
924
925 let uri = Url::parse("file:///test.md").unwrap();
927 let action = warning_to_code_action(&warning, &uri, "Test content");
928 assert!(action.is_none());
929 }
930
931 #[tokio::test]
932 async fn test_multiple_documents() {
933 let server = create_test_server();
934
935 let uri1 = Url::parse("file:///test1.md").unwrap();
936 let uri2 = Url::parse("file:///test2.md").unwrap();
937 let text1 = "# Document 1";
938 let text2 = "# Document 2";
939
940 {
942 let mut docs = server.documents.write().await;
943 let entry1 = DocumentEntry {
944 content: text1.to_string(),
945 version: Some(1),
946 from_disk: false,
947 };
948 let entry2 = DocumentEntry {
949 content: text2.to_string(),
950 version: Some(1),
951 from_disk: false,
952 };
953 docs.insert(uri1.clone(), entry1);
954 docs.insert(uri2.clone(), entry2);
955 }
956
957 let docs = server.documents.read().await;
959 assert_eq!(docs.len(), 2);
960 assert_eq!(docs.get(&uri1).map(|s| s.content.as_str()), Some(text1));
961 assert_eq!(docs.get(&uri2).map(|s| s.content.as_str()), Some(text2));
962 }
963
964 #[tokio::test]
965 async fn test_auto_fix_on_save() {
966 let server = create_test_server();
967
968 {
970 let mut config = server.config.write().await;
971 config.enable_auto_fix = true;
972 }
973
974 let uri = Url::parse("file:///test.md").unwrap();
975 let text = "#Heading without space"; let entry = DocumentEntry {
979 content: text.to_string(),
980 version: Some(1),
981 from_disk: false,
982 };
983 server.documents.write().await.insert(uri.clone(), entry);
984
985 let fixed = server.apply_all_fixes(&uri, text).await.unwrap();
987 assert!(fixed.is_some());
988 assert_eq!(fixed.unwrap(), "# Heading without space");
989 }
990
991 #[tokio::test]
992 async fn test_get_end_position() {
993 let server = create_test_server();
994
995 let pos = server.get_end_position("Hello");
997 assert_eq!(pos.line, 0);
998 assert_eq!(pos.character, 5);
999
1000 let pos = server.get_end_position("Hello\nWorld\nTest");
1002 assert_eq!(pos.line, 2);
1003 assert_eq!(pos.character, 4);
1004
1005 let pos = server.get_end_position("");
1007 assert_eq!(pos.line, 0);
1008 assert_eq!(pos.character, 0);
1009
1010 let pos = server.get_end_position("Hello\n");
1012 assert_eq!(pos.line, 1);
1013 assert_eq!(pos.character, 0);
1014 }
1015
1016 #[tokio::test]
1017 async fn test_empty_document_handling() {
1018 let server = create_test_server();
1019
1020 let uri = Url::parse("file:///empty.md").unwrap();
1021 let text = "";
1022
1023 let diagnostics = server.lint_document(&uri, text).await.unwrap();
1025 assert!(diagnostics.is_empty());
1026
1027 let range = Range {
1029 start: Position { line: 0, character: 0 },
1030 end: Position { line: 0, character: 0 },
1031 };
1032 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
1033 assert!(actions.is_empty());
1034 }
1035
1036 #[tokio::test]
1037 async fn test_config_update() {
1038 let server = create_test_server();
1039
1040 {
1042 let mut config = server.config.write().await;
1043 config.enable_auto_fix = true;
1044 config.config_path = Some("/custom/path.toml".to_string());
1045 }
1046
1047 let config = server.config.read().await;
1049 assert!(config.enable_auto_fix);
1050 assert_eq!(config.config_path, Some("/custom/path.toml".to_string()));
1051 }
1052
1053 #[tokio::test]
1054 async fn test_document_formatting() {
1055 let server = create_test_server();
1056 let uri = Url::parse("file:///test.md").unwrap();
1057 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
1058
1059 let entry = DocumentEntry {
1061 content: text.to_string(),
1062 version: Some(1),
1063 from_disk: false,
1064 };
1065 server.documents.write().await.insert(uri.clone(), entry);
1066
1067 let params = DocumentFormattingParams {
1069 text_document: TextDocumentIdentifier { uri: uri.clone() },
1070 options: FormattingOptions {
1071 tab_size: 4,
1072 insert_spaces: true,
1073 properties: HashMap::new(),
1074 trim_trailing_whitespace: Some(true),
1075 insert_final_newline: Some(true),
1076 trim_final_newlines: Some(true),
1077 },
1078 work_done_progress_params: WorkDoneProgressParams::default(),
1079 };
1080
1081 let result = server.formatting(params).await.unwrap();
1083
1084 assert!(result.is_some());
1086 let edits = result.unwrap();
1087 assert!(!edits.is_empty());
1088
1089 let edit = &edits[0];
1091 let expected = "# Test\n\nThis is a test \nWith trailing spaces\n";
1094 assert_eq!(edit.new_text, expected);
1095 }
1096}