use crate::lex::SyntaxKind;
use crate::nodes::{SyntaxNode, SyntaxToken};
use rowan::NodeOrToken;
pub(crate) fn token_column(token: &SyntaxToken) -> usize {
line_prefix(token.prev_token()).chars().count()
}
pub(crate) struct ColumnIndex {
starts: Vec<(rowan::TextSize, usize)>,
line_breaks: Vec<rowan::TextSize>,
}
impl ColumnIndex {
pub(crate) fn new(node: &SyntaxNode) -> Self {
let mut column = node.first_token().map_or(0, |t| token_column(&t));
let mut starts = Vec::new();
let mut line_breaks = Vec::new();
for token in node
.descendants_with_tokens()
.filter_map(|e| e.into_token())
{
let start = token.text_range().start();
starts.push((start, column));
line_breaks.extend(
token
.text()
.match_indices(['\n', '\r'])
.map(|(i, _)| start + rowan::TextSize::try_from(i).expect("offset in a token")),
);
column = if token.kind() == SyntaxKind::BOM {
0
} else {
column_after(column, token.text())
};
}
Self {
starts,
line_breaks,
}
}
pub(crate) fn has_line_break(&self, range: rowan::TextRange) -> bool {
let i = self.line_breaks.partition_point(|&b| b < range.start());
self.line_breaks.get(i).is_some_and(|&b| b < range.end())
}
pub(crate) fn column(&self, token: &SyntaxToken) -> usize {
self.column_at(token.text_range().start())
.unwrap_or_else(|| token_column(token))
}
pub(crate) fn column_at(&self, offset: rowan::TextSize) -> Option<usize> {
let i = self
.starts
.binary_search_by_key(&offset, |&(s, _)| s)
.ok()?;
Some(self.starts[i].1)
}
}
pub(crate) fn node_column(node: &SyntaxNode) -> usize {
line_prefix(token_before(node)).chars().count()
}
pub(crate) fn line_indent(node: &SyntaxNode) -> usize {
space_indent(&line_prefix(token_before(node)))
}
pub(crate) fn column_after(column: usize, text: &str) -> usize {
match text.rfind(['\n', '\r']) {
Some(i) => text[i + 1..].chars().count(),
None => column + text.chars().count(),
}
}
pub(crate) fn space_indent(text: &str) -> usize {
text.len() - text.trim_start_matches(' ').len()
}
fn line_prefix(last: Option<SyntaxToken>) -> String {
let mut pieces = Vec::new();
let mut cursor = last;
while let Some(token) = cursor {
if token.kind() == SyntaxKind::BOM {
break;
}
let text = token.text();
if let Some(i) = text.rfind(['\n', '\r']) {
pieces.push(text[i + 1..].to_string());
break;
}
pieces.push(text.to_string());
cursor = token.prev_token();
}
pieces.reverse();
pieces.concat()
}
fn token_before(node: &SyntaxNode) -> Option<SyntaxToken> {
if let Some(first) = node.first_token() {
return first.prev_token();
}
let mut current = node.clone();
loop {
let mut cursor = current.prev_sibling_or_token();
while let Some(element) = cursor {
match element {
NodeOrToken::Token(token) => return Some(token),
NodeOrToken::Node(n) => match n.last_token() {
Some(token) => return Some(token),
None => cursor = n.prev_sibling_or_token(),
},
}
}
current = current.parent()?;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::yaml::YamlFile;
use rowan::ast::AstNode;
use std::str::FromStr;
fn columns(text: &str, kind: SyntaxKind) -> Vec<(usize, usize)> {
let file = YamlFile::from_str(text).unwrap();
file.syntax()
.descendants()
.filter(|n| n.kind() == kind)
.map(|n| (node_column(&n), line_indent(&n)))
.collect()
}
#[test]
fn test_node_column_and_line_indent() {
assert_eq!(
columns("a:\n - b: [x, y]\n c: 1\n", SyntaxKind::MAPPING),
vec![(0, 0), (4, 2)]
);
assert_eq!(
columns("a:\r - b: 1\r", SyntaxKind::SEQUENCE),
vec![(2, 2)]
);
assert_eq!(
columns("\u{FEFF}a: {b: \u{e9}}\n", SyntaxKind::MAPPING),
vec![(0, 0), (3, 0)]
);
}
#[test]
fn test_token_column() {
let file = YamlFile::from_str("- \u{e9}\r\n- x\n").unwrap();
let cols: Vec<_> = file
.syntax()
.descendants_with_tokens()
.filter_map(|e| e.into_token())
.filter(|t| !t.text().is_empty())
.map(|t| (t.text().to_string(), token_column(&t)))
.collect();
assert_eq!(
cols,
vec![
("-".to_string(), 0),
(" ".to_string(), 1),
("\u{e9}".to_string(), 2),
("\r\n".to_string(), 3),
("-".to_string(), 0),
(" ".to_string(), 1),
("x".to_string(), 2),
("\n".to_string(), 3),
]
);
}
#[test]
fn test_column_index() {
for text in [
"- \u{e9}\r\n- x\n",
"\u{FEFF}a: {b: \u{e9}, c: [1,\n 2]}\r d: \"x\n y\"\n",
"--- |\n text\n...\n\u{FEFF}--- - a\n - b\n",
"k:\n # c\n - \u{FEFF}x\n",
"---\n- ? - a\n - - ? - - b\n - c\n : d\n - e\n : 23\n",
] {
let file = YamlFile::from_str(text).unwrap();
let index = ColumnIndex::new(file.syntax());
let lines = crate::line_index::LineIndex::new(text);
for token in file
.syntax()
.descendants_with_tokens()
.filter_map(|e| e.into_token())
{
assert_eq!(
index.column(&token),
lines.column(token.text_range().start().into()),
"{token:?} in {text:?}"
);
}
}
}
#[test]
fn test_column_after() {
assert_eq!(column_after(2, "ab"), 4);
assert_eq!(column_after(2, "\u{e9}"), 3);
assert_eq!(column_after(2, "a\n b"), 3);
assert_eq!(column_after(2, "a\r\n b"), 3);
assert_eq!(column_after(2, "a\r b"), 3);
assert_eq!(column_after(2, "\r"), 0);
}
#[test]
fn test_space_indent() {
assert_eq!(space_indent(""), 0);
assert_eq!(space_indent(" \tx"), 2);
assert_eq!(space_indent(" "), 3);
}
}