Skip to main content

rumdl_lib/lsp/
mod.rs

1//! Language Server Protocol implementation for rumdl
2//!
3//! This module provides a Language Server Protocol (LSP) implementation for rumdl,
4//! enabling real-time markdown linting in editors and IDEs.
5//!
6//! Following Ruff's approach, this is built directly into the main rumdl binary
7//! and can be started with `rumdl server`.
8
9pub mod index_worker;
10pub mod server;
11pub mod types;
12
13pub use server::RumdlLanguageServer;
14pub use types::{RumdlLspConfig, warning_to_code_actions, warning_to_diagnostic};
15
16use anyhow::Result;
17use tokio::net::TcpListener;
18use tower_lsp::{LspService, Server};
19
20/// Start the Language Server Protocol server
21/// This is the main entry point for `rumdl server`
22pub async fn start_server(config_path: Option<&str>) -> Result<()> {
23    let stdin = tokio::io::stdin();
24    let stdout = tokio::io::stdout();
25
26    let (service, socket) = LspService::new(|client| RumdlLanguageServer::new(client, config_path));
27
28    log::info!("Starting rumdl Language Server Protocol server");
29
30    Server::new(stdin, stdout, socket).serve(service).await;
31
32    Ok(())
33}
34
35/// Start the LSP server over TCP (useful for debugging)
36pub async fn start_tcp_server(port: u16, config_path: Option<&str>) -> Result<()> {
37    let listener = TcpListener::bind(format!("127.0.0.1:{port}")).await?;
38    log::info!("rumdl LSP server listening on 127.0.0.1:{port}");
39
40    // Clone config_path to owned String so we can move it into the spawned task
41    let config_path_owned = config_path.map(|s| s.to_string());
42
43    loop {
44        let (stream, _) = listener.accept().await?;
45        let config_path_clone = config_path_owned.clone();
46        let (service, socket) =
47            LspService::new(move |client| RumdlLanguageServer::new(client, config_path_clone.as_deref()));
48
49        tokio::spawn(async move {
50            let (read, write) = tokio::io::split(stream);
51            Server::new(read, write, socket).serve(service).await;
52        });
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_module_exports() {
62        // Verify that the module exports are accessible
63        // This ensures the public API is stable
64        fn _check_exports() {
65            // These should compile without errors
66            let _server_type: RumdlLanguageServer;
67            let _config_type: RumdlLspConfig;
68            let _func1: fn(&crate::rule::LintWarning) -> tower_lsp::lsp_types::Diagnostic = warning_to_diagnostic;
69            let _func2: fn(
70                &crate::rule::LintWarning,
71                &tower_lsp::lsp_types::Url,
72                &str,
73            ) -> Vec<tower_lsp::lsp_types::CodeAction> = warning_to_code_actions;
74        }
75    }
76
77    #[tokio::test]
78    async fn test_tcp_server_bind() {
79        use std::net::TcpListener as StdTcpListener;
80
81        // Find an available port
82        let listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
83        let port = listener.local_addr().unwrap().port();
84        drop(listener);
85
86        // Start the server in a background task
87        let server_handle = tokio::spawn(async move {
88            // Server should start without panicking
89            match tokio::time::timeout(std::time::Duration::from_millis(100), start_tcp_server(port, None)).await {
90                Ok(Ok(())) => {} // Server started and stopped normally
91                Ok(Err(_)) => {} // Server had an error (expected in test)
92                Err(_) => {}     // Timeout (expected - server runs forever)
93            }
94        });
95
96        // Give the server time to start
97        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
98
99        // Try to connect to verify it's listening
100        match tokio::time::timeout(
101            std::time::Duration::from_millis(50),
102            tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")),
103        )
104        .await
105        {
106            Ok(Ok(_)) => {
107                // Successfully connected
108            }
109            _ => {
110                // Connection failed or timed out - that's okay for this test
111            }
112        }
113
114        // Cancel the server task
115        server_handle.abort();
116    }
117
118    #[tokio::test]
119    async fn test_tcp_server_invalid_port() {
120        // Port 0 is technically valid (OS assigns), but let's test a privileged port
121        // that we likely can't bind to without root
122        let result = tokio::time::timeout(std::time::Duration::from_millis(100), start_tcp_server(80, None)).await;
123
124        match result {
125            Ok(Err(_)) => {
126                // Expected - should fail to bind to privileged port
127            }
128            Ok(Ok(())) => {
129                panic!("Should not be able to bind to port 80 without privileges");
130            }
131            Err(_) => {
132                // Timeout - server tried to run, which means bind succeeded
133                // This might happen if tests are run as root
134            }
135        }
136    }
137
138    #[tokio::test]
139    async fn test_service_creation() {
140        // Test that we can create the LSP service
141        let (service, _socket) = LspService::new(|client| RumdlLanguageServer::new(client, None));
142
143        // Service should be created successfully
144        // We can't easily test more without a full LSP client
145        drop(service);
146    }
147
148    #[tokio::test]
149    async fn test_multiple_tcp_connections() {
150        use std::net::TcpListener as StdTcpListener;
151
152        // Find an available port
153        let listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
154        let port = listener.local_addr().unwrap().port();
155        drop(listener);
156
157        // Start the server
158        let server_handle = tokio::spawn(async move {
159            let _ = tokio::time::timeout(std::time::Duration::from_millis(500), start_tcp_server(port, None)).await;
160        });
161
162        // Give server time to start
163        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
164
165        // Try multiple connections
166        let mut handles = vec![];
167        for _ in 0..3 {
168            let handle = tokio::spawn(async move {
169                match tokio::time::timeout(
170                    std::time::Duration::from_millis(100),
171                    tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")),
172                )
173                .await
174                {
175                    Ok(Ok(_stream)) => {
176                        // Connection successful
177                        true
178                    }
179                    _ => false,
180                }
181            });
182            handles.push(handle);
183        }
184
185        // Wait for all connections
186        for handle in handles {
187            let _ = handle.await;
188        }
189
190        // Clean up
191        server_handle.abort();
192    }
193
194    #[test]
195    fn test_logging_initialization() {
196        // Verify that starting the server includes proper logging
197        // This is more of a smoke test to ensure logging statements compile
198
199        // The actual log::info! calls are in the async functions,
200        // but we can at least verify the module imports and uses logging
201        let _info_level = log::Level::Info;
202    }
203}