use crate::{DescItem, DescItemKind};
use glua_parser::{
LuaAstNode, LuaDocDescription, LuaKind, LuaSyntaxElement, LuaTokenKind, Reader, SourceRange,
};
use rowan::Direction;
use std::cmp::min;
use unicode_general_category::{GeneralCategory, get_general_category};
pub fn is_ws(c: char) -> bool {
matches!(c, ' ' | '\t')
}
pub fn desc_to_lines(
text: &str,
desc: LuaDocDescription,
cursor_position: Option<usize>,
) -> Vec<SourceRange> {
let mut lines = Vec::new();
let mut line = SourceRange::EMPTY;
let mut skip_current_line_content = false;
let mut seen_doc_comments = false;
let mut handle_token = |token: &LuaSyntaxElement| {
let LuaSyntaxElement::Token(token) = token else {
return;
};
match token.kind() {
LuaKind::Token(LuaTokenKind::TkDocDetail) => {
if skip_current_line_content {
return;
}
let range: SourceRange = token.text_range().into();
if line.end_offset() == range.start_offset {
line.length += range.length;
} else {
if line != SourceRange::EMPTY {
seen_doc_comments |= !text[line.start_offset..line.end_offset()]
.chars()
.all(|c| c == '-');
lines.push(line);
}
line = range;
}
}
LuaKind::Token(LuaTokenKind::TkEndOfLine) => {
seen_doc_comments |= !text[line.start_offset..line.end_offset()]
.chars()
.all(|c| c == '-');
lines.push(line);
line = SourceRange::EMPTY;
skip_current_line_content = false
}
LuaKind::Token(LuaTokenKind::TkNormalStart | LuaTokenKind::TkDocContinue) => {
let leading_marks = token.text().chars().take_while(|c| *c == '-').count();
skip_current_line_content = leading_marks != 3;
if skip_current_line_content {
line = SourceRange::new(token.text_range().start().into(), 0);
} else {
line = token.text_range().into();
line.start_offset += leading_marks;
line.length -= leading_marks;
}
}
_ => {}
}
};
let prev_token = desc
.syntax()
.siblings_with_tokens(Direction::Prev)
.skip(1)
.find(|tk| tk.kind() != LuaTokenKind::TkWhitespace.into());
if let Some(prev_token) = prev_token
&& prev_token.kind() == LuaTokenKind::TkNormalStart.into()
{
handle_token(&prev_token);
}
for child in desc.syntax().children_with_tokens() {
handle_token(&child);
}
if !line.is_empty() {
seen_doc_comments |= !text[line.start_offset..line.end_offset()]
.trim_end()
.chars()
.all(|c| c == '-');
lines.push(line);
}
if !seen_doc_comments {
return Vec::new();
}
let mut new_start = 0;
for line in lines.iter() {
let line_text = &text[line.start_offset..line.end_offset()];
if line_text.trim_end().chars().all(|c| c == '-') {
new_start += 1;
} else {
break;
}
}
let mut new_end = lines.len();
for line in lines[new_start..].iter().rev() {
let line_text = &text[line.start_offset..line.end_offset()];
if line_text.trim_end().chars().all(|c| c == '-') {
new_end -= 1;
} else {
break;
}
}
if new_start > 0 || new_end < lines.len() {
lines = lines.drain(new_start..new_end).collect();
}
let mut common_indent = None;
for line in lines.iter() {
let text = &text[line.start_offset..line.end_offset()];
if is_blank(text) {
continue;
}
let indent = text.chars().take_while(|c| is_ws(*c)).count();
common_indent = match common_indent {
None => Some(indent),
Some(common_indent) => Some(min(common_indent, indent)),
};
}
let common_indent = common_indent.unwrap_or_default();
if common_indent > 0 {
for line in lines.iter_mut() {
if line.length >= common_indent {
line.start_offset += common_indent;
line.length -= common_indent;
}
}
}
if let Some(cursor_position) = cursor_position {
for (i, line) in lines.iter().enumerate() {
let start: usize = line.start_offset;
if start > cursor_position {
lines.truncate(i);
break;
}
}
}
lines
}
pub trait ResultContainer {
fn results(&self) -> &Vec<DescItem>;
fn results_mut(&mut self) -> &mut Vec<DescItem>;
fn cursor_position(&self) -> Option<usize>;
fn emit_range(&mut self, range: SourceRange, kind: DescItemKind) {
let should_emit = if let Some(cursor_position) = self.cursor_position() {
matches!(kind, DescItemKind::Ref | DescItemKind::JavadocLink)
&& range.contains_inclusive(cursor_position)
} else {
!range.is_empty()
};
if should_emit {
let Some(last) = self.results_mut().last_mut() else {
self.results_mut().push(DescItem {
range: range.into(),
kind,
});
return;
};
let end: usize = last.range.end().into();
if last.kind == kind && end == range.start_offset {
last.range = last.range.cover(range.into());
} else {
self.results_mut().push(DescItem {
range: range.into(),
kind,
});
}
}
}
fn emit(&mut self, reader: &mut Reader, kind: DescItemKind) {
self.emit_range(reader.current_range(), kind);
reader.reset_buff();
}
}
pub struct BacktrackPoint<'a> {
prev_reader: Reader<'a>,
prev_pos: usize,
}
impl<'a> BacktrackPoint<'a> {
pub fn new<C: ResultContainer>(c: &mut C, reader: &mut Reader<'a>) -> Self {
Self {
prev_reader: reader.clone(),
prev_pos: c.results().len(),
}
}
pub fn commit<C: ResultContainer>(self, c: &mut C, reader: &mut Reader<'a>) {
let (_c, _reader) = (c, reader); std::mem::forget(self);
}
pub fn rollback<C: ResultContainer>(self, c: &mut C, reader: &mut Reader<'a>) {
*reader = self.prev_reader.clone();
c.results_mut().truncate(self.prev_pos);
std::mem::forget(self);
}
}
impl<'a> Drop for BacktrackPoint<'a> {
fn drop(&mut self) {
panic!("backtrack point should be committed or rolled back");
}
}
pub fn is_punct(c: char) -> bool {
if c.is_ascii() {
c.is_ascii_punctuation()
} else {
matches!(
get_general_category(c),
GeneralCategory::ClosePunctuation
| GeneralCategory::ConnectorPunctuation
| GeneralCategory::DashPunctuation
| GeneralCategory::FinalPunctuation
| GeneralCategory::InitialPunctuation
| GeneralCategory::OpenPunctuation
| GeneralCategory::OtherPunctuation
| GeneralCategory::CurrencySymbol
| GeneralCategory::MathSymbol
| GeneralCategory::ModifierSymbol
| GeneralCategory::OtherSymbol
)
}
}
pub fn is_opening_quote(c: char) -> bool {
if c.is_ascii() {
matches!(c, '\'' | '"' | '<' | '(' | '[' | '{')
} else {
matches!(
get_general_category(c),
GeneralCategory::OpenPunctuation
| GeneralCategory::InitialPunctuation
| GeneralCategory::FinalPunctuation
)
}
}
pub fn is_closing_quote(c: char) -> bool {
if c.is_ascii() {
matches!(c, '\'' | '"' | '>' | ')' | ']' | '}')
} else {
matches!(
get_general_category(c),
GeneralCategory::ClosePunctuation
| GeneralCategory::InitialPunctuation
| GeneralCategory::FinalPunctuation
)
}
}
pub fn is_quote_match(l: char, r: char) -> bool {
if !l.is_ascii() || !r.is_ascii() {
return true;
}
matches!(
(l, r),
('\'', '\'') | ('"', '"') | ('<', '>') | ('(', ')') | ('[', ']') | ('{', '}')
)
}
pub fn is_blank(s: &str) -> bool {
s.is_empty() || s.chars().all(|c| c.is_ascii_whitespace())
}
pub fn is_code_directive(name: &str) -> bool {
matches!(
name,
"code-block" | "sourcecode" | "code" | "literalinclude" | "math"
)
}
pub fn is_lua_role(name: &str) -> bool {
matches!(
name,
"func"
| "data"
| "const"
| "class"
| "alias"
| "enum"
| "meth"
| "attr"
| "mod"
| "obj"
| "lua"
)
}
pub fn sort_result(items: &mut [DescItem]) {
items.sort_by_key(|r| {
let len: usize = r.range.len().into();
(
r.range.start(), usize::MAX - len, r.kind != DescItemKind::Scope, )
});
}
#[cfg(test)]
mod tests {
use super::*;
use glua_parser::{LuaParser, ParserConfig};
use googletest::prelude::*;
fn get_desc(code: &str) -> LuaDocDescription {
LuaParser::parse(code, ParserConfig::default())
.get_chunk_node()
.descendants::<LuaDocDescription>()
.next()
.unwrap()
}
fn run_desc_to_lines(code: &str) -> Vec<&str> {
let desc = get_desc(code);
let lines = desc_to_lines(code, desc, None);
lines
.iter()
.map(|range| &code[range.start_offset..range.end_offset()])
.collect()
}
#[gtest]
fn test_desc_to_lines() {
expect_eq!(
run_desc_to_lines(
r#"
-- Desc
"#
),
vec![""; 0]
);
expect_eq!(
run_desc_to_lines(
r#"
----------
-- Desc --
----------
"#
),
vec![""; 0]
);
expect_eq!(
run_desc_to_lines(
r#"
----------
-- Desc --
----------
-- Desc --
----------
"#
),
vec![""; 0]
);
expect_eq!(
run_desc_to_lines(
r#"
--- Desc
"#
),
vec!["Desc"]
);
expect_eq!(
run_desc_to_lines(
r#"
--------
--- Desc
--------
"#
),
vec!["Desc"]
);
expect_eq!(
run_desc_to_lines(
r#"
--------
--- Desc
--------
--- Desc
--------
"#
),
vec![" Desc", "-----", " Desc"]
);
expect_eq!(
run_desc_to_lines(
r#"
--- Desc
---Desc 2
"#
),
vec![" Desc", "Desc 2"]
);
expect_eq!(
run_desc_to_lines(
r#"
---Desc
--- Desc 2
"#
),
vec!["Desc", " Desc 2"]
);
expect_eq!(
run_desc_to_lines(
r#"
--- Desc
--- Desc 2
"#
),
vec!["Desc", "Desc 2"]
);
expect_eq!(
run_desc_to_lines(
r#"
--- @param x int Desc
"#
),
vec!["Desc"]
);
}
}