use lsp_types::{Position, Range, SelectionRange};
use rowan::{TextRange, TextSize, TokenAtOffset};
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
use crate::text::LineIndex;
pub(crate) fn selection_ranges(
root: &SyntaxNode,
idx: &LineIndex,
text: &str,
positions: &[Position],
) -> Vec<SelectionRange> {
positions
.iter()
.map(|pos| selection_range_at(root, idx, text, *pos))
.collect()
}
fn selection_range_at(
root: &SyntaxNode,
idx: &LineIndex,
text: &str,
pos: Position,
) -> SelectionRange {
let offset = idx.offset_at(text, pos.line, pos.character);
let at = TextSize::new(offset.min(u32::MAX as usize) as u32);
let mut ranges: Vec<TextRange> = Vec::new();
match root.token_at_offset(at) {
TokenAtOffset::Single(tok) => push_token_chain(&tok, &mut ranges),
TokenAtOffset::Between(left, right) => {
push_token_chain(&prefer_nontrivia(left, right), &mut ranges)
}
TokenAtOffset::None => ranges.push(root.text_range()),
}
ranges.dedup();
let to_range = |r: TextRange| {
let (sl, sc) = idx.position(text, r.start().into());
let (el, ec) = idx.position(text, r.end().into());
Range {
start: Position::new(sl, sc),
end: Position::new(el, ec),
}
};
let mut current: Option<Box<SelectionRange>> = None;
for &r in ranges.iter().rev() {
current = Some(Box::new(SelectionRange {
range: to_range(r),
parent: current,
}));
}
*current.expect("chain always contains the root range")
}
fn push_token_chain(tok: &SyntaxToken, ranges: &mut Vec<TextRange>) {
ranges.push(tok.text_range());
ranges.extend(tok.parent_ancestors().map(|n| n.text_range()));
}
fn prefer_nontrivia(left: SyntaxToken, right: SyntaxToken) -> SyntaxToken {
let is_trivia = |k| {
matches!(
k,
SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::COMMENT
)
};
if is_trivia(right.kind()) && !is_trivia(left.kind()) {
left
} else {
right
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
type Pair = ((u32, u32), (u32, u32));
fn chain(src: &str, line: u32, character: u32) -> Vec<Pair> {
let root = SyntaxNode::new_root(parse(src).green);
let idx = LineIndex::new(src);
let sr = selection_range_at(&root, &idx, src, Position::new(line, character));
let mut out = Vec::new();
let mut cur = Some(Box::new(sr));
while let Some(node) = cur {
let r = node.range;
out.push((
(r.start.line, r.start.character),
(r.end.line, r.end.character),
));
cur = node.parent;
}
out
}
fn assert_strictly_nested(chain: &[Pair]) {
for pair in chain.windows(2) {
let (inner, outer) = (pair[0], pair[1]);
assert!(
outer.0 <= inner.0 && inner.1 <= outer.1 && outer != inner,
"each step must strictly widen: {inner:?} inside {outer:?}",
);
}
}
#[test]
fn command_argument_expands_group_to_command_to_root() {
let src = "\\section{Intro}\n";
let c = chain(src, 0, 10);
assert_strictly_nested(&c);
assert_eq!(
c[0],
((0, 9), (0, 14)),
"innermost is the title word: {c:?}"
);
assert!(
c.contains(&((0, 8), (0, 15))),
"the {{Intro}} group is a level: {c:?}"
);
assert!(
c.contains(&((0, 0), (0, 15))),
"the \\section command is a level: {c:?}"
);
assert_eq!(
*c.last().unwrap(),
((0, 0), (1, 0)),
"root spans the doc: {c:?}"
);
}
#[test]
fn cursor_in_environment_body_has_environment_ancestor() {
let src = "\\begin{itemize}\n\\item hello\n\\end{itemize}\n";
let c = chain(src, 1, 8);
assert_strictly_nested(&c);
assert!(
c.iter().any(|&(s, e)| s == (0, 0) && e == (2, 13)),
"an ENVIRONMENT level spans \\begin..\\end: {c:?}"
);
assert_eq!(
*c.last().unwrap(),
((0, 0), (3, 0)),
"root spans the doc: {c:?}"
);
}
#[test]
fn math_script_expands_outward() {
let src = "$x^2$\n";
let c = chain(src, 0, 3);
assert_strictly_nested(&c);
assert!(c.len() >= 2, "at least token + root: {c:?}");
assert_eq!(
*c.last().unwrap(),
((0, 0), (1, 0)),
"root spans the doc: {c:?}"
);
}
#[test]
fn bare_word_in_paragraph_expands_to_root() {
let src = "hello world\n";
let c = chain(src, 0, 2);
assert_strictly_nested(&c);
assert_eq!(c[0], ((0, 0), (0, 5)), "innermost is the word: {c:?}");
assert_eq!(
*c.last().unwrap(),
((0, 0), (1, 0)),
"root spans the doc: {c:?}"
);
}
#[test]
fn out_of_range_position_clamps_and_ends_at_root() {
let src = "\\section{A}\n";
let c = chain(src, 99, 0);
assert_strictly_nested(&c);
assert_eq!(
*c.last().unwrap(),
((0, 0), (1, 0)),
"root spans the doc: {c:?}"
);
}
#[test]
fn empty_document_yields_a_single_empty_range() {
let c = chain("", 0, 0);
assert_eq!(c.len(), 1, "only the root range: {c:?}");
assert_eq!(c[0], ((0, 0), (0, 0)), "empty root: {c:?}");
}
#[test]
fn multiple_positions_map_one_to_one() {
let src = "\\section{A}\n\\section{B}\n";
let root = SyntaxNode::new_root(parse(src).green);
let idx = LineIndex::new(src);
let out = selection_ranges(
&root,
&idx,
src,
&[Position::new(0, 9), Position::new(1, 9)],
);
assert_eq!(out.len(), 2, "one chain per input position");
}
}