1use lsp_types::{ClientCapabilities, Position, PositionEncodingKind, Range};
5use nifty_syntax::{FileId, Parsed, Span};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub(crate) enum Encoding {
10 Utf8,
11 Utf16,
12}
13
14impl Encoding {
15 pub(crate) fn negotiate(capabilities: &ClientCapabilities) -> Self {
18 let offers_utf8 = capabilities
19 .general
20 .as_ref()
21 .and_then(|general| general.position_encodings.as_ref())
22 .is_some_and(|encodings| encodings.contains(&PositionEncodingKind::UTF8));
23 if offers_utf8 { Self::Utf8 } else { Self::Utf16 }
24 }
25
26 pub(crate) fn kind(self) -> PositionEncodingKind {
27 match self {
28 Self::Utf8 => PositionEncodingKind::UTF8,
29 Self::Utf16 => PositionEncodingKind::UTF16,
30 }
31 }
32
33 fn units(self, c: char) -> usize {
34 match self {
35 Self::Utf8 => c.len_utf8(),
36 Self::Utf16 => c.len_utf16(),
37 }
38 }
39
40 pub(crate) fn len(self, text: &str) -> u32 {
41 to_u32(text.chars().map(|c| self.units(c)).sum())
42 }
43}
44
45#[derive(Debug)]
46pub(crate) struct Document {
47 pub file: FileId,
48 text: String,
49 line_starts: Vec<usize>,
51 pub parsed: Parsed,
52}
53
54impl Document {
55 pub(crate) fn new(text: String, file: FileId) -> Self {
56 let line_starts = std::iter::once(0)
57 .chain(text.match_indices('\n').map(|(newline, _)| newline + 1))
58 .collect();
59 let parsed = nifty_syntax::parse(&text, file);
60 Self {
61 file,
62 text,
63 line_starts,
64 parsed,
65 }
66 }
67
68 pub(crate) fn source(&self) -> &str {
69 &self.text
70 }
71
72 pub(crate) fn text(&self, span: Span) -> &str {
74 &self.text[span.range()]
75 }
76
77 pub(crate) fn word_start(&self, offset: usize) -> usize {
79 self.text[..offset]
80 .rfind(|c: char| !is_name_char(c))
81 .map_or(0, |boundary| boundary + 1)
82 }
83
84 pub(crate) fn range(&self, span: Span, encoding: Encoding) -> Range {
85 Range::new(
86 self.position(span.start, encoding),
87 self.position(span.end, encoding),
88 )
89 }
90
91 pub(crate) fn position(&self, offset: usize, encoding: Encoding) -> Position {
92 let line = self.line_starts.partition_point(|&start| start <= offset) - 1;
93 let line_text = &self.text[self.line_starts[line]..offset];
94 Position::new(to_u32(line), encoding.len(line_text))
95 }
96
97 pub(crate) fn offset(&self, position: Position, encoding: Encoding) -> usize {
101 let Some(&line_start) = self.line_starts.get(position.line as usize) else {
102 return self.text.len();
103 };
104 let line_end = self.text[line_start..]
105 .find('\n')
106 .map_or(self.text.len(), |newline| line_start + newline);
107 let mut column = 0;
108 for (index, c) in self.text[line_start..line_end].char_indices() {
109 if column >= position.character as usize {
110 return line_start + index;
111 }
112 column += encoding.units(c);
113 }
114 line_end
115 }
116}
117
118fn is_name_char(c: char) -> bool {
120 c.is_ascii_alphanumeric() || c == '_'
121}
122
123pub(crate) fn to_u32(value: usize) -> u32 {
125 u32::try_from(value).unwrap_or(u32::MAX)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn converts_between_offsets_and_positions_in_both_encodings() {
134 let document = Document::new("a\n\"é𝄞\" x".to_owned(), FileId::default());
136 let x = document.text.find('x').unwrap_or_default();
137 assert_eq!(document.position(x, Encoding::Utf8), Position::new(1, 9));
138 assert_eq!(document.position(x, Encoding::Utf16), Position::new(1, 6));
139 assert_eq!(document.offset(Position::new(1, 9), Encoding::Utf8), x);
140 assert_eq!(document.offset(Position::new(1, 6), Encoding::Utf16), x);
141 }
142
143 #[test]
144 fn clamps_positions_past_the_end_of_a_line_or_file() {
145 let document = Document::new("ab\ncd".to_owned(), FileId::default());
146 assert_eq!(document.offset(Position::new(0, 99), Encoding::Utf16), 2);
147 assert_eq!(document.offset(Position::new(9, 0), Encoding::Utf16), 5);
148 }
149}