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_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 return Vec::new();
104 }
105
106 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 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 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); 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 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(), usize::MAX - len, r.kind != DescItemKind::Scope, )
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}