gitlab_ci_ls_parser/
handlers.rs

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 = &params.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                // Check if we found the same line that triggered the hover event and discard it
79                // adding format : because yaml parser removes it from the key
80                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        // TODO: nodes
116
117        let mut store = self.store.lock().unwrap();
118        let mut all_nodes = self.nodes.lock().unwrap();
119        // reset previous
120        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            &params.text_document.uri,
126            &params.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            // should be per file...
152            // TODO: clear correct variables
153            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(&params.text_document.uri, &params.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        // PUBLISH DIAGNOSTICS
497
498        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(&params.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 = &params.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                // currently support only those that are extends
604                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}