Skip to main content

glua_parser_desc/
util.rs

1use crate::{DescItem, DescItemKind};
2use glua_parser::{
3    LuaAstNode, LuaDocDescription, LuaKind, LuaSyntaxElement, LuaTokenKind, Reader, SourceRange,
4};
5use rowan::Direction;
6use std::cmp::min;
7use unicode_general_category::{GeneralCategory, get_general_category};
8
9pub fn is_ws(c: char) -> bool {
10    matches!(c, ' ' | '\t')
11}
12
13pub fn desc_to_lines(
14    text: &str,
15    desc: LuaDocDescription,
16    cursor_position: Option<usize>,
17) -> Vec<SourceRange> {
18    let mut lines = Vec::new();
19    let mut line = SourceRange::EMPTY;
20    let mut skip_current_line_content = false;
21    let mut seen_doc_comments = false;
22
23    let mut handle_token = |token: &LuaSyntaxElement| {
24        let LuaSyntaxElement::Token(token) = token else {
25            return;
26        };
27
28        match token.kind() {
29            LuaKind::Token(LuaTokenKind::TkDocDetail) => {
30                if skip_current_line_content {
31                    return;
32                }
33
34                let range: SourceRange = token.text_range().into();
35                if line.end_offset() == range.start_offset {
36                    line.length += range.length;
37                } else {
38                    if line != SourceRange::EMPTY {
39                        seen_doc_comments |= !text[line.start_offset..line.end_offset()]
40                            .chars()
41                            .all(|c| c == '-');
42                        lines.push(line);
43                    }
44                    line = range;
45                }
46            }
47            LuaKind::Token(LuaTokenKind::TkEndOfLine) => {
48                seen_doc_comments |= !text[line.start_offset..line.end_offset()]
49                    .chars()
50                    .all(|c| c == '-');
51                lines.push(line);
52                line = SourceRange::EMPTY;
53                skip_current_line_content = false
54            }
55            LuaKind::Token(LuaTokenKind::TkNormalStart | LuaTokenKind::TkDocContinue) => {
56                let leading_marks = token.text().chars().take_while(|c| *c == '-').count();
57
58                // Skip content for lines that don't start with three dashes.
59                // Parser will see them as empty lines.
60                //
61                // Note: `leading_marks` can't be longer than three dashes.
62                // If comment starts with four or more dashes, the first three
63                // will end up in `TkNormalStart`, and the rest will be in `TkDocDetail`.
64                skip_current_line_content = leading_marks != 3;
65
66                if skip_current_line_content {
67                    line = SourceRange::new(token.text_range().start().into(), 0);
68                } else {
69                    line = token.text_range().into();
70                    line.start_offset += leading_marks;
71                    line.length -= leading_marks;
72                }
73            }
74            _ => {}
75        }
76    };
77
78    let prev_token = desc
79        .syntax()
80        .siblings_with_tokens(Direction::Prev)
81        .skip(1)
82        .find(|tk| tk.kind() != LuaTokenKind::TkWhitespace.into());
83    if let Some(prev_token) = prev_token
84        && prev_token.kind() == LuaTokenKind::TkNormalStart.into()
85    {
86        handle_token(&prev_token);
87    }
88    for child in desc.syntax().children_with_tokens() {
89        handle_token(&child);
90    }
91
92    if !line.is_empty() {
93        seen_doc_comments |= !text[line.start_offset..line.end_offset()]
94            .trim_end()
95            .chars()
96            .all(|c| c == '-');
97        lines.push(line);
98    }
99
100    if !seen_doc_comments {
101        // Comment block consists entirely of lines that only start with
102        // two dashes, or lines that consist only of dashes and nothing else.
103        return Vec::new();
104    }
105
106    // Strip lines that consist entirely of dashes from start and end
107    // of the comment block.
108    //
109    // This handles cases where comment is adorned with long dashed lines:
110    //
111    // ```
112    // ---------------
113    // --- Comment ---
114    // ---------------
115    // ```
116    let mut new_start = 0;
117    for line in lines.iter() {
118        let line_text = &text[line.start_offset..line.end_offset()];
119        if line_text.trim_end().chars().all(|c| c == '-') {
120            new_start += 1;
121        } else {
122            break;
123        }
124    }
125    let mut new_end = lines.len();
126    for line in lines[new_start..].iter().rev() {
127        let line_text = &text[line.start_offset..line.end_offset()];
128        if line_text.trim_end().chars().all(|c| c == '-') {
129            new_end -= 1;
130        } else {
131            break;
132        }
133    }
134    if new_start > 0 || new_end < lines.len() {
135        lines = lines.drain(new_start..new_end).collect();
136    }
137
138    // Find and remove comment indentation.
139    let mut common_indent = None;
140    for line in lines.iter() {
141        let text = &text[line.start_offset..line.end_offset()];
142
143        if is_blank(text) {
144            continue;
145        }
146
147        let indent = text.chars().take_while(|c| is_ws(*c)).count();
148        common_indent = match common_indent {
149            None => Some(indent),
150            Some(common_indent) => Some(min(common_indent, indent)),
151        };
152    }
153
154    let common_indent = common_indent.unwrap_or_default();
155    if common_indent > 0 {
156        for line in lines.iter_mut() {
157            if line.length >= common_indent {
158                line.start_offset += common_indent;
159                line.length -= common_indent;
160            }
161        }
162    }
163
164    // Don't parse lines past user's cursor when calculating
165    // Go To Definition or Completion. We handle this here so that
166    // we don't affect common indent and other logic.
167    if let Some(cursor_position) = cursor_position {
168        for (i, line) in lines.iter().enumerate() {
169            let start: usize = line.start_offset;
170            if start > cursor_position {
171                lines.truncate(i);
172                break;
173            }
174        }
175    }
176
177    lines
178}
179
180pub trait ResultContainer {
181    fn results(&self) -> &Vec<DescItem>;
182
183    fn results_mut(&mut self) -> &mut Vec<DescItem>;
184
185    fn cursor_position(&self) -> Option<usize>;
186
187    fn emit_range(&mut self, range: SourceRange, kind: DescItemKind) {
188        let should_emit = if let Some(cursor_position) = self.cursor_position() {
189            matches!(kind, DescItemKind::Ref | DescItemKind::JavadocLink)
190                && range.contains_inclusive(cursor_position)
191        } else {
192            !range.is_empty()
193        };
194
195        if should_emit {
196            let Some(last) = self.results_mut().last_mut() else {
197                self.results_mut().push(DescItem {
198                    range: range.into(),
199                    kind,
200                });
201                return;
202            };
203
204            let end: usize = last.range.end().into();
205            if last.kind == kind && end == range.start_offset {
206                last.range = last.range.cover(range.into());
207            } else {
208                self.results_mut().push(DescItem {
209                    range: range.into(),
210                    kind,
211                });
212            }
213        }
214    }
215
216    fn emit(&mut self, reader: &mut Reader, kind: DescItemKind) {
217        self.emit_range(reader.current_range(), kind);
218        reader.reset_buff();
219    }
220}
221
222pub struct BacktrackPoint<'a> {
223    prev_reader: Reader<'a>,
224    prev_pos: usize,
225}
226
227impl<'a> BacktrackPoint<'a> {
228    pub fn new<C: ResultContainer>(c: &mut C, reader: &mut Reader<'a>) -> Self {
229        Self {
230            prev_reader: reader.clone(),
231            prev_pos: c.results().len(),
232        }
233    }
234
235    pub fn commit<C: ResultContainer>(self, c: &mut C, reader: &mut Reader<'a>) {
236        let (_c, _reader) = (c, reader); // We don't actually do anything.
237        std::mem::forget(self);
238    }
239
240    pub fn rollback<C: ResultContainer>(self, c: &mut C, reader: &mut Reader<'a>) {
241        *reader = self.prev_reader.clone();
242        c.results_mut().truncate(self.prev_pos);
243        std::mem::forget(self);
244    }
245}
246
247impl<'a> Drop for BacktrackPoint<'a> {
248    fn drop(&mut self) {
249        panic!("backtrack point should be committed or rolled back");
250    }
251}
252
253pub fn is_punct(c: char) -> bool {
254    if c.is_ascii() {
255        c.is_ascii_punctuation()
256    } else {
257        matches!(
258            get_general_category(c),
259            // P | S
260            GeneralCategory::ClosePunctuation
261                | GeneralCategory::ConnectorPunctuation
262                | GeneralCategory::DashPunctuation
263                | GeneralCategory::FinalPunctuation
264                | GeneralCategory::InitialPunctuation
265                | GeneralCategory::OpenPunctuation
266                | GeneralCategory::OtherPunctuation
267                | GeneralCategory::CurrencySymbol
268                | GeneralCategory::MathSymbol
269                | GeneralCategory::ModifierSymbol
270                | GeneralCategory::OtherSymbol
271        )
272    }
273}
274
275pub fn is_opening_quote(c: char) -> bool {
276    if c.is_ascii() {
277        matches!(c, '\'' | '"' | '<' | '(' | '[' | '{')
278    } else {
279        matches!(
280            get_general_category(c),
281            GeneralCategory::OpenPunctuation
282                | GeneralCategory::InitialPunctuation
283                | GeneralCategory::FinalPunctuation
284        )
285    }
286}
287
288pub fn is_closing_quote(c: char) -> bool {
289    if c.is_ascii() {
290        matches!(c, '\'' | '"' | '>' | ')' | ']' | '}')
291    } else {
292        matches!(
293            get_general_category(c),
294            GeneralCategory::ClosePunctuation
295                | GeneralCategory::InitialPunctuation
296                | GeneralCategory::FinalPunctuation
297        )
298    }
299}
300
301pub fn is_quote_match(l: char, r: char) -> bool {
302    if !l.is_ascii() || !r.is_ascii() {
303        return true;
304    }
305
306    matches!(
307        (l, r),
308        ('\'', '\'') | ('"', '"') | ('<', '>') | ('(', ')') | ('[', ']') | ('{', '}')
309    )
310}
311
312pub fn is_blank(s: &str) -> bool {
313    s.is_empty() || s.chars().all(|c| c.is_ascii_whitespace())
314}
315
316pub fn is_code_directive(name: &str) -> bool {
317    matches!(
318        name,
319        "code-block" | "sourcecode" | "code" | "literalinclude" | "math"
320    )
321}
322
323pub fn is_lua_role(name: &str) -> bool {
324    matches!(
325        name,
326        "func"
327            | "data"
328            | "const"
329            | "class"
330            | "alias"
331            | "enum"
332            | "meth"
333            | "attr"
334            | "mod"
335            | "obj"
336            | "lua"
337    )
338}
339
340pub fn sort_result(items: &mut [DescItem]) {
341    items.sort_by_key(|r| {
342        let len: usize = r.range.len().into();
343
344        (
345            r.range.start(),               // Sort by start position,
346            usize::MAX - len,              // longer tokens first,
347            r.kind != DescItemKind::Scope, // scopes go first.
348        )
349    });
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use glua_parser::{LuaParser, ParserConfig};
356    use googletest::prelude::*;
357
358    fn get_desc(code: &str) -> LuaDocDescription {
359        LuaParser::parse(code, ParserConfig::default())
360            .get_chunk_node()
361            .descendants::<LuaDocDescription>()
362            .next()
363            .unwrap()
364    }
365
366    fn run_desc_to_lines(code: &str) -> Vec<&str> {
367        let desc = get_desc(code);
368        let lines = desc_to_lines(code, desc, None);
369        lines
370            .iter()
371            .map(|range| &code[range.start_offset..range.end_offset()])
372            .collect()
373    }
374
375    #[gtest]
376    fn test_desc_to_lines() {
377        expect_eq!(
378            run_desc_to_lines(
379                r#"
380                -- Desc
381            "#
382            ),
383            vec![""; 0]
384        );
385
386        expect_eq!(
387            run_desc_to_lines(
388                r#"
389                ----------
390                -- Desc --
391                ----------
392            "#
393            ),
394            vec![""; 0]
395        );
396
397        expect_eq!(
398            run_desc_to_lines(
399                r#"
400                ----------
401                -- Desc --
402                ----------
403                -- Desc --
404                ----------
405            "#
406            ),
407            vec![""; 0]
408        );
409
410        expect_eq!(
411            run_desc_to_lines(
412                r#"
413                --- Desc
414            "#
415            ),
416            vec!["Desc"]
417        );
418
419        expect_eq!(
420            run_desc_to_lines(
421                r#"
422                --------
423                --- Desc
424                --------
425            "#
426            ),
427            vec!["Desc"]
428        );
429
430        expect_eq!(
431            run_desc_to_lines(
432                r#"
433                --------
434                --- Desc
435                --------
436                --- Desc
437                --------
438            "#
439            ),
440            vec![" Desc", "-----", " Desc"]
441        );
442
443        expect_eq!(
444            run_desc_to_lines(
445                r#"
446                --- Desc
447                ---Desc 2
448            "#
449            ),
450            vec![" Desc", "Desc 2"]
451        );
452
453        expect_eq!(
454            run_desc_to_lines(
455                r#"
456                ---Desc
457                --- Desc 2
458            "#
459            ),
460            vec!["Desc", " Desc 2"]
461        );
462
463        expect_eq!(
464            run_desc_to_lines(
465                r#"
466                ---  Desc
467                ---  Desc 2
468            "#
469            ),
470            vec!["Desc", "Desc 2"]
471        );
472
473        expect_eq!(
474            run_desc_to_lines(
475                r#"
476                --- @param x int Desc
477            "#
478            ),
479            vec!["Desc"]
480        );
481    }
482}