Skip to main content

voirs_cli/lsp/
mod.rs

1//! Language Server Protocol (LSP) implementation for VoiRS
2//!
3//! Provides IDE integration for SSML editing, voice management, and synthesis configuration.
4//! Supports features like autocompletion, diagnostics, hover information, and code actions.
5
6pub mod capabilities;
7pub mod code_actions;
8pub mod completion;
9pub mod diagnostics;
10pub mod formatting;
11pub mod hover;
12pub mod protocol;
13pub mod server;
14pub mod text_document;
15
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::sync::Arc;
19use tokio::sync::RwLock;
20
21pub use server::LspServer;
22
23/// LSP server state
24pub struct LspState {
25    /// Open text documents
26    pub documents: Arc<RwLock<HashMap<String, TextDocument>>>,
27
28    /// Available voices cache
29    pub voices: Arc<RwLock<Vec<VoiceInfo>>>,
30
31    /// Server capabilities
32    pub capabilities: ServerCapabilities,
33}
34
35impl LspState {
36    /// Create a new LSP state
37    pub fn new() -> Self {
38        Self {
39            documents: Arc::new(RwLock::new(HashMap::new())),
40            voices: Arc::new(RwLock::new(Vec::new())),
41            capabilities: ServerCapabilities::default(),
42        }
43    }
44
45    /// Open a document
46    pub async fn open_document(&self, uri: String, text: String, language_id: String) {
47        let document = TextDocument::new(uri.clone(), text, language_id);
48        self.documents.write().await.insert(uri, document);
49    }
50
51    /// Update a document
52    pub async fn update_document(&self, uri: &str, text: String, version: i32) {
53        if let Some(doc) = self.documents.write().await.get_mut(uri) {
54            doc.update(text, version);
55        }
56    }
57
58    /// Close a document
59    pub async fn close_document(&self, uri: &str) {
60        self.documents.write().await.remove(uri);
61    }
62
63    /// Get a document
64    pub async fn get_document(&self, uri: &str) -> Option<TextDocument> {
65        self.documents.read().await.get(uri).cloned()
66    }
67}
68
69impl Default for LspState {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75/// Text document representation
76#[derive(Debug, Clone)]
77pub struct TextDocument {
78    /// Document URI
79    pub uri: String,
80
81    /// Document text content
82    pub text: String,
83
84    /// Language identifier (ssml, voirs-config, etc.)
85    pub language_id: String,
86
87    /// Document version
88    pub version: i32,
89}
90
91impl TextDocument {
92    /// Create a new text document
93    pub fn new(uri: String, text: String, language_id: String) -> Self {
94        Self {
95            uri,
96            text,
97            language_id,
98            version: 1,
99        }
100    }
101
102    /// Update document content
103    pub fn update(&mut self, text: String, version: i32) {
104        self.text = text;
105        self.version = version;
106    }
107
108    /// Get line count
109    pub fn line_count(&self) -> usize {
110        self.text.lines().count()
111    }
112
113    /// Get line at position
114    pub fn get_line(&self, line: usize) -> Option<&str> {
115        self.text.lines().nth(line)
116    }
117
118    /// Get character at position
119    pub fn get_char_at(&self, line: usize, character: usize) -> Option<char> {
120        self.get_line(line)?.chars().nth(character)
121    }
122}
123
124/// Voice information for completion
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct VoiceInfo {
127    /// Voice ID
128    pub id: String,
129
130    /// Voice name
131    pub name: String,
132
133    /// Language code
134    pub language: String,
135
136    /// Voice description
137    pub description: String,
138
139    /// Supported features
140    pub features: Vec<String>,
141}
142
143/// Server capabilities
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ServerCapabilities {
146    /// Text document sync kind
147    pub text_document_sync: TextDocumentSyncKind,
148
149    /// Supports completion
150    pub completion_provider: bool,
151
152    /// Supports hover
153    pub hover_provider: bool,
154
155    /// Supports diagnostics
156    pub diagnostic_provider: bool,
157
158    /// Supports code actions
159    pub code_action_provider: bool,
160}
161
162impl Default for ServerCapabilities {
163    fn default() -> Self {
164        Self {
165            text_document_sync: TextDocumentSyncKind::Full,
166            completion_provider: true,
167            hover_provider: true,
168            diagnostic_provider: true,
169            code_action_provider: true,
170        }
171    }
172}
173
174/// Text document sync kind
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176pub enum TextDocumentSyncKind {
177    /// Documents should not be synced
178    None,
179
180    /// Documents are synced by sending full content
181    Full,
182
183    /// Documents are synced by sending incremental updates
184    Incremental,
185}
186
187/// Position in a text document
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
189pub struct Position {
190    /// Line number (0-indexed)
191    pub line: u32,
192
193    /// Character offset (0-indexed)
194    pub character: u32,
195}
196
197impl Position {
198    /// Create a new position
199    pub fn new(line: u32, character: u32) -> Self {
200        Self { line, character }
201    }
202}
203
204/// Range in a text document
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206pub struct Range {
207    /// Start position
208    pub start: Position,
209
210    /// End position (exclusive)
211    pub end: Position,
212}
213
214impl Range {
215    /// Create a new range
216    pub fn new(start: Position, end: Position) -> Self {
217        Self { start, end }
218    }
219
220    /// Create a single-line range
221    pub fn single_line(line: u32, start_char: u32, end_char: u32) -> Self {
222        Self {
223            start: Position::new(line, start_char),
224            end: Position::new(line, end_char),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[tokio::test]
234    async fn test_lsp_state_creation() {
235        let state = LspState::new();
236        assert!(state.documents.read().await.is_empty());
237    }
238
239    #[tokio::test]
240    async fn test_open_document() {
241        let state = LspState::new();
242        state
243            .open_document(
244                "file:///test.ssml".to_string(),
245                "<speak>Hello</speak>".to_string(),
246                "ssml".to_string(),
247            )
248            .await;
249
250        let docs = state.documents.read().await;
251        assert_eq!(docs.len(), 1);
252        assert!(docs.contains_key("file:///test.ssml"));
253    }
254
255    #[tokio::test]
256    async fn test_update_document() {
257        let state = LspState::new();
258        state
259            .open_document(
260                "file:///test.ssml".to_string(),
261                "<speak>Hello</speak>".to_string(),
262                "ssml".to_string(),
263            )
264            .await;
265
266        state
267            .update_document("file:///test.ssml", "<speak>World</speak>".to_string(), 2)
268            .await;
269
270        let doc = state.get_document("file:///test.ssml").await.unwrap();
271        assert_eq!(doc.text, "<speak>World</speak>");
272        assert_eq!(doc.version, 2);
273    }
274
275    #[tokio::test]
276    async fn test_close_document() {
277        let state = LspState::new();
278        state
279            .open_document(
280                "file:///test.ssml".to_string(),
281                "<speak>Hello</speak>".to_string(),
282                "ssml".to_string(),
283            )
284            .await;
285
286        state.close_document("file:///test.ssml").await;
287
288        assert!(state.get_document("file:///test.ssml").await.is_none());
289    }
290
291    #[test]
292    fn test_text_document_line_count() {
293        let doc = TextDocument::new(
294            "file:///test.txt".to_string(),
295            "line 1\nline 2\nline 3".to_string(),
296            "text".to_string(),
297        );
298        assert_eq!(doc.line_count(), 3);
299    }
300
301    #[test]
302    fn test_position_creation() {
303        let pos = Position::new(5, 10);
304        assert_eq!(pos.line, 5);
305        assert_eq!(pos.character, 10);
306    }
307
308    #[test]
309    fn test_range_creation() {
310        let range = Range::single_line(3, 5, 15);
311        assert_eq!(range.start.line, 3);
312        assert_eq!(range.start.character, 5);
313        assert_eq!(range.end.line, 3);
314        assert_eq!(range.end.character, 15);
315    }
316
317    #[test]
318    fn test_server_capabilities_default() {
319        let caps = ServerCapabilities::default();
320        assert!(caps.completion_provider);
321        assert!(caps.hover_provider);
322        assert!(caps.diagnostic_provider);
323        assert_eq!(caps.text_document_sync, TextDocumentSyncKind::Full);
324    }
325}