use std::{
ops::Range,
sync::atomic::{AtomicUsize, Ordering},
};
use texter::core::text::Text;
use texter_impl::{change::WrapChange, updateable::WrapTree};
use tree_sitter::{Point, Tree};
use crate::errors::{DocumentError, PositionError, TexterError, TreeSitterError};
pub(crate) mod texter_impl;
#[derive(Debug, Clone)]
pub struct Document {
pub texter: Text,
pub tree: Tree,
}
thread_local! {
pub static LAST_LINE: AtomicUsize = const { AtomicUsize::new(0) };
}
impl Document {
pub fn new(texter: Text, tree: Tree) -> Self {
Self { texter, tree }
}
pub fn as_str(&self) -> &str {
&self.texter.text
}
pub fn as_bytes(&self) -> &[u8] {
self.texter.text.as_bytes()
}
pub fn is_empty(&self) -> bool {
self.texter.text.is_empty()
}
pub fn update(
&mut self,
parser: &mut tree_sitter::Parser,
changes: &[lsp_types::TextDocumentContentChangeEvent],
) -> Result<(), DocumentError> {
let mut new_tree = WrapTree::from(&mut self.tree);
for change in changes {
self.texter
.update(WrapChange::from(change).change, &mut new_tree)
.map_err(|e| DocumentError::from(TexterError::from(e)))?;
}
self.tree = parser
.parse(self.texter.text.as_bytes(), Some(&self.tree))
.ok_or_else(|| DocumentError::from(TreeSitterError::TreeSitterParser))?;
Ok(())
}
pub fn node_at_position(&self, position: lsp_types::Position) -> Option<tree_sitter::Node<'_>> {
let position = Point {
row: position.line as usize,
column: position.character as usize,
};
self.tree
.root_node()
.named_descendant_for_point_range(position, position)
}
pub fn node_range_at(&self, offset: usize) -> Option<lsp_types::Range> {
self.tree
.root_node()
.named_descendant_for_byte_range(offset, offset)
.map(|pos| lsp_types::Range {
start: lsp_types::Position {
line: pos.start_position().row as u32,
character: pos.start_position().column as u32,
},
end: lsp_types::Position {
line: pos.end_position().row as u32,
character: pos.end_position().column as u32,
},
})
}
pub fn position_at(&self, offset: usize) -> Result<lsp_types::Position, PositionError> {
let mut last_br_index = 0;
let last_line = LAST_LINE.with(|a| a.load(Ordering::SeqCst));
if self.texter.br_indexes.0.len() == 1 {
return if offset > self.texter.text.len() {
Err(PositionError::LineOutOfBound {
offset,
length: self.texter.text.len(),
})
} else {
Ok(lsp_types::Position {
line: 0,
character: offset as u32,
})
};
}
let start = match self.texter.br_indexes.0.get(last_line) {
Some(&br_index) if offset > br_index && last_line >= 1 => last_line, _ => 1, };
for (i, &br_index) in self.texter.br_indexes.0.iter().skip(start).enumerate() {
if offset <= br_index {
LAST_LINE.with(|a| a.store(i + (start - 1), Ordering::Release));
let col = offset.saturating_sub(last_br_index);
return Ok(lsp_types::Position {
line: (i + (start - 1)) as u32,
character: col as u32,
});
}
last_br_index = br_index + 1; }
if offset <= self.texter.text.len() {
let last_known_col = self.texter.br_indexes.0.iter().len();
let last_br = *self.texter.br_indexes.0.last().unwrap();
Ok(lsp_types::Position {
line: last_known_col.saturating_sub(1) as u32,
character: offset.saturating_sub(last_br) as u32,
})
} else {
Err(PositionError::WrongPosition { offset })
}
}
pub fn range_at(&self, range: Range<usize>) -> Result<lsp_types::Range, PositionError> {
let start = self
.position_at(range.start)
.map_err(|err| PositionError::WrongRange {
range: range.clone(),
position_error: Box::new(err),
})?;
let end = self
.position_at(range.end)
.map_err(|err| PositionError::WrongRange {
range: range.clone(),
position_error: Box::new(err),
})?;
Ok(lsp_types::Range { start, end })
}
pub fn offset_at(&self, position: lsp_types::Position) -> Option<usize> {
let line_index = self.texter.br_indexes.row_start(position.line as usize)?;
let line_str = self.texter.get_row(position.line as usize)?;
let col = position.character as usize;
if col > line_str.len() {
None
} else {
Some(line_index + col)
}
}
}
#[cfg(test)]
mod test {
use super::*;
use lsp_types::Position;
use rstest::{fixture, rstest};
use tree_sitter::Parser;
#[fixture]
fn parser() -> Parser {
let mut p = Parser::new();
p.set_language(&tree_sitter_html::LANGUAGE.into()).unwrap();
p
}
fn get_last_line() -> usize {
use crate::document::LAST_LINE; use std::sync::atomic::Ordering;
LAST_LINE.with(|val| val.load(Ordering::Acquire))
}
#[rstest]
fn position_at(mut parser: Parser) {
let source = "<div>こんにちは\nGoodbye\r\nSee you!\n</div>";
let text = Text::new(source.into());
let document = Document::new(text, parser.parse(source, None).unwrap());
assert_eq!(&document.texter.br_indexes.0, &[0, 20, 29, 38]);
assert_eq!(
document.position_at(0).unwrap(),
Position {
line: 0,
character: 0
}
);
assert_eq!(
document.position_at(11).unwrap(),
Position {
line: 0,
character: 11
}
);
assert_eq!(
document.position_at(21).unwrap(),
Position {
line: 1,
character: 0
}
);
assert_eq!(
document.position_at(28).unwrap(),
Position {
line: 1,
character: 7
}
);
assert_eq!(
document.position_at(30).unwrap(),
Position {
line: 2,
character: 0
}
);
assert_eq!(
document.position_at(40).unwrap(),
Position {
line: 3,
character: 2
}
);
}
#[rstest]
fn position_at_single_line(mut parser: Parser) {
let source = "<div>AREALLYREALLYREALLYLONGTEXT<div>";
let text = Text::new(source.into());
let document = Document::new(text, parser.parse(source, None).unwrap());
assert_eq!(&document.texter.br_indexes.0, &[0]);
assert_eq!(
document.position_at(0).unwrap(),
Position {
line: 0,
character: 0
}
);
assert_eq!(
document.position_at(5).unwrap(),
Position {
line: 0,
character: 5
}
);
assert_eq!(
document.position_at(30).unwrap(),
Position {
line: 0,
character: 30
}
);
}
#[rstest]
fn range_at(mut parser: Parser) {
let source = "<div>こんにちは\nGoodbye\r\nSee you!\n</div>";
let text = Text::new(source.into());
let document = Document::new(text, parser.parse(source, None).unwrap());
assert_eq!(&document.texter.br_indexes.0, &[0, 20, 29, 38]);
assert_eq!(
document.range_at(0..11).unwrap(),
lsp_types::Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: 11
},
}
);
assert_eq!(
document.range_at(15..28).unwrap(),
lsp_types::Range {
start: Position {
line: 0,
character: 15
},
end: Position {
line: 1,
character: 7
},
}
);
assert_eq!(
document.range_at(21..30).unwrap(),
lsp_types::Range {
start: Position {
line: 1,
character: 0
},
end: Position {
line: 2,
character: 0
},
}
);
assert_eq!(
document.range_at(30..35).unwrap(),
lsp_types::Range {
start: Position {
line: 2,
character: 0
},
end: Position {
line: 2,
character: 5
},
}
);
assert_eq!(
document.range_at(35..50),
Err(PositionError::WrongRange {
range: 35..50,
position_error: Box::new(PositionError::WrongPosition { offset: 50 })
})
);
}
#[rstest]
fn range_at_single_line(mut parser: Parser) {
let source = "<div>AREALLYREALLYREALLYLONGTEXT<div>";
let text = Text::new(source.into());
let document = Document::new(text, parser.parse(source, None).unwrap());
assert_eq!(&document.texter.br_indexes.0, &[0]);
assert_eq!(&document.texter.br_indexes.0, &[0]);
assert_eq!(
document.range_at(0..5).unwrap(),
lsp_types::Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: 5
}
}
);
let length = source.len();
assert_eq!(
document.range_at(0..length).unwrap(),
lsp_types::Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: length as u32
}
}
);
assert_eq!(
document.range_at(0..(length + 5)),
Err(PositionError::WrongRange {
range: 0..(length + 5),
position_error: Box::new(PositionError::LineOutOfBound {
offset: 42,
length: 37
})
})
);
}
#[rstest]
fn offset_at(mut parser: Parser) {
let source = "Apples\nBashdjad\nashdkasdh\nasdsad";
let text = Text::new(source.into());
let document = Document::new(text, parser.parse(source, None).unwrap());
assert_eq!(&document.texter.br_indexes.0, &[0, 6, 15, 25]);
assert_eq!(
document.offset_at(Position {
line: 0,
character: 0
}),
Some(0)
);
assert_eq!(
document.offset_at(Position {
line: 0,
character: 5
}),
Some(5)
);
assert_eq!(
document.offset_at(Position {
line: 1,
character: 3
}),
Some(10)
);
assert_eq!(
document.offset_at(Position {
line: 3,
character: 5
}),
Some(31)
);
assert_eq!(
document.offset_at(Position {
line: 10,
character: 0
}),
None
);
assert_eq!(
document.offset_at(Position {
line: 1,
character: 100
}),
None
);
}
#[rstest]
fn line_tracking(mut parser: Parser) {
let source = "one\nline two\nline three\n";
let text = Text::new(source.into());
let document = Document::new(text, parser.parse(source, None).unwrap());
let pos1 = document.position_at(2).unwrap();
assert_eq!(pos1.line, 0);
assert_eq!(get_last_line(), 0);
let pos2 = document.position_at(6).unwrap();
assert_eq!(pos2.line, 1);
assert_eq!(get_last_line(), 1);
let pos3 = document.position_at(18).unwrap();
assert_eq!(pos3.line, 2);
assert_eq!(get_last_line(), 2);
let pos3 = document.position_at(0).unwrap();
assert_eq!(pos3.line, 0);
assert_eq!(get_last_line(), 0);
}
}