Skip to main content

shuck_server/
edit.rs

1#![allow(dead_code)]
2
3mod range;
4mod text_document;
5
6use lsp_types::{PositionEncodingKind, Url};
7use shuck_ast::TextRange;
8
9pub(crate) use range::RangeExt;
10pub(crate) use text_document::DocumentVersion;
11pub use text_document::TextDocument;
12
13#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
14pub enum PositionEncoding {
15    #[default]
16    UTF16,
17    UTF32,
18    UTF8,
19}
20
21#[derive(Clone, Debug)]
22pub enum DocumentKey {
23    Text(Url),
24}
25
26impl DocumentKey {
27    pub(crate) fn into_url(self) -> Url {
28        match self {
29            Self::Text(url) => url,
30        }
31    }
32}
33
34impl std::fmt::Display for DocumentKey {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::Text(url) => url.fmt(f),
38        }
39    }
40}
41
42impl From<PositionEncoding> for PositionEncodingKind {
43    fn from(value: PositionEncoding) -> Self {
44        match value {
45            PositionEncoding::UTF8 => PositionEncodingKind::UTF8,
46            PositionEncoding::UTF16 => PositionEncodingKind::UTF16,
47            PositionEncoding::UTF32 => PositionEncodingKind::UTF32,
48        }
49    }
50}
51
52impl TryFrom<&PositionEncodingKind> for PositionEncoding {
53    type Error = ();
54
55    fn try_from(value: &PositionEncodingKind) -> Result<Self, Self::Error> {
56        Ok(if value == &PositionEncodingKind::UTF8 {
57            Self::UTF8
58        } else if value == &PositionEncodingKind::UTF16 {
59            Self::UTF16
60        } else if value == &PositionEncodingKind::UTF32 {
61            Self::UTF32
62        } else {
63            return Err(());
64        })
65    }
66}
67
68fn clamp_offset_to_char_boundary(text: &str, offset: usize) -> usize {
69    let mut clamped = offset.min(text.len());
70    while clamped > 0 && !text.is_char_boundary(clamped) {
71        clamped -= 1;
72    }
73    clamped
74}
75
76fn offset_to_position(
77    text: &str,
78    index: &shuck_indexer::LineIndex,
79    offset: usize,
80    encoding: PositionEncoding,
81) -> lsp_types::Position {
82    let offset = clamp_offset_to_char_boundary(text, offset);
83    let line = index.line_number(shuck_ast::TextSize::new(offset as u32));
84    let line_start = index.line_start(line).map(usize::from).unwrap_or_default();
85    let prefix = &text[line_start..offset];
86    let character = match encoding {
87        PositionEncoding::UTF8 => prefix.len(),
88        PositionEncoding::UTF16 => prefix.encode_utf16().count(),
89        PositionEncoding::UTF32 => prefix.chars().count(),
90    };
91
92    lsp_types::Position {
93        line: u32::try_from(line.saturating_sub(1)).unwrap_or(u32::MAX),
94        character: u32::try_from(character).unwrap_or(u32::MAX),
95    }
96}
97
98fn position_to_offset(
99    text: &str,
100    index: &shuck_indexer::LineIndex,
101    position: lsp_types::Position,
102    encoding: PositionEncoding,
103) -> usize {
104    let line = usize::try_from(position.line).unwrap_or(usize::MAX) + 1;
105    let line = line.min(index.line_count());
106    let line_start = index
107        .line_start(line)
108        .map(usize::from)
109        .unwrap_or(text.len());
110    let line_end = index
111        .line_range(line, text)
112        .map(|range| usize::from(range.end()))
113        .unwrap_or(text.len());
114    let line_text = &text[line_start..line_end];
115    let target = usize::try_from(position.character).unwrap_or(usize::MAX);
116
117    let relative = match encoding {
118        PositionEncoding::UTF8 => target.min(line_text.len()),
119        PositionEncoding::UTF16 => {
120            let mut units = 0usize;
121            let mut offset = line_text.len();
122            for (idx, ch) in line_text.char_indices() {
123                if units >= target {
124                    offset = idx;
125                    break;
126                }
127                units += ch.len_utf16();
128            }
129            if units < target {
130                line_text.len()
131            } else {
132                offset
133            }
134        }
135        PositionEncoding::UTF32 => {
136            let mut chars = 0usize;
137            let mut offset = line_text.len();
138            for (idx, _) in line_text.char_indices() {
139                if chars >= target {
140                    offset = idx;
141                    break;
142                }
143                chars += 1;
144            }
145            if chars < target {
146                line_text.len()
147            } else {
148                offset
149            }
150        }
151    };
152
153    clamp_offset_to_char_boundary(line_text, relative) + line_start
154}
155
156pub(crate) fn to_text_range(
157    range: &lsp_types::Range,
158    text: &str,
159    index: &shuck_indexer::LineIndex,
160    encoding: PositionEncoding,
161) -> TextRange {
162    let start = position_to_offset(text, index, range.start, encoding);
163    let end = position_to_offset(text, index, range.end, encoding);
164    TextRange::new(
165        shuck_ast::TextSize::new(start.min(end) as u32),
166        shuck_ast::TextSize::new(end.max(start) as u32),
167    )
168}
169
170pub(crate) fn to_lsp_range(
171    range: TextRange,
172    text: &str,
173    index: &shuck_indexer::LineIndex,
174    encoding: PositionEncoding,
175) -> lsp_types::Range {
176    lsp_types::Range {
177        start: offset_to_position(text, index, usize::from(range.start()), encoding),
178        end: offset_to_position(text, index, usize::from(range.end()), encoding),
179    }
180}