use cpp_code_analysis::file::coordinates::{FileOffset, ReadingOffset};
use cpp_code_analysis::{FileView, VfsFile};
use cpp_parser::LineIndex;
use lsp_types::Position;
pub fn offset_at_position(view: &FileView, position: Position) -> Option<ReadingOffset> {
let line = position.line as usize;
let written = view.written_text().unwrap_or(&view.source);
let body = line_body(written, view.written_lines(), line)?;
let column = character_column(body, position.character as usize);
let in_the_file = view.file_offset_at(line, column)?;
view.reading_offset_of(in_the_file)
}
pub fn file_offset_at_position(view: &FileView, position: Position) -> Option<FileOffset> {
let line = position.line as usize;
let written = view.written_text().unwrap_or(&view.source);
let body = line_body(written, view.written_lines(), line)?;
let column = character_column(body, position.character as usize);
view.file_offset_at(line, column)
}
pub fn position_at_offset(view: &FileView, offset: ReadingOffset) -> Option<Position> {
let in_the_file = view.file_offset_of(offset)?.get();
position_in(
view.written_text().unwrap_or(&view.source),
view.written_lines(),
in_the_file,
)
}
pub fn position_in(text: &str, index: &LineIndex, offset: usize) -> Option<Position> {
let (line, column) = index.position_of(offset, text)?;
let body = line_body(text, index, line)?;
Some(Position::new(line as u32, utf16_column(body, column)))
}
pub fn position_in_file(file: &VfsFile, offset: usize) -> Option<Position> {
position_in(&file.text, &file.line_index, offset)
}
pub fn line_body<'a>(source: &'a str, index: &LineIndex, line: usize) -> Option<&'a str> {
let start = usize::from(index.get_line_offset(line)?);
let end = index
.get_line_offset(line + 1)
.map_or(source.len(), usize::from)
.min(source.len());
Some(without_terminator(source.get(start..end)?))
}
pub fn character_column(body: &str, utf16_column: usize) -> usize {
let mut units = 0;
for (characters, character) in body.chars().enumerate() {
if units >= utf16_column {
return characters;
}
let next = units + character.len_utf16();
if next > utf16_column {
return characters;
}
units = next;
}
body.chars().count()
}
pub fn utf16_column(body: &str, character_column: usize) -> u32 {
body.chars()
.take(character_column)
.map(char::len_utf16)
.sum::<usize>() as u32
}
fn without_terminator(line: &str) -> &str {
let line = line.strip_suffix('\n').unwrap_or(line);
line.strip_suffix('\r').unwrap_or(line)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_body_is_the_line_without_its_break() {
let source = "one\nsecond\r\n";
let index = LineIndex::parse(source);
assert_eq!(line_body(source, &index, 0), Some("one"));
assert_eq!(line_body(source, &index, 1), Some("second"));
assert_eq!(
line_body(source, &index, 2),
Some(""),
"the position after the last newline is a line of its own"
);
assert_eq!(line_body(source, &index, 3), None);
let empty = LineIndex::parse("");
assert_eq!(line_body("", &empty, 0), Some(""));
assert_eq!(line_body("", &empty, 1), None);
}
#[test]
fn a_column_counts_utf16_units_and_not_characters() {
let body = "a😀b";
assert_eq!(utf16_column(body, 0), 0);
assert_eq!(utf16_column(body, 1), 1);
assert_eq!(utf16_column(body, 2), 3, "the emoji cost two units");
assert_eq!(utf16_column(body, 3), 4);
assert_eq!(character_column(body, 0), 0);
assert_eq!(character_column(body, 1), 1);
assert_eq!(character_column(body, 3), 2);
assert_eq!(
character_column(body, 2),
1,
"a column inside the surrogate pair rounds down to the character's start"
);
assert_eq!(
character_column(body, 99),
3,
"a column past the end clamps to the line's end"
);
}
#[test]
fn a_client_position_and_a_byte_offset_agree_through_a_view() {
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
let files =
MemoryFiles::new().with_file("/p/a.cpp", "int x = 1;\n// 😀 emoji\nint y = 2;\n");
let documents = OpenDocuments::new();
let providers = SessionFiles::new(documents, files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.load("/p/a.cpp");
session.want_cooked_reading(std::path::Path::new("/p/a.cpp"));
session.index_everything();
let view = session.view("/p/a.cpp").expect("the file reads");
let written = view.written_text().expect("the fixture has a rendering");
let at = written.find("y = 2").expect("the statement is in the text");
let position = position_in(written, view.written_lines(), at).expect("the offset maps");
assert_eq!(
position,
Position::new(2, 4),
"the emoji is on the line above and shifts nothing"
);
assert_eq!(
offset_at_position(&view, position),
Some(ReadingOffset::new(
view.source
.find("y = 2")
.expect("the statement is in the reading")
))
);
let emoji = written.find('😀').expect("the emoji is in the text");
assert_eq!(
position_in(written, view.written_lines(), emoji),
Some(Position::new(1, 3))
);
assert_eq!(
offset_at_position(&view, Position::new(1, 5)),
Some(ReadingOffset::new(
view.source
.find("int y")
.expect("the statement follows the comment in the reading")
))
);
}
}