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 mut line = 0u32;
164 let mut character = 0u32;
165
166 for ch in text.chars() {
167 if ch == '\n' {
168 line += 1;
169 character = 0;
170 } else {
171 character += 1;
172 }
173 }
174
175 Position { line, character }
176 }
177
178 async fn get_code_actions(&self, uri: &Url, text: &str, range: Range) -> Result<Vec<CodeAction>> {
180 let config_guard = self.config.read().await;
181 let lsp_config = config_guard.clone();
182 drop(config_guard);
183
184 let rumdl_config = self.rumdl_config.read().await;
185 let all_rules = rules::all_rules(&rumdl_config);
186 let flavor = rumdl_config.markdown_flavor();
187
188 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
190 drop(rumdl_config);
191
192 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
194
195 match crate::lint(text, &filtered_rules, false, flavor) {
196 Ok(warnings) => {
197 let mut actions = Vec::new();
198 let mut fixable_count = 0;
199
200 for warning in &warnings {
201 let warning_line = (warning.line.saturating_sub(1)) as u32;
203 if warning_line >= range.start.line
204 && warning_line <= range.end.line
205 && let Some(action) = warning_to_code_action(warning, uri, text)
206 {
207 actions.push(action);
208 if warning.fix.is_some() {
209 fixable_count += 1;
210 }
211 }
212 }
213
214 if fixable_count > 1 {
216 let total_fixable = warnings.iter().filter(|w| w.fix.is_some()).count();
218
219 if let Ok(fixed_content) = crate::utils::fix_utils::apply_warning_fixes(text, &warnings)
220 && fixed_content != text
221 {
222 let mut line = 0u32;
224 let mut character = 0u32;
225 for ch in text.chars() {
226 if ch == '\n' {
227 line += 1;
228 character = 0;
229 } else {
230 character += 1;
231 }
232 }
233
234 let fix_all_action = CodeAction {
235 title: format!("Fix all rumdl issues ({total_fixable} fixable)"),
236 kind: Some(CodeActionKind::QUICKFIX),
237 diagnostics: Some(Vec::new()),
238 edit: Some(WorkspaceEdit {
239 changes: Some(
240 [(
241 uri.clone(),
242 vec![TextEdit {
243 range: Range {
244 start: Position { line: 0, character: 0 },
245 end: Position { line, character },
246 },
247 new_text: fixed_content,
248 }],
249 )]
250 .into_iter()
251 .collect(),
252 ),
253 ..Default::default()
254 }),
255 command: None,
256 is_preferred: Some(true),
257 disabled: None,
258 data: None,
259 };
260
261 actions.insert(0, fix_all_action);
263 }
264 }
265
266 Ok(actions)
267 }
268 Err(e) => {
269 log::error!("Failed to get code actions: {e}");
270 Ok(Vec::new())
271 }
272 }
273 }
274
275 async fn load_configuration(&self, notify_client: bool) {
277 let config_guard = self.config.read().await;
278 let explicit_config_path = config_guard.config_path.clone();
279 drop(config_guard);
280
281 match Self::load_config_for_lsp(explicit_config_path.as_deref()) {
283 Ok(sourced_config) => {
284 let loaded_files = sourced_config.loaded_files.clone();
285 *self.rumdl_config.write().await = sourced_config.into();
286
287 if !loaded_files.is_empty() {
288 let message = format!("Loaded rumdl config from: {}", loaded_files.join(", "));
289 log::info!("{message}");
290 if notify_client {
291 self.client.log_message(MessageType::INFO, &message).await;
292 }
293 } else {
294 log::info!("Using default rumdl configuration (no config files found)");
295 }
296 }
297 Err(e) => {
298 let message = format!("Failed to load rumdl config: {e}");
299 log::warn!("{message}");
300 if notify_client {
301 self.client.log_message(MessageType::WARNING, &message).await;
302 }
303 *self.rumdl_config.write().await = crate::config::Config::default();
305 }
306 }
307 }
308
309 async fn reload_configuration(&self) {
311 self.load_configuration(true).await;
312 }
313
314 fn load_config_for_lsp(
316 config_path: Option<&str>,
317 ) -> Result<crate::config::SourcedConfig, crate::config::ConfigError> {
318 crate::config::SourcedConfig::load_with_discovery(config_path, None, false)
320 }
321}
322
323#[tower_lsp::async_trait]
324impl LanguageServer for RumdlLanguageServer {
325 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
326 log::info!("Initializing rumdl Language Server");
327
328 if let Some(options) = params.initialization_options
330 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
331 {
332 *self.config.write().await = config;
333 }
334
335 self.load_configuration(false).await;
337
338 Ok(InitializeResult {
339 capabilities: ServerCapabilities {
340 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
341 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
342 document_formatting_provider: Some(OneOf::Left(true)),
343 document_range_formatting_provider: Some(OneOf::Left(true)),
344 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
345 identifier: Some("rumdl".to_string()),
346 inter_file_dependencies: false,
347 workspace_diagnostics: false,
348 work_done_progress_options: WorkDoneProgressOptions::default(),
349 })),
350 workspace: Some(WorkspaceServerCapabilities {
351 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
352 supported: Some(true),
353 change_notifications: Some(OneOf::Left(true)),
354 }),
355 file_operations: None,
356 }),
357 ..Default::default()
358 },
359 server_info: Some(ServerInfo {
360 name: "rumdl".to_string(),
361 version: Some(env!("CARGO_PKG_VERSION").to_string()),
362 }),
363 })
364 }
365
366 async fn initialized(&self, _: InitializedParams) {
367 log::info!("rumdl Language Server initialized");
368
369 self.client
370 .log_message(MessageType::INFO, "rumdl Language Server started")
371 .await;
372 }
373
374 async fn did_change_workspace_folders(&self, _params: DidChangeWorkspaceFoldersParams) {
375 self.reload_configuration().await;
377 }
378
379 async fn shutdown(&self) -> JsonRpcResult<()> {
380 log::info!("Shutting down rumdl Language Server");
381 Ok(())
382 }
383
384 async fn did_open(&self, params: DidOpenTextDocumentParams) {
385 let uri = params.text_document.uri;
386 let text = params.text_document.text;
387
388 self.documents.write().await.insert(uri.clone(), text.clone());
390
391 self.update_diagnostics(uri, text).await;
393 }
394
395 async fn did_change(&self, params: DidChangeTextDocumentParams) {
396 let uri = params.text_document.uri;
397
398 if let Some(change) = params.content_changes.into_iter().next() {
400 let text = change.text;
401
402 self.documents.write().await.insert(uri.clone(), text.clone());
404
405 self.update_diagnostics(uri, text).await;
407 }
408 }
409
410 async fn did_save(&self, params: DidSaveTextDocumentParams) {
411 let config_guard = self.config.read().await;
412 let enable_auto_fix = config_guard.enable_auto_fix;
413 drop(config_guard);
414
415 if enable_auto_fix && let Some(text) = self.documents.read().await.get(¶ms.text_document.uri) {
417 match self.apply_all_fixes(¶ms.text_document.uri, text).await {
418 Ok(Some(fixed_text)) => {
419 let edit = TextEdit {
421 range: Range {
422 start: Position { line: 0, character: 0 },
423 end: self.get_end_position(text),
424 },
425 new_text: fixed_text.clone(),
426 };
427
428 let mut changes = std::collections::HashMap::new();
429 changes.insert(params.text_document.uri.clone(), vec![edit]);
430
431 let workspace_edit = WorkspaceEdit {
432 changes: Some(changes),
433 document_changes: None,
434 change_annotations: None,
435 };
436
437 match self.client.apply_edit(workspace_edit).await {
439 Ok(response) => {
440 if response.applied {
441 log::info!("Auto-fix applied successfully");
442 self.documents
444 .write()
445 .await
446 .insert(params.text_document.uri.clone(), fixed_text);
447 } else {
448 log::warn!("Auto-fix was not applied: {:?}", response.failure_reason);
449 }
450 }
451 Err(e) => {
452 log::error!("Failed to apply auto-fix: {e}");
453 }
454 }
455 }
456 Ok(None) => {
457 log::debug!("No fixes to apply");
458 }
459 Err(e) => {
460 log::error!("Failed to generate fixes: {e}");
461 }
462 }
463 }
464
465 if let Some(text) = self.documents.read().await.get(¶ms.text_document.uri) {
467 self.update_diagnostics(params.text_document.uri, text.clone()).await;
468 }
469 }
470
471 async fn did_close(&self, params: DidCloseTextDocumentParams) {
472 self.documents.write().await.remove(¶ms.text_document.uri);
474
475 self.client
477 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
478 .await;
479 }
480
481 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
482 let uri = params.text_document.uri;
483 let range = params.range;
484
485 if let Some(text) = self.documents.read().await.get(&uri) {
486 match self.get_code_actions(&uri, text, range).await {
487 Ok(actions) => {
488 let response: Vec<CodeActionOrCommand> =
489 actions.into_iter().map(CodeActionOrCommand::CodeAction).collect();
490 Ok(Some(response))
491 }
492 Err(e) => {
493 log::error!("Failed to get code actions: {e}");
494 Ok(None)
495 }
496 }
497 } else {
498 Ok(None)
499 }
500 }
501
502 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
503 log::debug!(
508 "Range formatting requested for {:?}, formatting entire document due to rule interdependencies",
509 params.range
510 );
511
512 let formatting_params = DocumentFormattingParams {
513 text_document: params.text_document,
514 options: params.options,
515 work_done_progress_params: params.work_done_progress_params,
516 };
517
518 self.formatting(formatting_params).await
519 }
520
521 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
522 let uri = params.text_document.uri;
523
524 log::debug!("Formatting request for: {uri}");
525
526 if let Some(text) = self.documents.read().await.get(&uri) {
527 let config_guard = self.config.read().await;
529 let lsp_config = config_guard.clone();
530 drop(config_guard);
531
532 let rumdl_config = self.rumdl_config.read().await;
534 let all_rules = rules::all_rules(&rumdl_config);
535 let flavor = rumdl_config.markdown_flavor();
536
537 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
539 drop(rumdl_config);
540
541 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
543
544 match crate::lint(text, &filtered_rules, false, flavor) {
546 Ok(warnings) => {
547 log::debug!(
548 "Found {} warnings, {} with fixes",
549 warnings.len(),
550 warnings.iter().filter(|w| w.fix.is_some()).count()
551 );
552
553 let has_fixes = warnings.iter().any(|w| w.fix.is_some());
555
556 if has_fixes {
557 match crate::utils::fix_utils::apply_warning_fixes(text, &warnings) {
559 Ok(fixed_content) => {
560 if fixed_content != *text {
562 log::debug!("Returning formatting edits");
563 let mut line = 0u32;
566 let mut character = 0u32;
567
568 for ch in text.chars() {
569 if ch == '\n' {
570 line += 1;
571 character = 0;
572 } else {
573 character += 1;
574 }
575 }
576
577 let edit = TextEdit {
578 range: Range {
579 start: Position { line: 0, character: 0 },
580 end: Position { line, character },
581 },
582 new_text: fixed_content,
583 };
584
585 return Ok(Some(vec![edit]));
586 }
587 }
588 Err(e) => {
589 log::error!("Failed to apply fixes: {e}");
590 }
591 }
592 } else {
593 log::debug!("No fixes available for formatting");
594 }
595
596 Ok(Some(Vec::new()))
598 }
599 Err(e) => {
600 log::error!("Failed to format document: {e}");
601 Ok(Some(Vec::new()))
603 }
604 }
605 } else {
606 log::warn!("Document not found in cache: {uri}");
607 Ok(Some(Vec::new()))
609 }
610 }
611
612 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
613 let uri = params.text_document.uri;
614
615 if let Some(text) = self.documents.read().await.get(&uri) {
616 match self.lint_document(&uri, text).await {
617 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
618 RelatedFullDocumentDiagnosticReport {
619 related_documents: None,
620 full_document_diagnostic_report: FullDocumentDiagnosticReport {
621 result_id: None,
622 items: diagnostics,
623 },
624 },
625 ))),
626 Err(e) => {
627 log::error!("Failed to get diagnostics: {e}");
628 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
629 RelatedFullDocumentDiagnosticReport {
630 related_documents: None,
631 full_document_diagnostic_report: FullDocumentDiagnosticReport {
632 result_id: None,
633 items: Vec::new(),
634 },
635 },
636 )))
637 }
638 }
639 } else {
640 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
641 RelatedFullDocumentDiagnosticReport {
642 related_documents: None,
643 full_document_diagnostic_report: FullDocumentDiagnosticReport {
644 result_id: None,
645 items: Vec::new(),
646 },
647 },
648 )))
649 }
650 }
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use crate::rule::LintWarning;
657 use tower_lsp::LspService;
658
659 fn create_test_server() -> RumdlLanguageServer {
660 let (service, _socket) = LspService::new(RumdlLanguageServer::new);
661 service.inner().clone()
662 }
663
664 #[tokio::test]
665 async fn test_server_creation() {
666 let server = create_test_server();
667
668 let config = server.config.read().await;
670 assert!(config.enable_linting);
671 assert!(!config.enable_auto_fix);
672 }
673
674 #[tokio::test]
675 async fn test_lint_document() {
676 let server = create_test_server();
677
678 let uri = Url::parse("file:///test.md").unwrap();
680 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
681
682 let diagnostics = server.lint_document(&uri, text).await.unwrap();
683
684 assert!(!diagnostics.is_empty());
686 assert!(diagnostics.iter().any(|d| d.message.contains("trailing")));
687 }
688
689 #[tokio::test]
690 async fn test_lint_document_disabled() {
691 let server = create_test_server();
692
693 server.config.write().await.enable_linting = false;
695
696 let uri = Url::parse("file:///test.md").unwrap();
697 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
698
699 let diagnostics = server.lint_document(&uri, text).await.unwrap();
700
701 assert!(diagnostics.is_empty());
703 }
704
705 #[tokio::test]
706 async fn test_get_code_actions() {
707 let server = create_test_server();
708
709 let uri = Url::parse("file:///test.md").unwrap();
710 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
711
712 let range = Range {
714 start: Position { line: 0, character: 0 },
715 end: Position { line: 3, character: 21 },
716 };
717
718 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
719
720 assert!(!actions.is_empty());
722 assert!(actions.iter().any(|a| a.title.contains("trailing")));
723 }
724
725 #[tokio::test]
726 async fn test_get_code_actions_outside_range() {
727 let server = create_test_server();
728
729 let uri = Url::parse("file:///test.md").unwrap();
730 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
731
732 let range = Range {
734 start: Position { line: 0, character: 0 },
735 end: Position { line: 0, character: 6 },
736 };
737
738 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
739
740 assert!(actions.is_empty());
742 }
743
744 #[tokio::test]
745 async fn test_document_storage() {
746 let server = create_test_server();
747
748 let uri = Url::parse("file:///test.md").unwrap();
749 let text = "# Test Document";
750
751 server.documents.write().await.insert(uri.clone(), text.to_string());
753
754 let stored = server.documents.read().await.get(&uri).cloned();
756 assert_eq!(stored, Some(text.to_string()));
757
758 server.documents.write().await.remove(&uri);
760
761 let stored = server.documents.read().await.get(&uri).cloned();
763 assert_eq!(stored, None);
764 }
765
766 #[tokio::test]
767 async fn test_configuration_loading() {
768 let server = create_test_server();
769
770 server.load_configuration(false).await;
772
773 let rumdl_config = server.rumdl_config.read().await;
776 drop(rumdl_config); }
779
780 #[tokio::test]
781 async fn test_load_config_for_lsp() {
782 let result = RumdlLanguageServer::load_config_for_lsp(None);
784 assert!(result.is_ok());
785
786 let result = RumdlLanguageServer::load_config_for_lsp(Some("/nonexistent/config.toml"));
788 assert!(result.is_err());
789 }
790
791 #[tokio::test]
792 async fn test_warning_conversion() {
793 let warning = LintWarning {
794 message: "Test warning".to_string(),
795 line: 1,
796 column: 1,
797 end_line: 1,
798 end_column: 10,
799 severity: crate::rule::Severity::Warning,
800 fix: None,
801 rule_name: Some("MD001"),
802 };
803
804 let diagnostic = warning_to_diagnostic(&warning);
806 assert_eq!(diagnostic.message, "Test warning");
807 assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::WARNING));
808 assert_eq!(diagnostic.code, Some(NumberOrString::String("MD001".to_string())));
809
810 let uri = Url::parse("file:///test.md").unwrap();
812 let action = warning_to_code_action(&warning, &uri, "Test content");
813 assert!(action.is_none());
814 }
815
816 #[tokio::test]
817 async fn test_multiple_documents() {
818 let server = create_test_server();
819
820 let uri1 = Url::parse("file:///test1.md").unwrap();
821 let uri2 = Url::parse("file:///test2.md").unwrap();
822 let text1 = "# Document 1";
823 let text2 = "# Document 2";
824
825 {
827 let mut docs = server.documents.write().await;
828 docs.insert(uri1.clone(), text1.to_string());
829 docs.insert(uri2.clone(), text2.to_string());
830 }
831
832 let docs = server.documents.read().await;
834 assert_eq!(docs.len(), 2);
835 assert_eq!(docs.get(&uri1).map(|s| s.as_str()), Some(text1));
836 assert_eq!(docs.get(&uri2).map(|s| s.as_str()), Some(text2));
837 }
838
839 #[tokio::test]
840 async fn test_auto_fix_on_save() {
841 let server = create_test_server();
842
843 {
845 let mut config = server.config.write().await;
846 config.enable_auto_fix = true;
847 }
848
849 let uri = Url::parse("file:///test.md").unwrap();
850 let text = "#Heading without space"; server.documents.write().await.insert(uri.clone(), text.to_string());
854
855 let fixed = server.apply_all_fixes(&uri, text).await.unwrap();
857 assert!(fixed.is_some());
858 assert_eq!(fixed.unwrap(), "# Heading without space");
859 }
860
861 #[tokio::test]
862 async fn test_get_end_position() {
863 let server = create_test_server();
864
865 let pos = server.get_end_position("Hello");
867 assert_eq!(pos.line, 0);
868 assert_eq!(pos.character, 5);
869
870 let pos = server.get_end_position("Hello\nWorld\nTest");
872 assert_eq!(pos.line, 2);
873 assert_eq!(pos.character, 4);
874
875 let pos = server.get_end_position("");
877 assert_eq!(pos.line, 0);
878 assert_eq!(pos.character, 0);
879
880 let pos = server.get_end_position("Hello\n");
882 assert_eq!(pos.line, 1);
883 assert_eq!(pos.character, 0);
884 }
885
886 #[tokio::test]
887 async fn test_empty_document_handling() {
888 let server = create_test_server();
889
890 let uri = Url::parse("file:///empty.md").unwrap();
891 let text = "";
892
893 let diagnostics = server.lint_document(&uri, text).await.unwrap();
895 assert!(diagnostics.is_empty());
896
897 let range = Range {
899 start: Position { line: 0, character: 0 },
900 end: Position { line: 0, character: 0 },
901 };
902 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
903 assert!(actions.is_empty());
904 }
905
906 #[tokio::test]
907 async fn test_config_update() {
908 let server = create_test_server();
909
910 {
912 let mut config = server.config.write().await;
913 config.enable_auto_fix = true;
914 config.config_path = Some("/custom/path.toml".to_string());
915 }
916
917 let config = server.config.read().await;
919 assert!(config.enable_auto_fix);
920 assert_eq!(config.config_path, Some("/custom/path.toml".to_string()));
921 }
922
923 #[tokio::test]
924 async fn test_document_formatting() {
925 let server = create_test_server();
926 let uri = Url::parse("file:///test.md").unwrap();
927 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
928
929 server.documents.write().await.insert(uri.clone(), text.to_string());
931
932 let params = DocumentFormattingParams {
934 text_document: TextDocumentIdentifier { uri: uri.clone() },
935 options: FormattingOptions {
936 tab_size: 4,
937 insert_spaces: true,
938 properties: HashMap::new(),
939 trim_trailing_whitespace: Some(true),
940 insert_final_newline: Some(true),
941 trim_final_newlines: Some(true),
942 },
943 work_done_progress_params: WorkDoneProgressParams::default(),
944 };
945
946 let result = server.formatting(params).await.unwrap();
948
949 assert!(result.is_some());
951 let edits = result.unwrap();
952 assert!(!edits.is_empty());
953
954 let edit = &edits[0];
956 let expected = "# Test\n\nThis is a test \nWith trailing spaces\n";
959 assert_eq!(edit.new_text, expected);
960 }
961}