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 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 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 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 }
39 Err(e) => {
40 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 self.check_common_errors(content, &mut diagnostics);
61
62 diagnostics
63 }
64
65 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 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 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 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 fn get_completion_items(&self, position: &Position, document_uri: &Url) -> Vec<CompletionItem> {
138 let mut items = Vec::new();
139
140 if let Some(content) = self.documents.get(document_uri) {
142 let lines: Vec<&str> = content.lines().collect();
143
144 if let Some(line) = lines.get(position.line as usize) {
146 let line = *line;
147
148 if line.trim().starts_with('[') && !line.contains(']') {
150 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 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 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 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 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 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 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 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 let mut documents = self.documents.clone();
483 documents.insert(
484 params.text_document.uri.clone(),
485 params.text_document.text.clone(),
486 );
487
488 let diagnostics = self.generate_diagnostics(¶ms.text_document.text);
490
491 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 let mut documents = self.documents.clone();
503 documents.insert(params.text_document.uri.clone(), change.text.clone());
504
505 let diagnostics = self.generate_diagnostics(&change.text);
507
508 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 let mut documents = self.documents.clone();
520 documents.remove(¶ms.text_document.uri);
521
522 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
545pub 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}