1use std::{collections::HashMap, path::PathBuf, sync::Mutex, time::Instant};
2
3use log::{debug, error, info};
4use lsp_server::{Notification, Request};
5use lsp_types::{
6 request::GotoTypeDefinitionParams, CompletionParams, Diagnostic, DidChangeTextDocumentParams,
7 DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentDiagnosticParams, HoverParams,
8 Url,
9};
10
11use crate::{
12 parser::{self, Parser},
13 parser_utils::ParserUtils,
14 treesitter::TreesitterImpl,
15 DefinitionResult, GitlabElement, HoverResult, LSPCompletion, LSPConfig, LSPLocation,
16 LSPPosition, LSPResult, Range, ReferencesResult,
17};
18
19pub struct LSPHandlers {
20 cfg: LSPConfig,
21 store: Mutex<HashMap<String, String>>,
22 nodes: Mutex<HashMap<String, HashMap<String, String>>>,
23 stages: Mutex<HashMap<String, GitlabElement>>,
24 variables: Mutex<HashMap<String, GitlabElement>>,
25 indexing_in_progress: Mutex<bool>,
26 parser: Box<dyn Parser>,
27}
28
29impl LSPHandlers {
30 pub fn new(cfg: LSPConfig) -> LSPHandlers {
31 let store = Mutex::new(HashMap::new());
32 let nodes = Mutex::new(HashMap::new());
33 let stages = Mutex::new(HashMap::new());
34 let variables = Mutex::new(HashMap::new());
35 let indexing_in_progress = Mutex::new(false);
36
37 let events = LSPHandlers {
38 cfg: cfg.clone(),
39 store,
40 nodes,
41 stages,
42 variables,
43 indexing_in_progress,
44 parser: Box::new(parser::ParserImpl::new(
45 cfg.remote_urls,
46 cfg.package_map,
47 cfg.cache_path,
48 Box::new(TreesitterImpl::new()),
49 )),
50 };
51
52 match events.index_workspace(events.cfg.root_dir.as_str()) {
53 Ok(_) => {}
54 Err(err) => {
55 error!("error indexing workspace; err: {}", err);
56 }
57 };
58
59 events
60 }
61
62 pub fn on_hover(&self, request: Request) -> Option<LSPResult> {
63 let params = serde_json::from_value::<HoverParams>(request.params).ok()?;
64
65 let store = self.store.lock().unwrap();
66 let uri = ¶ms.text_document_position_params.text_document.uri;
67 let document = store.get::<String>(&uri.clone().into())?;
68
69 let position = params.text_document_position_params.position;
70 let line = document.lines().nth(position.line as usize)?;
71
72 let word =
73 ParserUtils::extract_word(line, position.character as usize)?.trim_end_matches(':');
74
75 let mut hover = String::new();
76 for (content_uri, content) in store.iter() {
77 if let Some(element) = self.parser.get_root_node(content_uri, content, word) {
78 if content_uri.ends_with(uri.as_str())
81 && line.eq(&format!("{}:", element.key.as_str()))
82 {
83 continue;
84 }
85
86 if !hover.is_empty() {
87 hover = format!("{}\r\n--------\r\n", hover);
88 }
89
90 hover = format!("{}{}", hover, element.content?);
91 }
92 }
93
94 if hover.is_empty() {
95 return None;
96 }
97
98 hover = format!("```yaml \r\n{}\r\n```", hover);
99
100 Some(LSPResult::Hover(HoverResult {
101 id: request.id,
102 content: hover,
103 }))
104 }
105
106 pub fn on_change(&self, notification: Notification) -> Option<LSPResult> {
107 let start = Instant::now();
108 let params =
109 serde_json::from_value::<DidChangeTextDocumentParams>(notification.params).ok()?;
110
111 if params.content_changes.len() != 1 {
112 return None;
113 }
114
115 let mut store = self.store.lock().unwrap();
118 let mut all_nodes = self.nodes.lock().unwrap();
119 all_nodes.insert(params.text_document.uri.to_string(), HashMap::new());
121
122 let mut all_variables = self.variables.lock().unwrap();
123
124 if let Some(results) = self.parser.parse_contents(
125 ¶ms.text_document.uri,
126 ¶ms.content_changes.first()?.text,
127 false,
128 ) {
129 for file in results.files {
130 store.insert(file.path, file.content);
131 }
132
133 for node in results.nodes {
134 info!("found node: {:?}", &node);
135 all_nodes
136 .entry(node.uri)
137 .or_default()
138 .insert(node.key, node.content?);
139 }
140
141 if !results.stages.is_empty() {
142 let mut all_stages = self.stages.lock().unwrap();
143 all_stages.clear();
144
145 for stage in results.stages {
146 info!("found stage: {:?}", &stage);
147 all_stages.insert(stage.key.clone(), stage);
148 }
149 }
150
151 for variable in results.variables {
154 info!("found variable: {:?}", &variable);
155 all_variables.insert(variable.key.clone(), variable);
156 }
157 }
158
159 info!("ONCHANGE ELAPSED: {:?}", start.elapsed());
160
161 None
162 }
163
164 pub fn on_open(&self, notification: Notification) -> Option<LSPResult> {
165 let in_progress = self.indexing_in_progress.lock().unwrap();
166 drop(in_progress);
167
168 let params =
169 serde_json::from_value::<DidOpenTextDocumentParams>(notification.params).ok()?;
170
171 let mut store = self.store.lock().unwrap();
172 let mut all_nodes = self.nodes.lock().unwrap();
173 let mut all_stages = self.stages.lock().unwrap();
174
175 if let Some(results) =
176 self.parser
177 .parse_contents(¶ms.text_document.uri, ¶ms.text_document.text, true)
178 {
179 for file in results.files {
180 store.insert(file.path, file.content);
181 }
182
183 for node in results.nodes {
184 info!("found node: {:?}", &node);
185
186 all_nodes
187 .entry(node.uri)
188 .or_default()
189 .insert(node.key, node.content?);
190 }
191
192 for stage in results.stages {
193 info!("found stage: {:?}", &stage);
194 all_stages.insert(stage.key.clone(), stage);
195 }
196 }
197
198 debug!("finished searching");
199
200 None
201 }
202
203 pub fn on_definition(&self, request: Request) -> Option<LSPResult> {
204 let params = serde_json::from_value::<GotoTypeDefinitionParams>(request.params).ok()?;
205
206 let store = self.store.lock().unwrap();
207 let document_uri = params.text_document_position_params.text_document.uri;
208 let document = store.get::<String>(&document_uri.clone().into())?;
209 let position = params.text_document_position_params.position;
210
211 let mut locations: Vec<LSPLocation> = vec![];
212
213 match self.parser.get_position_type(document, position) {
214 parser::CompletionType::RootNode | parser::CompletionType::Extend => {
215 let line = document.lines().nth(position.line as usize)?;
216 let word = ParserUtils::extract_word(line, position.character as usize)?
217 .trim_end_matches(':');
218
219 for (uri, content) in store.iter() {
220 if let Some(element) = self.parser.get_root_node(uri, content, word) {
221 if document_uri.as_str().ends_with(uri)
222 && line.eq(&format!("{}:", element.key.as_str()))
223 {
224 continue;
225 }
226
227 locations.push(LSPLocation {
228 uri: uri.clone(),
229 range: element.range,
230 });
231 }
232 }
233 }
234 parser::CompletionType::Include(info) => {
235 if let Some(local) = info.local {
236 let local = ParserUtils::strip_quotes(&local.path).trim_start_matches('.');
237
238 for (uri, _) in store.iter() {
239 if uri.ends_with(local) {
240 locations.push(LSPLocation {
241 uri: uri.clone(),
242 range: Range {
243 start: LSPPosition {
244 line: 0,
245 character: 0,
246 },
247 end: LSPPosition {
248 line: 0,
249 character: 0,
250 },
251 },
252 });
253
254 break;
255 }
256 }
257 }
258 if let Some(remote) = info.remote {
259 let file = remote.file?;
260 let file = ParserUtils::strip_quotes(&file).trim_start_matches('/');
261
262 let path = format!("{}/{}/{}", remote.project?, remote.reference?, file);
263
264 for (uri, _) in store.iter() {
265 if uri.ends_with(path.as_str()) {
266 locations.push(LSPLocation {
267 uri: uri.clone(),
268 range: Range {
269 start: LSPPosition {
270 line: 0,
271 character: 0,
272 },
273 end: LSPPosition {
274 line: 0,
275 character: 0,
276 },
277 },
278 });
279
280 break;
281 }
282 }
283 }
284 }
285 _ => {
286 error!("invalid position type for goto def");
287 return None;
288 }
289 };
290
291 Some(LSPResult::Definition(DefinitionResult {
292 id: request.id,
293 locations,
294 }))
295 }
296
297 pub fn on_completion(&self, request: Request) -> Option<LSPResult> {
298 let start = Instant::now();
299 let params: CompletionParams = serde_json::from_value(request.params).ok()?;
300
301 let store = self.store.lock().unwrap();
302 let document_uri = params.text_document_position.text_document.uri;
303 let document = store.get::<String>(&document_uri.clone().into())?;
304
305 let position = params.text_document_position.position;
306 let line = document.lines().nth(position.line as usize)?;
307
308 let mut items: Vec<LSPCompletion> = vec![];
309
310 let completion_type = self.parser.get_position_type(document, position);
311
312 match completion_type {
313 parser::CompletionType::None => return None,
314 parser::CompletionType::Include(_) => return None,
315 parser::CompletionType::RootNode => {}
316 parser::CompletionType::Stage => {
317 let stages = self.stages.lock().unwrap();
318 let word = ParserUtils::word_before_cursor(
319 line,
320 position.character as usize,
321 |c: char| c.is_whitespace(),
322 );
323 let after = ParserUtils::word_after_cursor(line, position.character as usize);
324
325 for (stage, _) in stages.iter() {
326 if stage.contains(word) {
327 items.push(LSPCompletion {
328 label: stage.clone(),
329 details: None,
330 location: LSPLocation {
331 range: crate::Range {
332 start: crate::LSPPosition {
333 line: position.line,
334 character: position.character - word.len() as u32,
335 },
336 end: crate::LSPPosition {
337 line: position.line,
338 character: position.character + after.len() as u32,
339 },
340 },
341 ..Default::default()
342 },
343 })
344 }
345 }
346 }
347 parser::CompletionType::Extend => {
348 let nodes = self.nodes.lock().unwrap();
349 let word = ParserUtils::word_before_cursor(
350 line,
351 position.character as usize,
352 |c: char| c.is_whitespace(),
353 );
354
355 let after = ParserUtils::word_after_cursor(line, position.character as usize);
356
357 for (_, node) in nodes.iter() {
358 for (node_key, node_description) in node.iter() {
359 if node_key.starts_with('.') && node_key.contains(word) {
360 items.push(LSPCompletion {
361 label: node_key.clone(),
362 details: Some(node_description.clone()),
363 location: LSPLocation {
364 range: crate::Range {
365 start: crate::LSPPosition {
366 line: position.line,
367 character: position.character - word.len() as u32,
368 },
369 end: crate::LSPPosition {
370 line: position.line,
371 character: position.character + after.len() as u32,
372 },
373 },
374 ..Default::default()
375 },
376 })
377 }
378 }
379 }
380 }
381 parser::CompletionType::Variable => {
382 let variables = self.variables.lock().unwrap();
383 let word = ParserUtils::word_before_cursor(
384 line,
385 position.character as usize,
386 |c: char| c == '$',
387 );
388
389 let after = ParserUtils::word_after_cursor(line, position.character as usize);
390
391 for (variable, _) in variables.iter() {
392 if variable.starts_with(word) {
393 items.push(LSPCompletion {
394 label: variable.clone(),
395 details: None,
396 location: LSPLocation {
397 range: crate::Range {
398 start: crate::LSPPosition {
399 line: position.line,
400 character: position.character - word.len() as u32,
401 },
402 end: crate::LSPPosition {
403 line: position.line,
404 character: position.character + after.len() as u32,
405 },
406 },
407 ..Default::default()
408 },
409 })
410 }
411 }
412 }
413 }
414
415 info!("AUTOCOMPLETE ELAPSED: {:?}", start.elapsed());
416
417 Some(LSPResult::Completion(crate::CompletionResult {
418 id: request.id,
419 list: items,
420 }))
421 }
422
423 fn index_workspace(&self, root_dir: &str) -> anyhow::Result<()> {
424 let mut in_progress = self.indexing_in_progress.lock().unwrap();
425 *in_progress = true;
426
427 let start = Instant::now();
428
429 let mut store = self.store.lock().unwrap();
430 let mut all_nodes = self.nodes.lock().unwrap();
431 let mut all_stages = self.stages.lock().unwrap();
432 let mut all_variables = self.variables.lock().unwrap();
433
434 let mut uri = Url::parse(format!("file://{}/", root_dir).as_str())?;
435 info!("uri: {}", &uri);
436
437 let list = std::fs::read_dir(root_dir)?;
438 let mut root_file: Option<PathBuf> = None;
439
440 for item in list.flatten() {
441 if item.file_name() == ".gitlab-ci.yaml" || item.file_name() == ".gitlab-ci.yml" {
442 root_file = Some(item.path());
443 break;
444 }
445 }
446
447 let root_file_content = match root_file {
448 Some(root_file) => {
449 let file_name = root_file.file_name().unwrap().to_str().unwrap();
450 uri = uri.join(file_name)?;
451
452 std::fs::read_to_string(root_file)?
453 }
454 _ => {
455 return Err(anyhow::anyhow!("root file missing"));
456 }
457 };
458
459 info!("URI: {}", &uri);
460 if let Some(results) = self.parser.parse_contents(&uri, &root_file_content, true) {
461 for file in results.files {
462 info!("found file: {:?}", &file);
463 store.insert(file.path, file.content);
464 }
465
466 for node in results.nodes {
467 info!("found node: {:?}", &node);
468 let content = node.content.unwrap_or("".to_string());
469
470 all_nodes
471 .entry(node.uri)
472 .or_default()
473 .insert(node.key, content);
474 }
475
476 for stage in results.stages {
477 info!("found stage: {:?}", &stage);
478 all_stages.insert(stage.key.clone(), stage);
479 }
480
481 for variable in results.variables {
482 info!("found variable: {:?}", &variable);
483 all_variables.insert(variable.key.clone(), variable);
484 }
485 }
486
487 error!("INDEX WORKSPACE ELAPSED: {:?}", start.elapsed());
488
489 Ok(())
490 }
491
492 pub fn on_save(&self, notification: Notification) -> Option<LSPResult> {
493 let _params =
494 serde_json::from_value::<DidSaveTextDocumentParams>(notification.params).ok()?;
495
496 None
499 }
500
501 pub fn on_diagnostic(&self, request: Request) -> Option<LSPResult> {
502 let start = Instant::now();
503 let params = serde_json::from_value::<DocumentDiagnosticParams>(request.params).ok()?;
504 let store = self.store.lock().unwrap();
505 let all_nodes = self.nodes.lock().unwrap();
506
507 let content: String = store
508 .get(¶ms.text_document.uri.to_string())?
509 .to_string();
510
511 let extends = self.parser.get_all_extends(
512 params.text_document.uri.to_string(),
513 content.as_str(),
514 None,
515 );
516
517 let mut diagnostics: Vec<Diagnostic> = vec![];
518
519 'extend: for extend in extends {
520 if extend.uri == params.text_document.uri.to_string() {
521 for (_, root_nodes) in all_nodes.iter() {
522 if root_nodes.get(&extend.key).is_some() {
523 continue 'extend;
524 }
525 }
526
527 diagnostics.push(Diagnostic::new_simple(
528 lsp_types::Range {
529 start: lsp_types::Position {
530 line: extend.range.start.line,
531 character: extend.range.start.character,
532 },
533 end: lsp_types::Position {
534 line: extend.range.end.line,
535 character: extend.range.end.character,
536 },
537 },
538 format!("Rule: {} does not exist.", extend.key),
539 ));
540 }
541 }
542
543 let stages = self
544 .parser
545 .get_all_stages(params.text_document.uri.to_string(), content.as_str());
546
547 let all_stages = self.stages.lock().unwrap();
548 for stage in stages {
549 if all_stages.get(&stage.key).is_none() {
550 diagnostics.push(Diagnostic::new_simple(
551 lsp_types::Range {
552 start: lsp_types::Position {
553 line: stage.range.start.line,
554 character: stage.range.start.character,
555 },
556 end: lsp_types::Position {
557 line: stage.range.end.line,
558 character: stage.range.end.character,
559 },
560 },
561 format!("Stage: {} does not exist.", stage.key),
562 ));
563 }
564 }
565
566 info!("DIAGNOSTICS ELAPSED: {:?}", start.elapsed());
567 Some(LSPResult::Diagnostics(crate::DiagnosticsResult {
568 id: request.id,
569 diagnostics,
570 }))
571 }
572
573 pub fn on_references(&self, request: Request) -> Option<LSPResult> {
574 let start = Instant::now();
575
576 let params = serde_json::from_value::<lsp_types::ReferenceParams>(request.params).ok()?;
577
578 let store = self.store.lock().unwrap();
579 let document_uri = ¶ms.text_document_position.text_document.uri;
580 let document = store.get::<String>(&document_uri.clone().into())?;
581
582 let position = params.text_document_position.position;
583 let line = document.lines().nth(position.line as usize)?;
584
585 let position_type = self.parser.get_position_type(document, position);
586 let mut references: Vec<GitlabElement> = vec![];
587
588 match position_type {
589 parser::CompletionType::Extend => {
590 let word = ParserUtils::extract_word(line, position.character as usize)?;
591
592 for (uri, content) in store.iter() {
593 let mut extends =
594 self.parser
595 .get_all_extends(uri.to_string(), content.as_str(), Some(word));
596 references.append(&mut extends);
597 }
598 }
599 parser::CompletionType::RootNode => {
600 let word = ParserUtils::extract_word(line, position.character as usize)?
601 .trim_end_matches(':');
602
603 if word.starts_with('.') {
605 for (uri, content) in store.iter() {
606 let mut extends = self.parser.get_all_extends(
607 uri.to_string(),
608 content.as_str(),
609 Some(word),
610 );
611 references.append(&mut extends);
612 }
613 }
614 }
615 _ => {}
616 }
617
618 info!("REFERENCES ELAPSED: {:?}", start.elapsed());
619
620 Some(LSPResult::References(ReferencesResult {
621 id: request.id,
622 locations: references,
623 }))
624 }
625}