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