use anyhow::Result;
use anyhow::bail;
use deno_tower_lsp::lsp_types as lsp;
use deno_tower_lsp::lsp_types::TextEdit;
use similar::ChangeTag;
use similar::TextDiff;
use std::collections::HashMap;
use std::time::Duration;
use std::time::Instant;
use text_size::TextRange;
use text_size::TextSize;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Utf16Char {
pub start: TextSize,
pub end: TextSize,
}
impl Utf16Char {
fn len(&self) -> TextSize {
self.end - self.start
}
fn len_utf16(&self) -> usize {
if self.len() == TextSize::from(4) { 2 } else { 1 }
}
}
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct LineIndex {
utf8_offsets: Vec<TextSize>,
utf8_line_ends: Vec<TextSize>,
utf16_lines: HashMap<u32, Vec<Utf16Char>>,
utf16_offsets: Vec<TextSize>,
}
impl LineIndex {
pub fn new(text: &str) -> LineIndex {
let mut utf16_lines = HashMap::new();
let mut utf16_chars = Vec::new();
let mut utf8_offsets = vec![0.into()];
let mut utf8_line_ends = Vec::new();
let mut utf16_offsets = vec![0.into()];
let mut was_last_cr = false;
let mut curr_row = 0.into();
let mut curr_col = 0.into();
let mut curr_offset_u16 = 0.into();
let mut line = 0;
for c in text.chars() {
let c_len = TextSize::of(c);
curr_row += c_len;
curr_offset_u16 += TextSize::from(c.len_utf16() as u32);
if c == '\n' {
let line_ending_len = if was_last_cr { 2 } else { 1 };
utf8_line_ends.push(curr_row - TextSize::from(line_ending_len));
was_last_cr = false;
utf8_offsets.push(curr_row);
utf16_offsets.push(curr_offset_u16);
if !utf16_chars.is_empty() {
utf16_lines.insert(line, utf16_chars);
utf16_chars = Vec::new();
}
curr_col = 0.into();
line += 1;
continue;
}
was_last_cr = c == '\r';
if !c.is_ascii() {
utf16_chars.push(Utf16Char {
start: curr_col,
end: curr_col + c_len,
});
}
curr_col += c_len;
}
utf8_offsets.push(curr_row);
utf8_line_ends.push(curr_row);
utf16_offsets.push(curr_offset_u16);
if !utf16_chars.is_empty() {
utf16_lines.insert(line, utf16_chars);
}
LineIndex {
utf8_offsets,
utf8_line_ends,
utf16_lines,
utf16_offsets,
}
}
pub fn get_text_range(&self, range: lsp::Range) -> Result<TextRange> {
let start = self.offset(range.start);
let end = self.offset(range.end);
if start > end {
bail!("The start of the range was after its end.")
}
Ok(TextRange::new(start, end))
}
pub fn offset(&self, position: lsp::Position) -> TextSize {
let line = position.line as usize;
let Some(line_end) = self.utf8_line_ends.get(line).copied() else {
return self.utf8_line_ends.last().copied().unwrap_or_default();
};
let line_start = u32::from(self.utf8_offsets[line]);
let col = u32::from(self.utf16_to_utf8_col(position.line, position.character));
std::cmp::min(TextSize::from(line_start.saturating_add(col)), line_end)
}
pub fn position_utf16(&self, offset: TextSize) -> lsp::Position {
let line = partition_point(&self.utf16_offsets, |&it| it <= offset) - 1;
let line_start_offset = self.utf16_offsets[line];
let col = offset - line_start_offset;
lsp::Position {
line: line as u32,
character: col.into(),
}
}
pub fn position_utf16_from_utf8_offset(&self, offset: TextSize) -> lsp::Position {
let line = partition_point(&self.utf8_offsets, |&it| it <= offset) - 1;
let col = offset - self.utf8_offsets[line];
lsp::Position {
line: line as u32,
character: self.utf8_to_utf16_col(line as u32, col),
}
}
pub fn last_line(&self) -> u32 {
self.utf8_line_ends.len().saturating_sub(1) as u32
}
fn utf16_to_utf8_col(&self, line: u32, mut col: u32) -> TextSize {
if let Some(utf16_chars) = self.utf16_lines.get(&line) {
for c in utf16_chars {
if col > u32::from(c.start) {
col = col.saturating_add(u32::from(c.len()) - c.len_utf16() as u32);
if col < u32::from(c.end) {
col = c.start.into();
break;
}
} else {
break;
}
}
}
col.into()
}
fn utf8_to_utf16_col(&self, line: u32, col: TextSize) -> u32 {
let mut utf16_col = u32::from(col);
if let Some(utf16_chars) = self.utf16_lines.get(&line) {
for c in utf16_chars {
if col >= c.end {
utf16_col -= u32::from(c.len()) - c.len_utf16() as u32;
} else {
if col > c.start {
utf16_col -= u32::from(col - c.start);
}
break;
}
}
}
utf16_col
}
}
pub fn normalize_to_source_line_endings(source: &str, formatted: String) -> String {
let Some(source_uses_crlf) = detect_crlf(source) else {
return formatted; };
if Some(source_uses_crlf) == detect_crlf(&formatted) {
return formatted;
}
if source_uses_crlf {
formatted.replace("\r\n", "\n").replace('\n', "\r\n")
} else {
formatted.replace("\r\n", "\n")
}
}
fn detect_crlf(text: &str) -> Option<bool> {
match text.find('\n') {
None => None,
Some(0) => Some(false),
Some(index) => Some(text.as_bytes()[index - 1] == b'\r'),
}
}
pub fn get_edits(a: &str, b: &str, line_index: &LineIndex) -> Vec<TextEdit> {
if a == b {
return vec![];
}
if b.chars().filter(|c| *c == '\n').count() > line_index.utf8_offsets.len() * 3 {
return vec![replace_whole_file(a, b, line_index)];
}
let deadline = Instant::now() + Duration::from_millis(500);
let diff = TextDiff::configure().deadline(deadline).diff_chars(a, b);
if Instant::now() >= deadline {
return vec![replace_whole_file(a, b, line_index)];
}
let mut chunks: Vec<(ChangeTag, String)> = Vec::new();
for change in diff.iter_all_changes() {
match chunks.last_mut() {
Some((tag, text)) if *tag == change.tag() => text.push_str(change.value()),
_ => chunks.push((change.tag(), change.value().to_string())),
}
}
let mut text_edits = Vec::<TextEdit>::new();
let mut iter = chunks.iter().peekable();
let mut a_pos = TextSize::from(0);
loop {
let chunk = iter.next();
match chunk {
None => break,
Some((ChangeTag::Equal, e)) => {
a_pos += TextSize::from(e.encode_utf16().count() as u32);
}
Some((ChangeTag::Delete, d)) => {
let start = line_index.position_utf16(a_pos);
a_pos += TextSize::from(d.encode_utf16().count() as u32);
let end = line_index.position_utf16(a_pos);
let range = lsp::Range { start, end };
match iter.peek() {
Some((ChangeTag::Insert, i)) => {
iter.next();
text_edits.push(TextEdit {
range,
new_text: i.to_string(),
});
}
_ => text_edits.push(TextEdit {
range,
new_text: "".to_string(),
}),
}
}
Some((ChangeTag::Insert, i)) => {
let pos = line_index.position_utf16(a_pos);
let range = lsp::Range { start: pos, end: pos };
text_edits.push(TextEdit {
range,
new_text: i.to_string(),
});
}
}
}
text_edits
}
fn replace_whole_file(old_text: &str, new_text: &str, line_index: &LineIndex) -> TextEdit {
TextEdit {
range: lsp::Range {
start: lsp::Position::new(0, 0),
end: line_index.position_utf16_from_utf8_offset(TextSize::of(old_text)),
},
new_text: new_text.to_string(),
}
}
fn partition_point<T, P>(slice: &[T], mut predicate: P) -> usize
where
P: FnMut(&T) -> bool,
{
let mut left = 0;
let mut right = slice.len() - 1;
while left != right {
let mid = left + (right - left) / 2;
let value = unsafe { slice.get_unchecked(mid) };
if predicate(value) {
left = mid + 1;
} else {
right = mid;
}
}
left
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_index() {
let text = "hello\nworld";
let index = LineIndex::new(text);
assert_eq!(index.position_utf16(0.into()), lsp::Position { line: 0, character: 0 });
assert_eq!(index.position_utf16(1.into()), lsp::Position { line: 0, character: 1 });
assert_eq!(index.position_utf16(5.into()), lsp::Position { line: 0, character: 5 });
assert_eq!(index.position_utf16(6.into()), lsp::Position { line: 1, character: 0 });
assert_eq!(index.position_utf16(7.into()), lsp::Position { line: 1, character: 1 });
assert_eq!(index.position_utf16(8.into()), lsp::Position { line: 1, character: 2 });
assert_eq!(index.position_utf16(10.into()), lsp::Position { line: 1, character: 4 });
assert_eq!(index.position_utf16(11.into()), lsp::Position { line: 1, character: 5 });
assert_eq!(index.position_utf16(12.into()), lsp::Position { line: 1, character: 6 });
let text = "\nhello\nworld";
let index = LineIndex::new(text);
assert_eq!(index.position_utf16(0.into()), lsp::Position { line: 0, character: 0 });
assert_eq!(index.position_utf16(1.into()), lsp::Position { line: 1, character: 0 });
assert_eq!(index.position_utf16(2.into()), lsp::Position { line: 1, character: 1 });
assert_eq!(index.position_utf16(6.into()), lsp::Position { line: 1, character: 5 });
assert_eq!(index.position_utf16(7.into()), lsp::Position { line: 2, character: 0 });
}
#[test]
fn test_position_utf16_from_utf8_offset() {
fn position(index: &LineIndex, offset: u32) -> (u32, u32) {
let position = index.position_utf16_from_utf8_offset(offset.into());
(position.line, position.character)
}
let index = LineIndex::new("hello\nworld");
assert_eq!(position(&index, 0), (0, 0));
assert_eq!(position(&index, 5), (0, 5));
assert_eq!(position(&index, 6), (1, 0));
assert_eq!(position(&index, 11), (1, 5));
let index = LineIndex::new("a\u{e9}b\u{1F995}c\r\n\u{1F995}\u{e9}\n");
assert_eq!(position(&index, 0), (0, 0));
assert_eq!(position(&index, 1), (0, 1));
assert_eq!(position(&index, 3), (0, 2));
assert_eq!(position(&index, 4), (0, 3));
assert_eq!(position(&index, 8), (0, 5));
assert_eq!(position(&index, 9), (0, 6));
assert_eq!(position(&index, 10), (0, 7));
assert_eq!(position(&index, 11), (1, 0));
assert_eq!(position(&index, 15), (1, 2));
assert_eq!(position(&index, 17), (1, 3));
assert_eq!(position(&index, 18), (2, 0));
assert_eq!(position(&index, 2), (0, 1));
assert_eq!(position(&index, 5), (0, 3));
assert_eq!(position(&index, 7), (0, 3));
assert_eq!(position(&index, 13), (1, 0));
let text = "a\u{e9}b\u{1F995}c\n\u{1F995}\u{e9}\n\u{30e1} \u{30e1}";
let index = LineIndex::new(text);
for (offset, _) in text.char_indices().chain([(text.len(), ' ')]) {
let offset = TextSize::from(offset as u32);
assert_eq!(index.offset(index.position_utf16_from_utf8_offset(offset)), offset);
}
assert_eq!(position(&LineIndex::new(""), 0), (0, 0));
}
#[test]
fn test_offset_out_of_range() {
fn offset(index: &LineIndex, line: u32, character: u32) -> u32 {
index.offset(lsp::Position { line, character }).into()
}
let index = LineIndex::new("ab\ncd");
assert_eq!(offset(&index, 0, 2), 2);
assert_eq!(offset(&index, 0, 3), 2);
assert_eq!(offset(&index, 0, 100), 2);
assert_eq!(offset(&index, 0, u32::MAX), 2);
assert_eq!(offset(&index, 1, 100), 5);
assert_eq!(offset(&index, 2, 0), 5);
assert_eq!(offset(&index, 100, 100), 5);
let index = LineIndex::new("ab\r\ncd\n");
assert_eq!(offset(&index, 0, 100), 2);
assert_eq!(offset(&index, 1, 100), 6);
assert_eq!(offset(&index, 2, 0), 7);
assert_eq!(offset(&index, 2, 100), 7);
assert_eq!(offset(&index, 3, 0), 7);
let index = LineIndex::new("");
assert_eq!(offset(&index, 0, 0), 0);
assert_eq!(offset(&index, 0, 5), 0);
assert_eq!(offset(&index, 5, 5), 0);
let index = LineIndex::new("a\u{1F995}b\n\u{e9}");
assert_eq!(offset(&index, 0, 1), 1);
assert_eq!(offset(&index, 0, 2), 1);
assert_eq!(offset(&index, 0, 3), 5);
assert_eq!(offset(&index, 0, 4), 6);
assert_eq!(offset(&index, 0, 100), 6);
assert_eq!(offset(&index, 1, 1), 9);
assert_eq!(offset(&index, 1, 100), 9);
}
#[test]
fn test_last_line() {
assert_eq!(LineIndex::new("").last_line(), 0);
assert_eq!(LineIndex::new("ab").last_line(), 0);
assert_eq!(LineIndex::new("ab\ncd").last_line(), 1);
assert_eq!(LineIndex::new("ab\r\ncd\n").last_line(), 2);
assert_eq!(LineIndex::default().last_line(), 0);
}
#[test]
fn test_get_text_range_out_of_range() {
fn range(index: &LineIndex, start: (u32, u32), end: (u32, u32)) -> Result<(u32, u32)> {
let range = index.get_text_range(lsp::Range {
start: lsp::Position {
line: start.0,
character: start.1,
},
end: lsp::Position { line: end.0, character: end.1 },
})?;
Ok((range.start().into(), range.end().into()))
}
let index = LineIndex::new("ab\ncd");
assert_eq!(range(&index, (0, 100), (1, 0)).unwrap(), (2, 3));
assert_eq!(range(&index, (0, 0), (0, 10)).unwrap(), (0, 2));
assert_eq!(range(&index, (0, 0), (100, 0)).unwrap(), (0, 5));
assert!(range(&index, (0, 2), (0, 1)).is_err());
assert!(range(&index, (1, 0), (0, 0)).is_err());
}
#[test]
fn test_char_len() {
assert_eq!('メ'.len_utf8(), 3);
assert_eq!('メ'.len_utf16(), 1);
assert_eq!('编'.len_utf8(), 3);
assert_eq!('编'.len_utf16(), 1);
assert_eq!('🦕'.len_utf8(), 4);
assert_eq!('🦕'.len_utf16(), 2);
}
#[test]
fn test_empty_index() {
let col_index = LineIndex::new(
"
const C: char = 'x';
",
);
assert_eq!(col_index.utf16_lines.len(), 0);
}
#[test]
fn test_single_char() {
let col_index = LineIndex::new(
"
const C: char = 'メ';
",
);
assert_eq!(col_index.utf16_lines.len(), 1);
assert_eq!(col_index.utf16_lines[&1].len(), 1);
assert_eq!(
col_index.utf16_lines[&1][0],
Utf16Char {
start: 17.into(),
end: 20.into()
}
);
assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextSize::from(15));
assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextSize::from(21));
let col_index = LineIndex::new("a𐐏b");
assert_eq!(col_index.utf16_to_utf8_col(0, 3), TextSize::from(5));
}
#[test]
fn test_string() {
let col_index = LineIndex::new(
"
const C: char = \"メ メ\";
",
);
assert_eq!(col_index.utf16_lines.len(), 1);
assert_eq!(col_index.utf16_lines[&1].len(), 2);
assert_eq!(
col_index.utf16_lines[&1][0],
Utf16Char {
start: 17.into(),
end: 20.into()
}
);
assert_eq!(
col_index.utf16_lines[&1][1],
Utf16Char {
start: 21.into(),
end: 24.into()
}
);
assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextSize::from(15));
assert_eq!(col_index.utf16_to_utf8_col(1, 17), TextSize::from(17)); assert_eq!(col_index.utf16_to_utf8_col(1, 18), TextSize::from(20)); assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextSize::from(21));
assert_eq!(col_index.utf16_to_utf8_col(2, 15), TextSize::from(15));
}
#[test]
fn test_get_edits() {
let a = "abcdefg";
let b = "a\nb\nchije\nfg\n";
let actual = get_edits(a, b, &LineIndex::new(a));
assert_eq!(
actual,
vec![
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 0, character: 1 },
end: lsp::Position { line: 0, character: 1 }
},
new_text: "\n".to_string()
},
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 0, character: 2 },
end: lsp::Position { line: 0, character: 2 }
},
new_text: "\n".to_string()
},
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 0, character: 3 },
end: lsp::Position { line: 0, character: 4 }
},
new_text: "hij".to_string()
},
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 0, character: 5 },
end: lsp::Position { line: 0, character: 5 }
},
new_text: "\n".to_string()
},
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 0, character: 7 },
end: lsp::Position { line: 0, character: 7 }
},
new_text: "\n".to_string()
},
]
);
}
#[test]
fn test_replace_whole_file_non_ascii() {
fn end(old_text: &str) -> lsp::Position {
let edit = replace_whole_file(old_text, "new", &LineIndex::new(old_text));
assert_eq!(edit.range.start, lsp::Position { line: 0, character: 0 });
assert_eq!(edit.new_text, "new");
edit.range.end
}
assert_eq!(end("ab\ncd"), lsp::Position { line: 1, character: 2 });
assert_eq!(end("ab\ncd\n"), lsp::Position { line: 2, character: 0 });
assert_eq!(end("\u{e9}\nab"), lsp::Position { line: 1, character: 2 });
assert_eq!(end("a\u{e9}\u{1F995}"), lsp::Position { line: 0, character: 4 });
assert_eq!(end("\u{e9}\u{e9}\u{e9}\n"), lsp::Position { line: 1, character: 0 });
assert_eq!(end("\u{1F995}\r\n\u{e9}b\u{1F995}"), lsp::Position { line: 1, character: 4 });
}
#[test]
fn test_normalize_to_source_line_endings() {
assert_eq!(normalize_to_source_line_endings("a\nb\n", "a\r\nb\r\nc\r\n".to_string()), "a\nb\nc\n");
assert_eq!(normalize_to_source_line_endings("a\r\nb\r\n", "a\nb\nc\n".to_string()), "a\r\nb\r\nc\r\n");
assert_eq!(normalize_to_source_line_endings("a\nb\n", "a\nb\n".to_string()), "a\nb\n");
assert_eq!(normalize_to_source_line_endings("a\r\nb\r\n", "a\r\nb\r\n".to_string()), "a\r\nb\r\n");
assert_eq!(normalize_to_source_line_endings("abc", "abc\r\n".to_string()), "abc\r\n");
assert_eq!(normalize_to_source_line_endings("\nabc", "\r\nabc".to_string()), "\nabc");
}
#[test]
fn test_get_edits_after_normalizing_line_endings() {
let a = "line1\nline2\nline3\n";
let formatted = "line1\r\nline2\r\nline3\r\n".to_string();
let new_text = normalize_to_source_line_endings(a, formatted);
assert_eq!(get_edits(a, &new_text, &LineIndex::new(a)), vec![]);
}
#[test]
fn test_get_edits_mbc() {
let a = "const bar = \"👍🇺🇸😃\";\nconsole.log('hello deno')\n";
let b = "const bar = \"👍🇺🇸😃\";\nconsole.log(\"hello deno\");\n";
let actual = get_edits(a, b, &LineIndex::new(a));
assert_eq!(
actual,
vec![
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 1, character: 12 },
end: lsp::Position { line: 1, character: 13 }
},
new_text: "\"".to_string()
},
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 1, character: 23 },
end: lsp::Position { line: 1, character: 24 }
},
new_text: "\"".to_string()
},
TextEdit {
range: lsp::Range {
start: lsp::Position { line: 1, character: 25 },
end: lsp::Position { line: 1, character: 25 }
},
new_text: ";".to_string()
},
]
)
}
}