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)]
28pub struct RumdlLanguageServer {
29 client: Client,
30 config: Arc<RwLock<RumdlLspConfig>>,
32 rumdl_config: Arc<RwLock<Config>>,
34 documents: Arc<RwLock<HashMap<Url, String>>>,
36}
37
38impl RumdlLanguageServer {
39 pub fn new(client: Client) -> Self {
40 Self {
41 client,
42 config: Arc::new(RwLock::new(RumdlLspConfig::default())),
43 rumdl_config: Arc::new(RwLock::new(Config::default())),
44 documents: Arc::new(RwLock::new(HashMap::new())),
45 }
46 }
47
48 fn apply_lsp_config_overrides(
50 &self,
51 mut filtered_rules: Vec<Box<dyn Rule>>,
52 lsp_config: &RumdlLspConfig,
53 ) -> Vec<Box<dyn Rule>> {
54 if let Some(enable) = &lsp_config.enable_rules
56 && !enable.is_empty()
57 {
58 let enable_set: std::collections::HashSet<String> = enable.iter().cloned().collect();
59 filtered_rules.retain(|rule| enable_set.contains(rule.name()));
60 }
61
62 if let Some(disable) = &lsp_config.disable_rules
64 && !disable.is_empty()
65 {
66 let disable_set: std::collections::HashSet<String> = disable.iter().cloned().collect();
67 filtered_rules.retain(|rule| !disable_set.contains(rule.name()));
68 }
69
70 filtered_rules
71 }
72
73 async fn lint_document(&self, uri: &Url, text: &str) -> Result<Vec<Diagnostic>> {
75 let config_guard = self.config.read().await;
76
77 if !config_guard.enable_linting {
79 return Ok(Vec::new());
80 }
81
82 let lsp_config = config_guard.clone();
83 drop(config_guard); let rumdl_config = self.rumdl_config.read().await;
87 let all_rules = rules::all_rules(&rumdl_config);
88 let flavor = rumdl_config.markdown_flavor();
89
90 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
92 drop(rumdl_config); filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
96
97 match crate::lint(text, &filtered_rules, false, flavor) {
99 Ok(warnings) => {
100 let diagnostics = warnings.iter().map(warning_to_diagnostic).collect();
101 Ok(diagnostics)
102 }
103 Err(e) => {
104 log::error!("Failed to lint document {uri}: {e}");
105 Ok(Vec::new())
106 }
107 }
108 }
109
110 async fn update_diagnostics(&self, uri: Url, text: String) {
112 match self.lint_document(&uri, &text).await {
113 Ok(diagnostics) => {
114 self.client.publish_diagnostics(uri, diagnostics, None).await;
115 }
116 Err(e) => {
117 log::error!("Failed to update diagnostics: {e}");
118 }
119 }
120 }
121
122 async fn apply_all_fixes(&self, _uri: &Url, text: &str) -> Result<Option<String>> {
124 let config_guard = self.config.read().await;
125 let lsp_config = config_guard.clone();
126 drop(config_guard);
127
128 let rumdl_config = self.rumdl_config.read().await;
129 let all_rules = rules::all_rules(&rumdl_config);
130 let flavor = rumdl_config.markdown_flavor();
131
132 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
134 drop(rumdl_config);
135
136 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
138
139 let mut fixed_text = text.to_string();
141 let mut any_changes = false;
142
143 for rule in &filtered_rules {
144 let ctx = crate::lint_context::LintContext::new(&fixed_text, flavor);
145 match rule.fix(&ctx) {
146 Ok(new_text) => {
147 if new_text != fixed_text {
148 fixed_text = new_text;
149 any_changes = true;
150 }
151 }
152 Err(e) => {
153 log::warn!("Failed to apply fix for rule {}: {}", rule.name(), e);
154 }
155 }
156 }
157
158 if any_changes { Ok(Some(fixed_text)) } else { Ok(None) }
159 }
160
161 fn get_end_position(&self, text: &str) -> Position {
163 let lines: Vec<&str> = text.lines().collect();
164 let line = lines.len().saturating_sub(1) as u32;
165 let character = lines.last().map_or(0, |l| l.len() as u32);
166 Position { line, character }
167 }
168
169 async fn get_code_actions(&self, uri: &Url, text: &str, range: Range) -> Result<Vec<CodeAction>> {
171 let config_guard = self.config.read().await;
172 let lsp_config = config_guard.clone();
173 drop(config_guard);
174
175 let rumdl_config = self.rumdl_config.read().await;
176 let all_rules = rules::all_rules(&rumdl_config);
177 let flavor = rumdl_config.markdown_flavor();
178
179 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
181 drop(rumdl_config);
182
183 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
185
186 match crate::lint(text, &filtered_rules, false, flavor) {
187 Ok(warnings) => {
188 let mut actions = Vec::new();
189 let mut fixable_count = 0;
190
191 for warning in &warnings {
192 let warning_line = (warning.line.saturating_sub(1)) as u32;
194 if warning_line >= range.start.line
195 && warning_line <= range.end.line
196 && let Some(action) = warning_to_code_action(warning, uri, text)
197 {
198 actions.push(action);
199 if warning.fix.is_some() {
200 fixable_count += 1;
201 }
202 }
203 }
204
205 if fixable_count > 1 {
207 let total_fixable = warnings.iter().filter(|w| w.fix.is_some()).count();
209
210 if let Ok(fixed_content) = crate::utils::fix_utils::apply_warning_fixes(text, &warnings)
211 && fixed_content != text
212 {
213 let mut line = 0u32;
215 let mut character = 0u32;
216 for ch in text.chars() {
217 if ch == '\n' {
218 line += 1;
219 character = 0;
220 } else {
221 character += 1;
222 }
223 }
224
225 let fix_all_action = CodeAction {
226 title: format!("Fix all rumdl issues ({total_fixable} fixable)"),
227 kind: Some(CodeActionKind::QUICKFIX),
228 diagnostics: Some(Vec::new()),
229 edit: Some(WorkspaceEdit {
230 changes: Some(
231 [(
232 uri.clone(),
233 vec![TextEdit {
234 range: Range {
235 start: Position { line: 0, character: 0 },
236 end: Position { line, character },
237 },
238 new_text: fixed_content,
239 }],
240 )]
241 .into_iter()
242 .collect(),
243 ),
244 ..Default::default()
245 }),
246 command: None,
247 is_preferred: Some(true),
248 disabled: None,
249 data: None,
250 };
251
252 actions.insert(0, fix_all_action);
254 }
255 }
256
257 Ok(actions)
258 }
259 Err(e) => {
260 log::error!("Failed to get code actions: {e}");
261 Ok(Vec::new())
262 }
263 }
264 }
265
266 async fn load_configuration(&self, notify_client: bool) {
268 let config_guard = self.config.read().await;
269 let explicit_config_path = config_guard.config_path.clone();
270 drop(config_guard);
271
272 match Self::load_config_for_lsp(explicit_config_path.as_deref()) {
274 Ok(sourced_config) => {
275 let loaded_files = sourced_config.loaded_files.clone();
276 *self.rumdl_config.write().await = sourced_config.into();
277
278 if !loaded_files.is_empty() {
279 let message = format!("Loaded rumdl config from: {}", loaded_files.join(", "));
280 log::info!("{message}");
281 if notify_client {
282 self.client.log_message(MessageType::INFO, &message).await;
283 }
284 } else {
285 log::info!("Using default rumdl configuration (no config files found)");
286 }
287 }
288 Err(e) => {
289 let message = format!("Failed to load rumdl config: {e}");
290 log::warn!("{message}");
291 if notify_client {
292 self.client.log_message(MessageType::WARNING, &message).await;
293 }
294 *self.rumdl_config.write().await = crate::config::Config::default();
296 }
297 }
298 }
299
300 async fn reload_configuration(&self) {
302 self.load_configuration(true).await;
303 }
304
305 fn load_config_for_lsp(
307 config_path: Option<&str>,
308 ) -> Result<crate::config::SourcedConfig, crate::config::ConfigError> {
309 crate::config::SourcedConfig::load_with_discovery(config_path, None, false)
311 }
312}
313
314#[tower_lsp::async_trait]
315impl LanguageServer for RumdlLanguageServer {
316 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
317 log::info!("Initializing rumdl Language Server");
318
319 if let Some(options) = params.initialization_options
321 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
322 {
323 *self.config.write().await = config;
324 }
325
326 self.load_configuration(false).await;
328
329 Ok(InitializeResult {
330 capabilities: ServerCapabilities {
331 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
332 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
333 document_formatting_provider: Some(OneOf::Left(true)),
334 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
335 identifier: Some("rumdl".to_string()),
336 inter_file_dependencies: false,
337 workspace_diagnostics: false,
338 work_done_progress_options: WorkDoneProgressOptions::default(),
339 })),
340 workspace: Some(WorkspaceServerCapabilities {
341 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
342 supported: Some(true),
343 change_notifications: Some(OneOf::Left(true)),
344 }),
345 file_operations: None,
346 }),
347 ..Default::default()
348 },
349 server_info: Some(ServerInfo {
350 name: "rumdl".to_string(),
351 version: Some(env!("CARGO_PKG_VERSION").to_string()),
352 }),
353 })
354 }
355
356 async fn initialized(&self, _: InitializedParams) {
357 log::info!("rumdl Language Server initialized");
358
359 self.client
360 .log_message(MessageType::INFO, "rumdl Language Server started")
361 .await;
362 }
363
364 async fn did_change_workspace_folders(&self, _params: DidChangeWorkspaceFoldersParams) {
365 self.reload_configuration().await;
367 }
368
369 async fn shutdown(&self) -> JsonRpcResult<()> {
370 log::info!("Shutting down rumdl Language Server");
371 Ok(())
372 }
373
374 async fn did_open(&self, params: DidOpenTextDocumentParams) {
375 let uri = params.text_document.uri;
376 let text = params.text_document.text;
377
378 self.documents.write().await.insert(uri.clone(), text.clone());
380
381 self.update_diagnostics(uri, text).await;
383 }
384
385 async fn did_change(&self, params: DidChangeTextDocumentParams) {
386 let uri = params.text_document.uri;
387
388 if let Some(change) = params.content_changes.into_iter().next() {
390 let text = change.text;
391
392 self.documents.write().await.insert(uri.clone(), text.clone());
394
395 self.update_diagnostics(uri, text).await;
397 }
398 }
399
400 async fn did_save(&self, params: DidSaveTextDocumentParams) {
401 let config_guard = self.config.read().await;
402 let enable_auto_fix = config_guard.enable_auto_fix;
403 drop(config_guard);
404
405 if enable_auto_fix && let Some(text) = self.documents.read().await.get(¶ms.text_document.uri) {
407 match self.apply_all_fixes(¶ms.text_document.uri, text).await {
408 Ok(Some(fixed_text)) => {
409 let edit = TextEdit {
411 range: Range {
412 start: Position { line: 0, character: 0 },
413 end: self.get_end_position(text),
414 },
415 new_text: fixed_text.clone(),
416 };
417
418 let mut changes = std::collections::HashMap::new();
419 changes.insert(params.text_document.uri.clone(), vec![edit]);
420
421 let workspace_edit = WorkspaceEdit {
422 changes: Some(changes),
423 document_changes: None,
424 change_annotations: None,
425 };
426
427 match self.client.apply_edit(workspace_edit).await {
429 Ok(response) => {
430 if response.applied {
431 log::info!("Auto-fix applied successfully");
432 self.documents
434 .write()
435 .await
436 .insert(params.text_document.uri.clone(), fixed_text);
437 } else {
438 log::warn!("Auto-fix was not applied: {:?}", response.failure_reason);
439 }
440 }
441 Err(e) => {
442 log::error!("Failed to apply auto-fix: {e}");
443 }
444 }
445 }
446 Ok(None) => {
447 log::debug!("No fixes to apply");
448 }
449 Err(e) => {
450 log::error!("Failed to generate fixes: {e}");
451 }
452 }
453 }
454
455 if let Some(text) = self.documents.read().await.get(¶ms.text_document.uri) {
457 self.update_diagnostics(params.text_document.uri, text.clone()).await;
458 }
459 }
460
461 async fn did_close(&self, params: DidCloseTextDocumentParams) {
462 self.documents.write().await.remove(¶ms.text_document.uri);
464
465 self.client
467 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
468 .await;
469 }
470
471 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
472 let uri = params.text_document.uri;
473 let range = params.range;
474
475 if let Some(text) = self.documents.read().await.get(&uri) {
476 match self.get_code_actions(&uri, text, range).await {
477 Ok(actions) => {
478 let response: Vec<CodeActionOrCommand> =
479 actions.into_iter().map(CodeActionOrCommand::CodeAction).collect();
480 Ok(Some(response))
481 }
482 Err(e) => {
483 log::error!("Failed to get code actions: {e}");
484 Ok(None)
485 }
486 }
487 } else {
488 Ok(None)
489 }
490 }
491
492 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
493 let uri = params.text_document.uri;
494
495 if let Some(text) = self.documents.read().await.get(&uri) {
496 let rumdl_config = self.rumdl_config.read().await;
498 let all_rules = rules::all_rules(&rumdl_config);
499 let flavor = rumdl_config.markdown_flavor();
500
501 let filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
503 drop(rumdl_config);
504
505 match crate::lint(text, &filtered_rules, false, flavor) {
507 Ok(warnings) => {
508 let has_fixes = warnings.iter().any(|w| w.fix.is_some());
510
511 if has_fixes {
512 match crate::utils::fix_utils::apply_warning_fixes(text, &warnings) {
514 Ok(fixed_content) => {
515 if fixed_content != *text {
517 let mut line = 0u32;
520 let mut character = 0u32;
521
522 for ch in text.chars() {
523 if ch == '\n' {
524 line += 1;
525 character = 0;
526 } else {
527 character += 1;
528 }
529 }
530
531 let edit = TextEdit {
532 range: Range {
533 start: Position { line: 0, character: 0 },
534 end: Position { line, character },
535 },
536 new_text: fixed_content,
537 };
538
539 return Ok(Some(vec![edit]));
540 }
541 }
542 Err(e) => {
543 log::error!("Failed to apply fixes: {e}");
544 }
545 }
546 }
547
548 Ok(None)
550 }
551 Err(e) => {
552 log::error!("Failed to format document: {e}");
553 Ok(None)
554 }
555 }
556 } else {
557 Ok(None)
558 }
559 }
560
561 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
562 let uri = params.text_document.uri;
563
564 if let Some(text) = self.documents.read().await.get(&uri) {
565 match self.lint_document(&uri, text).await {
566 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
567 RelatedFullDocumentDiagnosticReport {
568 related_documents: None,
569 full_document_diagnostic_report: FullDocumentDiagnosticReport {
570 result_id: None,
571 items: diagnostics,
572 },
573 },
574 ))),
575 Err(e) => {
576 log::error!("Failed to get diagnostics: {e}");
577 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
578 RelatedFullDocumentDiagnosticReport {
579 related_documents: None,
580 full_document_diagnostic_report: FullDocumentDiagnosticReport {
581 result_id: None,
582 items: Vec::new(),
583 },
584 },
585 )))
586 }
587 }
588 } else {
589 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
590 RelatedFullDocumentDiagnosticReport {
591 related_documents: None,
592 full_document_diagnostic_report: FullDocumentDiagnosticReport {
593 result_id: None,
594 items: Vec::new(),
595 },
596 },
597 )))
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::rule::LintWarning;
606 use tower_lsp::LspService;
607
608 fn create_test_server() -> RumdlLanguageServer {
609 let (service, _socket) = LspService::new(RumdlLanguageServer::new);
610 service.inner().clone()
611 }
612
613 #[tokio::test]
614 async fn test_server_creation() {
615 let server = create_test_server();
616
617 let config = server.config.read().await;
619 assert!(config.enable_linting);
620 assert!(!config.enable_auto_fix);
621 }
622
623 #[tokio::test]
624 async fn test_lint_document() {
625 let server = create_test_server();
626
627 let uri = Url::parse("file:///test.md").unwrap();
629 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
630
631 let diagnostics = server.lint_document(&uri, text).await.unwrap();
632
633 assert!(!diagnostics.is_empty());
635 assert!(diagnostics.iter().any(|d| d.message.contains("trailing")));
636 }
637
638 #[tokio::test]
639 async fn test_lint_document_disabled() {
640 let server = create_test_server();
641
642 server.config.write().await.enable_linting = false;
644
645 let uri = Url::parse("file:///test.md").unwrap();
646 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
647
648 let diagnostics = server.lint_document(&uri, text).await.unwrap();
649
650 assert!(diagnostics.is_empty());
652 }
653
654 #[tokio::test]
655 async fn test_get_code_actions() {
656 let server = create_test_server();
657
658 let uri = Url::parse("file:///test.md").unwrap();
659 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
660
661 let range = Range {
663 start: Position { line: 0, character: 0 },
664 end: Position { line: 3, character: 21 },
665 };
666
667 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
668
669 assert!(!actions.is_empty());
671 assert!(actions.iter().any(|a| a.title.contains("trailing")));
672 }
673
674 #[tokio::test]
675 async fn test_get_code_actions_outside_range() {
676 let server = create_test_server();
677
678 let uri = Url::parse("file:///test.md").unwrap();
679 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
680
681 let range = Range {
683 start: Position { line: 0, character: 0 },
684 end: Position { line: 0, character: 6 },
685 };
686
687 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
688
689 assert!(actions.is_empty());
691 }
692
693 #[tokio::test]
694 async fn test_document_storage() {
695 let server = create_test_server();
696
697 let uri = Url::parse("file:///test.md").unwrap();
698 let text = "# Test Document";
699
700 server.documents.write().await.insert(uri.clone(), text.to_string());
702
703 let stored = server.documents.read().await.get(&uri).cloned();
705 assert_eq!(stored, Some(text.to_string()));
706
707 server.documents.write().await.remove(&uri);
709
710 let stored = server.documents.read().await.get(&uri).cloned();
712 assert_eq!(stored, None);
713 }
714
715 #[tokio::test]
716 async fn test_configuration_loading() {
717 let server = create_test_server();
718
719 server.load_configuration(false).await;
721
722 let rumdl_config = server.rumdl_config.read().await;
725 drop(rumdl_config); }
728
729 #[tokio::test]
730 async fn test_load_config_for_lsp() {
731 let result = RumdlLanguageServer::load_config_for_lsp(None);
733 assert!(result.is_ok());
734
735 let result = RumdlLanguageServer::load_config_for_lsp(Some("/nonexistent/config.toml"));
737 assert!(result.is_err());
738 }
739
740 #[tokio::test]
741 async fn test_warning_conversion() {
742 let warning = LintWarning {
743 message: "Test warning".to_string(),
744 line: 1,
745 column: 1,
746 end_line: 1,
747 end_column: 10,
748 severity: crate::rule::Severity::Warning,
749 fix: None,
750 rule_name: Some("MD001"),
751 };
752
753 let diagnostic = warning_to_diagnostic(&warning);
755 assert_eq!(diagnostic.message, "Test warning");
756 assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::WARNING));
757 assert_eq!(diagnostic.code, Some(NumberOrString::String("MD001".to_string())));
758
759 let uri = Url::parse("file:///test.md").unwrap();
761 let action = warning_to_code_action(&warning, &uri, "Test content");
762 assert!(action.is_none());
763 }
764
765 #[tokio::test]
766 async fn test_multiple_documents() {
767 let server = create_test_server();
768
769 let uri1 = Url::parse("file:///test1.md").unwrap();
770 let uri2 = Url::parse("file:///test2.md").unwrap();
771 let text1 = "# Document 1";
772 let text2 = "# Document 2";
773
774 {
776 let mut docs = server.documents.write().await;
777 docs.insert(uri1.clone(), text1.to_string());
778 docs.insert(uri2.clone(), text2.to_string());
779 }
780
781 let docs = server.documents.read().await;
783 assert_eq!(docs.len(), 2);
784 assert_eq!(docs.get(&uri1).map(|s| s.as_str()), Some(text1));
785 assert_eq!(docs.get(&uri2).map(|s| s.as_str()), Some(text2));
786 }
787
788 #[tokio::test]
789 async fn test_auto_fix_on_save() {
790 let server = create_test_server();
791
792 {
794 let mut config = server.config.write().await;
795 config.enable_auto_fix = true;
796 }
797
798 let uri = Url::parse("file:///test.md").unwrap();
799 let text = "#Heading without space"; server.documents.write().await.insert(uri.clone(), text.to_string());
803
804 let fixed = server.apply_all_fixes(&uri, text).await.unwrap();
806 assert!(fixed.is_some());
807 assert_eq!(fixed.unwrap(), "# Heading without space");
808 }
809
810 #[tokio::test]
811 async fn test_get_end_position() {
812 let server = create_test_server();
813
814 let pos = server.get_end_position("Hello");
816 assert_eq!(pos.line, 0);
817 assert_eq!(pos.character, 5);
818
819 let pos = server.get_end_position("Hello\nWorld\nTest");
821 assert_eq!(pos.line, 2);
822 assert_eq!(pos.character, 4);
823
824 let pos = server.get_end_position("");
826 assert_eq!(pos.line, 0);
827 assert_eq!(pos.character, 0);
828
829 let pos = server.get_end_position("Hello\n");
831 assert_eq!(pos.line, 0);
832 assert_eq!(pos.character, 5);
833 }
834
835 #[tokio::test]
836 async fn test_empty_document_handling() {
837 let server = create_test_server();
838
839 let uri = Url::parse("file:///empty.md").unwrap();
840 let text = "";
841
842 let diagnostics = server.lint_document(&uri, text).await.unwrap();
844 assert!(diagnostics.is_empty());
845
846 let range = Range {
848 start: Position { line: 0, character: 0 },
849 end: Position { line: 0, character: 0 },
850 };
851 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
852 assert!(actions.is_empty());
853 }
854
855 #[tokio::test]
856 async fn test_config_update() {
857 let server = create_test_server();
858
859 {
861 let mut config = server.config.write().await;
862 config.enable_auto_fix = true;
863 config.config_path = Some("/custom/path.toml".to_string());
864 }
865
866 let config = server.config.read().await;
868 assert!(config.enable_auto_fix);
869 assert_eq!(config.config_path, Some("/custom/path.toml".to_string()));
870 }
871
872 #[tokio::test]
873 async fn test_document_formatting() {
874 let server = create_test_server();
875 let uri = Url::parse("file:///test.md").unwrap();
876 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
877
878 server.documents.write().await.insert(uri.clone(), text.to_string());
880
881 let params = DocumentFormattingParams {
883 text_document: TextDocumentIdentifier { uri: uri.clone() },
884 options: FormattingOptions {
885 tab_size: 4,
886 insert_spaces: true,
887 properties: HashMap::new(),
888 trim_trailing_whitespace: Some(true),
889 insert_final_newline: Some(true),
890 trim_final_newlines: Some(true),
891 },
892 work_done_progress_params: WorkDoneProgressParams::default(),
893 };
894
895 let result = server.formatting(params).await.unwrap();
897
898 assert!(result.is_some());
900 let edits = result.unwrap();
901 assert!(!edits.is_empty());
902
903 let edit = &edits[0];
905 let expected = "# Test\n\nThis is a test \nWith trailing spaces\n";
908 assert_eq!(edit.new_text, expected);
909 }
910}