use crate::{
buffer::Buffer,
exec::{Addr, AddrBase},
lsp::Pos,
};
use lsp_types::{InitializeResult, Location, Position, PositionEncodingKind, ServerCapabilities};
use tracing::warn;
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct Capabilities {
inner: ServerCapabilities,
pub(super) position_encoding: PositionEncoding,
}
impl Capabilities {
pub(crate) fn try_new(res: InitializeResult) -> Option<Self> {
let position_encoding = match &res.capabilities.position_encoding {
Some(p) if *p == PositionEncodingKind::UTF8 => PositionEncoding::Utf8,
Some(p) if *p == PositionEncodingKind::UTF16 => PositionEncoding::Utf16,
Some(p) if *p == PositionEncodingKind::UTF32 => PositionEncoding::Utf32,
None => PositionEncoding::Utf16,
Some(p) => {
warn!(
"LSP provided unknown position encoding: {p:?} {:?}",
res.server_info
);
return None;
}
};
Some(Self {
inner: res.capabilities,
position_encoding,
})
}
pub(crate) fn as_pretty_json(&self) -> Option<String> {
serde_json::to_string_pretty(&self.inner).ok()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum PositionEncoding {
Utf8,
Utf16,
Utf32,
}
impl PositionEncoding {
pub(crate) fn parse_lsp_position(&self, b: &Buffer, pos: Position) -> (usize, usize) {
let pos_line = pos.line as usize;
if pos_line > b.len_lines() - 1 {
warn!("LSP position out of bounds, clamping to EOF");
return (b.len_lines().saturating_sub(1), b.len_chars());
}
match self {
Self::Utf8 => {
let line_start = b.txt.line_to_byte(pos.line as usize);
let col = b.txt.chars_in_raw_range(
b.txt.byte_to_raw_byte(line_start),
b.txt.byte_to_raw_byte(line_start + pos.character as usize),
);
(pos.line as usize, col)
}
Self::Utf16 => {
let slice = b.txt.line(pos.line as usize);
let mut remaining = pos.character as usize;
let mut col = 0;
for ch in slice.chars() {
if remaining == 0 {
break;
}
remaining = remaining.saturating_sub(ch.len_utf16());
col += 1;
}
if remaining > 0 {
col = slice.chars().count(); }
(pos.line as usize, col)
}
Self::Utf32 => (pos.line as usize, pos.character as usize),
}
}
pub(super) fn buffer_pos(&self, b: &Buffer) -> Pos {
let file = b.full_name();
let (y, x) = b.dot.active_cur().as_yx(b);
let (line, character) = self.lsp_position(b, y, x);
Pos::new(file, line, character)
}
fn lsp_position(&self, b: &Buffer, line: usize, col: usize) -> (u32, u32) {
match self {
Self::Utf8 => {
let line_start = b.txt.line_to_char(line);
let start_idx = b.txt.char_to_byte(line_start);
let character = b.txt.char_to_byte(line_start + col) - start_idx;
(line as u32, character as u32)
}
Self::Utf16 => {
let slice = b.txt.line(line);
let mut character = 0;
for ch in slice.chars().take(col) {
character += ch.len_utf16();
}
(line as u32, character as u32)
}
Self::Utf32 => (line as u32, col as u32),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Coords {
pub(crate) start: Position,
pub(crate) end: Position,
pub(crate) encoding: PositionEncoding,
}
impl Coords {
pub(crate) fn new(loc: Location, encoding: PositionEncoding) -> (String, Self) {
let filepath = loc
.uri
.to_string()
.strip_prefix("file://")
.unwrap()
.to_owned();
let coords = Coords {
start: loc.range.start,
end: loc.range.end,
encoding,
};
(filepath, coords)
}
pub(crate) fn new_from_range(r: lsp_types::Range, encoding: PositionEncoding) -> Self {
Coords {
start: r.start,
end: r.end,
encoding,
}
}
pub(crate) fn new_from_pos(pos: Pos, encoding: PositionEncoding) -> Self {
Coords {
start: lsp_types::Position::new(pos.line, pos.character),
end: lsp_types::Position::new(pos.line, pos.character),
encoding,
}
}
pub fn line(&self) -> u32 {
self.start.line
}
pub(crate) fn as_addr(&self, b: &Buffer) -> Addr {
let (row_start, col_start) = self.encoding.parse_lsp_position(b, self.start);
let (mut row_end, mut col_end) = self.encoding.parse_lsp_position(b, self.end);
if (row_start, col_start) == (row_end, col_end) {
Addr::Simple(AddrBase::LineAndColumn(row_start, col_start).into())
} else if row_start == row_end && col_end == col_start + 1 {
Addr::Compound(
AddrBase::LineAndColumn(row_start, col_start).into(),
AddrBase::LineAndColumn(row_start, col_start).into(),
)
} else {
if col_end == 0 && !b.txt.line_is_blank(row_end) {
row_end = row_end.saturating_sub(1);
col_end = b.txt.line(row_end).chars().count();
}
Addr::Compound(
AddrBase::LineAndColumn(row_start, col_start).into(),
AddrBase::LineAndColumn(row_end, col_end.saturating_sub(1)).into(),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::Position;
use simple_test_case::test_case;
#[test_case("hello", 0, 0; "ascii position 0")]
#[test_case("hello", 1, 1; "ascii position 1")]
#[test_case("hello", 5, 5; "ascii position 5")]
#[test_case("a😀b", 0, 0; "emoji position 0")]
#[test_case("a😀b", 1, 1; "emoji position 1 after a before emoji")]
#[test_case("a😀b", 3, 2; "emoji position 3 after emoji before b")]
#[test_case("a😀b", 4, 3; "emoji position 4 after b")]
#[test]
fn parse_lsp_position_utf16_ascii(content: &str, lsp_char: u32, expected_col: usize) {
let b = Buffer::new_virtual(0, "test", content, Default::default());
let pos = Position {
line: 0,
character: lsp_char,
};
let (line, col) = PositionEncoding::Utf16.parse_lsp_position(&b, pos);
assert_eq!(line, 0);
assert_eq!(col, expected_col);
}
}