ad_editor/lsp/
capabilities.rs

1//! Capability based logic
2use crate::{
3    buffer::Buffer,
4    exec::{Addr, AddrBase},
5    lsp::Pos,
6};
7use lsp_types::{InitializeResult, Location, Position, PositionEncodingKind, ServerCapabilities};
8use tracing::warn;
9
10#[derive(Debug)]
11#[allow(dead_code)]
12pub(crate) struct Capabilities {
13    inner: ServerCapabilities,
14    pub(super) position_encoding: PositionEncoding,
15}
16
17impl Capabilities {
18    pub(crate) fn try_new(res: InitializeResult) -> Option<Self> {
19        let position_encoding = match &res.capabilities.position_encoding {
20            Some(p) if *p == PositionEncodingKind::UTF8 => PositionEncoding::Utf8,
21            Some(p) if *p == PositionEncodingKind::UTF16 => PositionEncoding::Utf16,
22            Some(p) if *p == PositionEncodingKind::UTF32 => PositionEncoding::Utf32,
23            None => PositionEncoding::Utf16, // see quote from the spec below
24
25            Some(p) => {
26                warn!(
27                    "LSP provided unknown position encoding: {p:?} {:?}",
28                    res.server_info
29                );
30                return None;
31            }
32        };
33
34        Some(Self {
35            inner: res.capabilities,
36            position_encoding,
37        })
38    }
39
40    pub(crate) fn as_pretty_json(&self) -> Option<String> {
41        serde_json::to_string_pretty(&self.inner).ok()
42    }
43}
44
45// NOTE: The LSP spec explicitly calls out needing to support \n, \r and \r\n line
46//       endings which ad doesn't do. Files using \r or \r\n will likely result in
47//       malformed positions.
48
49/// From the LSP 3.17 spec:
50///
51/// The position encodings supported by the client. Client and server
52/// have to agree on the same position encoding to ensure that offsets
53/// (e.g. character position in a line) are interpreted the same on both
54/// side.
55///
56/// To keep the protocol backwards compatible the following applies: if
57/// the value 'utf-16' is missing from the array of position encodings
58/// servers can assume that the client supports UTF-16. UTF-16 is
59/// therefore a mandatory encoding.
60///
61/// If omitted it defaults to ['utf-16'].
62///
63/// Implementation considerations: since the conversion from one encoding
64/// into another requires the content of the file / line the conversion
65/// is best done where the file is read which is usually on the server
66/// side.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
68pub(crate) enum PositionEncoding {
69    /// Raw bytes
70    Utf8,
71    /// Javascript / MS
72    Utf16,
73    /// Unicode code points
74    Utf32,
75}
76
77impl PositionEncoding {
78    pub(crate) fn parse_lsp_position(&self, b: &Buffer, pos: Position) -> (usize, usize) {
79        let pos_line = pos.line as usize;
80        if pos_line > b.len_lines() - 1 {
81            warn!("LSP position out of bounds, clamping to EOF");
82            return (b.len_lines().saturating_sub(1), b.len_chars());
83        }
84
85        match self {
86            Self::Utf8 => {
87                let line_start = b.txt.line_to_char(pos.line as usize);
88                let byte_idx = b.txt.char_to_byte(line_start + pos.character as usize);
89                let col = b.txt.raw_byte_to_char(byte_idx);
90
91                (pos.line as usize, col)
92            }
93
94            Self::Utf16 => {
95                let slice = b.txt.line(pos.line as usize);
96                let mut character = pos.character as usize;
97                let mut col = 0;
98                for (idx, ch) in slice.chars().enumerate() {
99                    let n = ch.len_utf16();
100                    col = idx;
101                    character -= n;
102                    if character == 0 {
103                        break;
104                    }
105                }
106
107                (pos.line as usize, col)
108            }
109
110            Self::Utf32 => (pos.line as usize, pos.character as usize),
111        }
112    }
113
114    pub(super) fn buffer_pos(&self, b: &Buffer) -> Pos {
115        let file = b.full_name();
116        let (y, x) = b.dot.active_cur().as_yx(b);
117        let (line, character) = self.lsp_position(b, y, x);
118
119        Pos::new(file, line, character)
120    }
121
122    fn lsp_position(&self, b: &Buffer, line: usize, col: usize) -> (u32, u32) {
123        match self {
124            Self::Utf8 => {
125                let line_start = b.txt.line_to_char(line);
126                let start_idx = b.txt.char_to_byte(line_start);
127                let character = b.txt.char_to_byte(line_start + col) - start_idx;
128
129                (line as u32, character as u32)
130            }
131
132            Self::Utf16 => {
133                let slice = b.txt.line(line);
134                let mut character = 0;
135                for ch in slice.chars().take(col) {
136                    character += ch.len_utf16();
137                }
138
139                (line as u32, character as u32)
140            }
141
142            Self::Utf32 => (line as u32, col as u32),
143        }
144    }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
148pub struct Coords {
149    encoding: PositionEncoding,
150    start: Position,
151    end: Position,
152}
153
154impl Coords {
155    pub(crate) fn new(loc: Location, encoding: PositionEncoding) -> (String, Self) {
156        let filepath = loc
157            .uri
158            .to_string()
159            .strip_prefix("file://")
160            .unwrap()
161            .to_owned();
162
163        let coords = Coords {
164            encoding,
165            start: loc.range.start,
166            end: loc.range.end,
167        };
168
169        (filepath, coords)
170    }
171
172    pub fn line(&self) -> u32 {
173        self.start.line
174    }
175
176    pub fn as_addr(&self, b: &Buffer) -> Addr {
177        let (sr, sc) = self.encoding.parse_lsp_position(b, self.start);
178        let (er, ec) = self.encoding.parse_lsp_position(b, self.end);
179
180        Addr::Compound(
181            AddrBase::LineAndColumn(sr, sc).into(),
182            AddrBase::LineAndColumn(er, ec).into(),
183        )
184    }
185}