Skip to main content

blues_lsp/syntax/
location.rs

1use std::{cmp::Ordering, ops::Range, sync::Arc};
2
3use tracing::debug;
4
5use crate::project::file::FileContents;
6
7#[derive(Debug)]
8pub struct OriginTable {
9    /// Indexed by `OriginId`
10    table: Vec<Origin>,
11}
12
13impl OriginTable {
14    pub fn new(root: Arc<FileContents>) -> Self {
15        Self {
16            table: vec![
17                Origin {
18                    origin: 0,
19                    span: FileSpan::default(),
20                    file: root.clone(),
21                    reason: OriginReason::Root,
22                },
23                Origin {
24                    origin: 0,
25                    span: FileSpan::default(),
26                    file: root.clone(),
27                    reason: OriginReason::Builtin,
28                },
29                Origin {
30                    origin: 0,
31                    span: FileSpan::default(),
32                    file: root,
33                    reason: OriginReason::Cli,
34                },
35            ],
36        }
37    }
38
39    pub fn get(&self, id: OriginId) -> &Origin {
40        &self.table[id as usize]
41    }
42
43    pub fn add(&mut self, origin: Origin) -> OriginId {
44        let id = self.table.len() as u32;
45        self.table.push(origin);
46        id
47    }
48
49    pub fn root(&self) -> Arc<FileContents> {
50        self.get(ROOT_ORIGIN).file.clone()
51    }
52}
53
54/// Id for OriginTable
55pub type OriginId = u32;
56pub const ROOT_ORIGIN: OriginId = 0;
57pub const BUILTIN_ORIGIN: OriginId = 1;
58pub const CLI_ORIGIN: OriginId = 2;
59
60#[derive(Debug)]
61pub struct Origin {
62    pub origin: OriginId,
63
64    /// Span in parrent origin
65    pub span: FileSpan,
66
67    /// New source, if needed.
68    /// Otherwise same as parent origin
69    // TODO: Turn this into a handle (path)
70    pub file: Arc<FileContents>,
71
72    pub reason: OriginReason,
73}
74
75#[derive(Debug, Clone, Copy)]
76pub enum OriginReason {
77    /// Root file of the CU
78    Root,
79    /// Result of macro expansion
80    Macro,
81    /// Macro interpolation (arguments)
82    MacroInterp,
83    /// File inlcude
84    Include,
85    /// Built-in (`bluespec and `BLUESPEC)
86    Builtin,
87    /// From CLI args
88    Cli,
89}
90
91#[derive(Debug, Clone, Copy, Default)]
92pub struct Span {
93    pub origin: OriginId,
94    pub text_span: FileSpan,
95}
96
97impl Span {
98    pub fn cross_origin(l: Pos, r: Pos, ot: &OriginTable) -> Self {
99        // TODO: In case we have a span across origins, we should
100        // probably find closest parent origin and go from there...
101        if l.origin != r.origin {
102            // Temp placeholder until we do just that:
103            debug!("TODO: unhandled cross-origin span between: {l:?}, {r:?}");
104            return Self {
105                origin: l.origin,
106                text_span: FileSpan::empty_at(l.text_pos),
107            };
108        }
109
110        let file = &ot.get(l.origin).file;
111
112        Self {
113            origin: l.origin,
114            text_span: FileSpan::from_range(l.text_pos..r.text_pos, file),
115        }
116    }
117
118    pub fn empty_at(pos: Pos) -> Self {
119        Self {
120            origin: pos.origin,
121            text_span: FileSpan::empty_at(pos.text_pos),
122        }
123    }
124
125    pub fn empty_at_start(self) -> Self {
126        Self {
127            origin: self.origin,
128            text_span: self.text_span.empty_at_start(),
129        }
130    }
131
132    pub fn empty_at_end(self, ot: &OriginTable) -> Self {
133        Self {
134            origin: self.origin,
135            text_span: self.text_span.empty_at_end(&ot.get(self.origin).file),
136        }
137    }
138
139    pub fn start(&self) -> Pos {
140        Pos {
141            origin: self.origin,
142            text_pos: self.text_span.start,
143        }
144    }
145
146    pub fn end(&self, ot: &OriginTable) -> Pos {
147        Pos {
148            origin: self.origin,
149            text_pos: self.text_span.end(&ot.get(self.origin).file),
150        }
151    }
152
153    pub fn contains(&self, pos: &Pos, ot: &OriginTable) -> Option<bool> {
154        let start = self.start();
155        let end = self.end(ot);
156
157        Some(start.try_cmp(pos)?.is_le() && pos.try_cmp(&end)?.is_le())
158    }
159
160    pub fn parent_span(&self, ot: &OriginTable) -> Option<Self> {
161        if self.origin == ROOT_ORIGIN {
162            None
163        } else {
164            let origin = ot.get(self.origin);
165            Some(Span {
166                origin: origin.origin,
167                text_span: origin.span,
168            })
169        }
170    }
171
172    pub fn to_lsp(&self, ot: &OriginTable) -> lsp_types::Range {
173        let mut span = *self;
174        while let Some(parent) = span.parent_span(ot) {
175            span = parent;
176        }
177        span.text_span.to_lsp(&ot.get(span.origin).file)
178    }
179
180    pub fn root_span(&self) -> Option<FileSpan> {
181        if self.origin != ROOT_ORIGIN {
182            return None;
183        }
184        Some(self.text_span)
185    }
186}
187
188#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
189pub struct Pos {
190    pub origin: OriginId,
191    pub text_pos: FilePos,
192}
193
194impl Pos {
195    pub const fn zero() -> Self {
196        Pos {
197            origin: ROOT_ORIGIN,
198            text_pos: FilePos {
199                line: 0,
200                col: LspCol(0),
201            },
202        }
203    }
204
205    pub fn from_lsp(lsp: lsp_types::Position) -> Self {
206        Self {
207            origin: ROOT_ORIGIN,
208            text_pos: FilePos::from_lsp(lsp),
209        }
210    }
211
212    pub fn parent_span(&self, ot: &OriginTable) -> Option<Self> {
213        if self.origin == ROOT_ORIGIN {
214            None
215        } else {
216            let origin = ot.get(self.origin);
217            Some(Pos {
218                origin: origin.origin,
219                text_pos: origin.span.start,
220            })
221        }
222    }
223
224    pub fn to_lsp(&self, ot: &OriginTable) -> lsp_types::Position {
225        let mut pos = *self;
226        while let Some(parent) = pos.parent_span(ot) {
227            pos = parent;
228        }
229        pos.text_pos.to_lsp()
230    }
231
232    pub fn at(origin: OriginId, line: u32, col: u32) -> Self {
233        Self {
234            origin,
235            text_pos: FilePos {
236                line,
237                col: LspCol::from_raw(col),
238            },
239        }
240    }
241
242    pub fn try_cmp(&self, other: &Self) -> Option<Ordering> {
243        // TODO: properly handle cross-origin stuff
244        if self.origin != other.origin {
245            None
246        } else {
247            Some(self.text_pos.cmp(&other.text_pos))
248        }
249    }
250}
251
252#[derive(Debug, Clone, Copy, Default)]
253pub struct FileSpan {
254    pub start: FilePos,
255    pub len: LspCol,
256}
257
258impl FileSpan {
259    pub fn empty_at(start: FilePos) -> Self {
260        Self {
261            start,
262            len: LspCol::from_raw(0),
263        }
264    }
265
266    pub fn from_range(range: Range<FilePos>, file: &FileContents) -> Self {
267        let start = range.start;
268        let len = file.pos_diff(range);
269        Self { start, len }
270    }
271
272    pub fn to_lsp(&self, file: &FileContents) -> lsp_types::Range {
273        let start = self.start;
274        let end = self.end(file);
275        lsp_types::Range {
276            start: start.to_lsp(),
277            end: end.to_lsp(),
278        }
279    }
280
281    pub fn end(&self, file: &FileContents) -> FilePos {
282        file.pos_add(self.start, self.len)
283    }
284
285    pub fn empty_at_start(&self) -> Self {
286        FileSpan {
287            start: self.start,
288            len: LspCol::from_raw(0),
289        }
290    }
291
292    pub fn empty_at_end(&self, file: &FileContents) -> Self {
293        FileSpan {
294            start: self.end(file),
295            len: LspCol::from_raw(0),
296        }
297    }
298
299    pub fn overlaps(&self, other: &Self, file: &FileContents) -> bool {
300        let self_end = self.end(file);
301        let other_end = other.end(file);
302        self.start < other_end && self_end > other.start
303    }
304}
305
306#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
307pub struct FilePos {
308    pub line: u32,
309    pub col: LspCol,
310}
311
312impl FilePos {
313    pub fn to_lsp(&self) -> lsp_types::Position {
314        lsp_types::Position {
315            line: self.line,
316            character: self.col.to_lsp(),
317        }
318    }
319
320    pub fn from_lsp(lsp: lsp_types::Position) -> Self {
321        Self {
322            line: lsp.line,
323            col: LspCol::from_raw(lsp.character),
324        }
325    }
326
327    /// Advance possition by given char (eg. increment line count on newline)
328    ///
329    /// NOTE: "\r\n" and "\r" are not handled.
330    /// They should be normalized to "\n" by caller code.
331    pub fn advance(&mut self, char: char) {
332        // Fun fact: this diverges from bsc's implementation for counting
333        // line:col. bsc doesn't properly handle "\r\n" and "\r" line endings,
334        // and treats tabs as converted spaces, for some reason.
335        //
336        // That's fine, we want to match LSP spec here.
337        // TODO: Maybe file a bsc issue.
338        match char {
339            '\r' => unreachable!(),
340            '\n' => {
341                self.line += 1;
342                self.col = LspCol::start();
343            }
344            c => self.col.advance(c),
345        }
346    }
347}
348
349impl PartialOrd for FilePos {
350    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
351        Some(self.cmp(other))
352    }
353}
354
355impl Ord for FilePos {
356    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
357        self.line.cmp(&other.line).then(self.col.cmp(&other.col))
358    }
359}
360
361#[derive(Debug, Clone, Copy, Default)]
362pub struct LineColCounter {
363    pos: FilePos,
364    post_cr: bool,
365}
366
367impl LineColCounter {
368    pub fn start_at(pos: FilePos) -> Self {
369        Self {
370            pos,
371            post_cr: false,
372        }
373    }
374
375    pub fn pos(&self, next: Option<char>) -> FilePos {
376        let mut pos = self.pos;
377        if self.post_cr && next != Some('\n') {
378            pos.advance('\n');
379        }
380        pos
381    }
382
383    pub fn advance(&mut self, c: char) {
384        if self.post_cr && c != '\n' {
385            self.pos.advance('\n');
386        }
387        self.post_cr = false;
388        match c {
389            '\r' => self.post_cr = true,
390            c => self.pos.advance(c),
391        }
392    }
393}
394
395/// Source column, in unit negotiated with LSP client (either UTF-8 or UTF-16 codepoints)
396///
397/// For now only UTF-16 is supported
398#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
399pub struct LspCol(u32);
400
401impl LspCol {
402    pub fn from_raw(col: u32) -> Self {
403        LspCol(col)
404    }
405
406    pub fn to_raw(self) -> u32 {
407        self.0
408    }
409
410    pub fn start() -> Self {
411        LspCol(0)
412    }
413
414    pub fn advance(&mut self, char: char) {
415        self.0 += char.len_utf16() as u32;
416    }
417
418    pub fn to_lsp(&self) -> u32 {
419        self.0
420    }
421}