systemd_language_server/
lib.rs

1use ini::configparser::ini::Ini;
2use log::info;
3use std::collections::HashMap;
4use tower_lsp::jsonrpc::Result;
5use tower_lsp::lsp_types::*;
6use tower_lsp::{Client, LanguageServer};
7
8pub struct Backend {
9    client: Client,
10    // Store opened file contents
11    documents: HashMap<Url, String>,
12}
13
14impl Backend {
15    pub fn new(client: Client) -> Self {
16        Self {
17            client,
18            documents: HashMap::new(),
19        }
20    }
21
22    // Parse systemd unit file
23    fn parse_unit_file(&self, content: &str) -> anyhow::Result<Ini> {
24        let mut ini = Ini::new();
25        if let Err(e) = ini.read(content.to_string()) {
26            return Err(anyhow::anyhow!(e));
27        }
28        Ok(ini)
29    }
30
31    // Generate diagnostics
32    fn generate_diagnostics(&self, content: &str) -> Vec<Diagnostic> {
33        let mut diagnostics = Vec::new();
34
35        match self.parse_unit_file(content) {
36            Ok(_) => {
37                // File format is correct, no diagnostics needed
38            }
39            Err(e) => {
40                // Add syntax error diagnostic
41                let diagnostic = Diagnostic {
42                    range: Range {
43                        start: Position::new(0, 0),
44                        end: Position::new(0, 1),
45                    },
46                    severity: Some(DiagnosticSeverity::ERROR),
47                    code: None,
48                    code_description: None,
49                    source: Some("systemd-lsp".into()),
50                    message: format!("Systemd unit file syntax error: {}", e),
51                    related_information: None,
52                    tags: None,
53                    data: None,
54                };
55                diagnostics.push(diagnostic);
56            }
57        }
58
59        // Check for common systemd configuration errors
60        self.check_common_errors(content, &mut diagnostics);
61
62        diagnostics
63    }
64
65    // Check for common systemd configuration errors
66    fn check_common_errors(&self, content: &str, diagnostics: &mut Vec<Diagnostic>) {
67        let lines: Vec<&str> = content.lines().collect();
68
69        for (i, line) in lines.iter().enumerate() {
70            let line_num = i as u32;
71
72            // Check line format
73            if line.contains('=') && !line.trim().starts_with('#') && !line.trim().starts_with('[')
74            {
75                let parts: Vec<&str> = line.splitn(2, '=').collect();
76                if parts.len() == 2 {
77                    let key = parts[0].trim();
78                    let value = parts[1].trim();
79
80                    // Check for empty values
81                    if value.is_empty() {
82                        diagnostics.push(Diagnostic {
83                            range: Range {
84                                start: Position::new(line_num, 0),
85                                end: Position::new(line_num, line.len() as u32),
86                            },
87                            severity: Some(DiagnosticSeverity::WARNING),
88                            message: format!("Key '{}' has an empty value", key),
89                            source: Some("systemd-lsp".into()),
90                            ..Default::default()
91                        });
92                    }
93
94                    // Check for common configuration errors
95                    match key {
96                        "ExecStart" => {
97                            if !value.starts_with('/') && !value.starts_with('-') {
98                                diagnostics.push(Diagnostic {
99                                    range: Range {
100                                        start: Position::new(line_num, 0),
101                                        end: Position::new(line_num, line.len() as u32),
102                                    },
103                                    severity: Some(DiagnosticSeverity::WARNING),
104                                    message: "ExecStart should use absolute paths".to_string(),
105                                    source: Some("systemd-lsp".into()),
106                                    ..Default::default()
107                                });
108                            }
109                        }
110                        "Type" => {
111                            let valid_types =
112                                ["simple", "forking", "oneshot", "dbus", "notify", "idle"];
113                            if !valid_types.contains(&value) {
114                                diagnostics.push(Diagnostic {
115                                    range: Range {
116                                        start: Position::new(line_num, 0),
117                                        end: Position::new(line_num, line.len() as u32),
118                                    },
119                                    severity: Some(DiagnosticSeverity::ERROR),
120                                    message: format!(
121                                        "Invalid service type: '{}'. Valid types: {:?}",
122                                        value, valid_types
123                                    ),
124                                    source: Some("systemd-lsp".into()),
125                                    ..Default::default()
126                                });
127                            }
128                        }
129                        _ => {}
130                    }
131                }
132            }
133        }
134    }
135
136    // Get completion items
137    fn get_completion_items(&self, position: &Position, document_uri: &Url) -> Vec<CompletionItem> {
138        let mut items = Vec::new();
139
140        // Get current document content
141        if let Some(content) = self.documents.get(document_uri) {
142            let lines: Vec<&str> = content.lines().collect();
143
144            // Get current line
145            if let Some(line) = lines.get(position.line as usize) {
146                let line = *line;
147
148                // Check if currently in a section name
149                if line.trim().starts_with('[') && !line.contains(']') {
150                    // Provide section name completions
151                    items.extend(vec![
152                        CompletionItem::new_simple(
153                            "Unit]".into(),
154                            "Unit configuration section".into(),
155                        ),
156                        CompletionItem::new_simple(
157                            "Service]".into(),
158                            "Service configuration section".into(),
159                        ),
160                        CompletionItem::new_simple(
161                            "Install]".into(),
162                            "Install configuration section".into(),
163                        ),
164                        CompletionItem::new_simple(
165                            "Socket]".into(),
166                            "Socket configuration section".into(),
167                        ),
168                        CompletionItem::new_simple(
169                            "Mount]".into(),
170                            "Mount configuration section".into(),
171                        ),
172                        CompletionItem::new_simple(
173                            "Timer]".into(),
174                            "Timer configuration section".into(),
175                        ),
176                    ]);
177                } else {
178                    // Provide key completions based on current section
179                    let current_section = self.get_current_section(lines, position.line as usize);
180
181                    match current_section.as_deref() {
182                        Some("Unit") => {
183                            items.extend(vec![
184                                CompletionItem::new_simple(
185                                    "Description=".into(),
186                                    "Unit description".into(),
187                                ),
188                                CompletionItem::new_simple(
189                                    "Documentation=".into(),
190                                    "Documentation URL".into(),
191                                ),
192                                CompletionItem::new_simple(
193                                    "Requires=".into(),
194                                    "Strong dependencies".into(),
195                                ),
196                                CompletionItem::new_simple(
197                                    "Wants=".into(),
198                                    "Weak dependencies".into(),
199                                ),
200                                CompletionItem::new_simple(
201                                    "After=".into(),
202                                    "Start order dependency".into(),
203                                ),
204                                CompletionItem::new_simple(
205                                    "Before=".into(),
206                                    "Start order dependency".into(),
207                                ),
208                                CompletionItem::new_simple(
209                                    "Conflicts=".into(),
210                                    "Conflicting units".into(),
211                                ),
212                            ]);
213                        }
214                        Some("Service") => {
215                            items.extend(vec![
216                                CompletionItem::new_simple("Type=".into(), "Service type".into()),
217                                CompletionItem::new_simple(
218                                    "ExecStart=".into(),
219                                    "Start command".into(),
220                                ),
221                                CompletionItem::new_simple(
222                                    "ExecStop=".into(),
223                                    "Stop command".into(),
224                                ),
225                                CompletionItem::new_simple(
226                                    "Restart=".into(),
227                                    "Restart policy".into(),
228                                ),
229                                CompletionItem::new_simple(
230                                    "RestartSec=".into(),
231                                    "Restart interval".into(),
232                                ),
233                                CompletionItem::new_simple("User=".into(), "Run as user".into()),
234                                CompletionItem::new_simple("Group=".into(), "Run as group".into()),
235                                CompletionItem::new_simple(
236                                    "WorkingDirectory=".into(),
237                                    "Working directory".into(),
238                                ),
239                            ]);
240                        }
241                        Some("Install") => {
242                            items.extend(vec![
243                                CompletionItem::new_simple(
244                                    "WantedBy=".into(),
245                                    "Wanted by targets".into(),
246                                ),
247                                CompletionItem::new_simple(
248                                    "RequiredBy=".into(),
249                                    "Required by targets".into(),
250                                ),
251                                CompletionItem::new_simple("Alias=".into(), "Unit alias".into()),
252                            ]);
253                        }
254                        Some("Socket") => {
255                            items.extend(vec![
256                                CompletionItem::new_simple(
257                                    "ListenStream=".into(),
258                                    "Listen on TCP port".into(),
259                                ),
260                                CompletionItem::new_simple(
261                                    "ListenDatagram=".into(),
262                                    "Listen on UDP port".into(),
263                                ),
264                                CompletionItem::new_simple(
265                                    "Accept=".into(),
266                                    "Accept connections".into(),
267                                ),
268                            ]);
269                        }
270                        Some("Timer") => {
271                            items.extend(vec![
272                                CompletionItem::new_simple(
273                                    "OnBootSec=".into(),
274                                    "Delay after boot".into(),
275                                ),
276                                CompletionItem::new_simple(
277                                    "OnUnitActiveSec=".into(),
278                                    "Delay after unit activation".into(),
279                                ),
280                                CompletionItem::new_simple(
281                                    "OnCalendar=".into(),
282                                    "Calendar-based trigger".into(),
283                                ),
284                            ]);
285                        }
286                        _ => {
287                            // Default to providing all section names
288                            items.extend(vec![
289                                CompletionItem::new_simple(
290                                    "[Unit]".into(),
291                                    "Unit configuration section".into(),
292                                ),
293                                CompletionItem::new_simple(
294                                    "[Service]".into(),
295                                    "Service configuration section".into(),
296                                ),
297                                CompletionItem::new_simple(
298                                    "[Install]".into(),
299                                    "Install configuration section".into(),
300                                ),
301                                CompletionItem::new_simple(
302                                    "[Socket]".into(),
303                                    "Socket configuration section".into(),
304                                ),
305                                CompletionItem::new_simple(
306                                    "[Mount]".into(),
307                                    "Mount configuration section".into(),
308                                ),
309                                CompletionItem::new_simple(
310                                    "[Timer]".into(),
311                                    "Timer configuration section".into(),
312                                ),
313                            ]);
314                        }
315                    }
316                }
317            }
318        }
319
320        items
321    }
322
323    // Get current section
324    fn get_current_section(&self, lines: Vec<&str>, current_line: usize) -> Option<String> {
325        let mut current_section = None;
326
327        for (i, line) in lines.iter().enumerate() {
328            if i > current_line {
329                break;
330            }
331
332            let line = line.trim();
333            if line.starts_with('[') && line.ends_with(']') {
334                let section = line[1..line.len() - 1].to_string();
335                current_section = Some(section);
336            }
337        }
338
339        current_section
340    }
341
342    // Get hover information
343    fn get_hover_info(&self, position: &Position, document_uri: &Url) -> Option<Hover> {
344        if let Some(content) = self.documents.get(document_uri) {
345            let lines: Vec<&str> = content.lines().collect();
346
347            if let Some(line) = lines.get(position.line as usize) {
348                let line = *line;
349
350                // Check if hovering over a section name
351                if line.trim().starts_with('[') && line.trim().ends_with(']') {
352                    let section = line.trim()[1..line.trim().len() - 1].to_string();
353
354                    let hover_text = match section.as_str() {
355                        "Unit" => {
356                            "The Unit section contains basic information about the unit, such as description and dependencies."
357                        }
358                        "Service" => {
359                            "The Service section contains service configuration, such as start commands and restart policies."
360                        }
361                        "Install" => {
362                            "The Install section contains installation information, such as which targets want this unit."
363                        }
364                        "Socket" => {
365                            "The Socket section contains socket configuration, such as listening addresses and ports."
366                        }
367                        "Mount" => "The Mount section contains mount point configuration.",
368                        "Timer" => {
369                            "The Timer section contains timer configuration, used for scheduled service activation."
370                        }
371                        _ => return None,
372                    };
373
374                    return Some(Hover {
375                        contents: HoverContents::Markup(MarkupContent {
376                            kind: MarkupKind::Markdown,
377                            value: hover_text.to_string(),
378                        }),
379                        range: Some(Range {
380                            start: Position::new(position.line, 0),
381                            end: Position::new(position.line, line.len() as u32),
382                        }),
383                    });
384                }
385
386                // Check if hovering over a key-value pair
387                if line.contains('=') && !line.trim().starts_with('#') {
388                    let parts: Vec<&str> = line.splitn(2, '=').collect();
389                    if parts.len() == 2 {
390                        let key = parts[0].trim();
391
392                        // Provide hover information based on key
393                        let hover_text = match key {
394                            "Description" => "Describes the unit's function and purpose.",
395                            "After" => {
396                                "Defines start order, this unit will start after the specified units."
397                            }
398                            "Before" => {
399                                "Defines start order, this unit will start before the specified units."
400                            }
401                            "Requires" => {
402                                "Strong dependency relationship, if the dependency fails, this unit will also fail."
403                            }
404                            "Wants" => {
405                                "Weak dependency relationship, dependency failure won't affect this unit."
406                            }
407                            "ExecStart" => {
408                                "Defines the command to execute when the service starts. Should use absolute paths."
409                            }
410                            "ExecStop" => "Defines the command to execute when the service stops.",
411                            "Type" => {
412                                "Defines the service type, can be simple, forking, oneshot, dbus, notify, or idle."
413                            }
414                            "Restart" => "Defines the restart policy when the service exits.",
415                            "WantedBy" => {
416                                "Specifies which targets want this unit, used for enabling the unit."
417                            }
418                            _ => return None,
419                        };
420
421                        return Some(Hover {
422                            contents: HoverContents::Markup(MarkupContent {
423                                kind: MarkupKind::Markdown,
424                                value: hover_text.to_string(),
425                            }),
426                            range: Some(Range {
427                                start: Position::new(position.line, 0),
428                                end: Position::new(position.line, key.len() as u32),
429                            }),
430                        });
431                    }
432                }
433            }
434        }
435
436        None
437    }
438}
439
440#[tower_lsp::async_trait]
441impl LanguageServer for Backend {
442    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
443        info!("Systemd Language Server initialized");
444
445        Ok(InitializeResult {
446            capabilities: ServerCapabilities {
447                text_document_sync: Some(TextDocumentSyncCapability::Kind(
448                    TextDocumentSyncKind::FULL,
449                )),
450                completion_provider: Some(CompletionOptions {
451                    resolve_provider: Some(false),
452                    trigger_characters: Some(vec!["[".to_string(), "=".to_string()]),
453                    ..Default::default()
454                }),
455                hover_provider: Some(HoverProviderCapability::Simple(true)),
456                ..Default::default()
457            },
458            server_info: Some(ServerInfo {
459                name: "systemd-language-server".to_string(),
460                version: Some("0.1.0".to_string()),
461            }),
462        })
463    }
464
465    async fn initialized(&self, _: InitializedParams) {
466        info!("Systemd Language Server is ready");
467
468        self.client
469            .log_message(MessageType::INFO, "Systemd Language Server has started")
470            .await;
471    }
472
473    async fn shutdown(&self) -> Result<()> {
474        info!("Systemd Language Server is shutting down");
475        Ok(())
476    }
477
478    async fn did_open(&self, params: DidOpenTextDocumentParams) {
479        info!("File opened: {:?}", params.text_document.uri);
480
481        // Store document content
482        let mut documents = self.documents.clone();
483        documents.insert(
484            params.text_document.uri.clone(),
485            params.text_document.text.clone(),
486        );
487
488        // Generate diagnostics
489        let diagnostics = self.generate_diagnostics(&params.text_document.text);
490
491        // Publish diagnostics
492        self.client
493            .publish_diagnostics(params.text_document.uri, diagnostics, None)
494            .await;
495    }
496
497    async fn did_change(&self, params: DidChangeTextDocumentParams) {
498        info!("File changed: {:?}", params.text_document.uri);
499
500        if let Some(change) = params.content_changes.get(0) {
501            // Update document content
502            let mut documents = self.documents.clone();
503            documents.insert(params.text_document.uri.clone(), change.text.clone());
504
505            // Generate diagnostics
506            let diagnostics = self.generate_diagnostics(&change.text);
507
508            // Publish diagnostics
509            self.client
510                .publish_diagnostics(params.text_document.uri.clone(), diagnostics, None)
511                .await;
512        }
513    }
514
515    async fn did_close(&self, params: DidCloseTextDocumentParams) {
516        info!("File closed: {:?}", params.text_document.uri);
517
518        // Remove document content
519        let mut documents = self.documents.clone();
520        documents.remove(&params.text_document.uri);
521
522        // Clear diagnostics
523        self.client
524            .publish_diagnostics(params.text_document.uri, vec![], None)
525            .await;
526    }
527
528    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
529        let position = params.text_document_position.position;
530        let document_uri = params.text_document_position.text_document.uri;
531
532        let items = self.get_completion_items(&position, &document_uri);
533
534        Ok(Some(CompletionResponse::Array(items)))
535    }
536
537    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
538        let position = params.text_document_position_params.position;
539        let document_uri = params.text_document_position_params.text_document.uri;
540
541        Ok(self.get_hover_info(&position, &document_uri))
542    }
543}
544
545// Export public function for testing
546pub fn parse_unit_file(content: &str) -> anyhow::Result<Ini> {
547    let mut ini = Ini::new();
548    if let Err(e) = ini.read(content.to_string()) {
549        return Err(anyhow::anyhow!(e));
550    }
551    Ok(ini)
552}