kcl_lib/
source_range.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
4
5use crate::modules::ModuleId;
6
7/// The first two items are the start and end points (byte offsets from the start of the file).
8/// The third item is whether the source range belongs to the 'main' file, i.e., the file currently
9/// being rendered/displayed in the editor.
10//
11// Don't use a doc comment for the below since the above goes in the website docs.
12// @see isTopLevelModule() in wasm.ts.
13// TODO we need to handle modules better in the frontend.
14#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
15#[ts(export, type = "[number, number, number]")]
16pub struct SourceRange([usize; 3]);
17
18impl From<[usize; 3]> for SourceRange {
19    fn from(value: [usize; 3]) -> Self {
20        Self(value)
21    }
22}
23
24impl Ord for SourceRange {
25    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
26        // Sort by module id first, then by start and end.
27        let module_id_cmp = self.module_id().cmp(&other.module_id());
28        if module_id_cmp != std::cmp::Ordering::Equal {
29            return module_id_cmp;
30        }
31        let start_cmp = self.start().cmp(&other.start());
32        if start_cmp != std::cmp::Ordering::Equal {
33            return start_cmp;
34        }
35        self.end().cmp(&other.end())
36    }
37}
38
39impl PartialOrd for SourceRange {
40    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
41        Some(self.cmp(other))
42    }
43}
44
45impl From<&SourceRange> for miette::SourceSpan {
46    fn from(source_range: &SourceRange) -> Self {
47        let length = source_range.end() - source_range.start();
48        let start = miette::SourceOffset::from(source_range.start());
49        Self::new(start, length)
50    }
51}
52
53impl From<SourceRange> for miette::SourceSpan {
54    fn from(source_range: SourceRange) -> Self {
55        Self::from(&source_range)
56    }
57}
58
59impl SourceRange {
60    /// Create a new source range.
61    pub fn new(start: usize, end: usize, module_id: ModuleId) -> Self {
62        Self([start, end, module_id.as_usize()])
63    }
64
65    /// A source range that doesn't correspond to any source code.
66    pub fn synthetic() -> Self {
67        Self::default()
68    }
69
70    /// True if this is a source range that doesn't correspond to any source
71    /// code.
72    pub fn is_synthetic(&self) -> bool {
73        self.start() == 0 && self.end() == 0
74    }
75
76    /// Get the start of the range.
77    pub fn start(&self) -> usize {
78        self.0[0]
79    }
80
81    /// Get the start of the range as a zero-length SourceRange, effectively collapse `self` to it's
82    /// start.
83    pub fn start_as_range(&self) -> Self {
84        Self([self.0[0], self.0[0], self.0[2]])
85    }
86
87    /// Get the end of the range.
88    pub fn end(&self) -> usize {
89        self.0[1]
90    }
91
92    /// Get the module ID of the range.
93    pub fn module_id(&self) -> ModuleId {
94        ModuleId::from_usize(self.0[2])
95    }
96
97    /// True if this source range is from the top-level module.
98    pub fn is_top_level_module(&self) -> bool {
99        self.module_id().is_top_level()
100    }
101
102    /// Check if the range contains a position.
103    pub fn contains(&self, pos: usize) -> bool {
104        pos >= self.start() && pos <= self.end()
105    }
106
107    /// Check if the range contains another range.  Modules must match.
108    pub(crate) fn contains_range(&self, other: &Self) -> bool {
109        self.module_id() == other.module_id() && self.start() <= other.start() && self.end() >= other.end()
110    }
111
112    pub fn start_to_lsp_position(&self, code: &str) -> LspPosition {
113        // Calculate the line and column of the error from the source range.
114        // Lines are zero indexed in vscode so we need to subtract 1.
115        let mut line = code.get(..self.start()).unwrap_or_default().lines().count();
116        if line > 0 {
117            line = line.saturating_sub(1);
118        }
119        let column = code[..self.start()].lines().last().map(|l| l.len()).unwrap_or_default();
120
121        LspPosition {
122            line: line as u32,
123            character: column as u32,
124        }
125    }
126
127    pub fn end_to_lsp_position(&self, code: &str) -> LspPosition {
128        let lines = code.get(..self.end()).unwrap_or_default().lines();
129        if lines.clone().count() == 0 {
130            return LspPosition { line: 0, character: 0 };
131        }
132
133        // Calculate the line and column of the error from the source range.
134        // Lines are zero indexed in vscode so we need to subtract 1.
135        let line = lines.clone().count() - 1;
136        let column = lines.last().map(|l| l.len()).unwrap_or_default();
137
138        LspPosition {
139            line: line as u32,
140            character: column as u32,
141        }
142    }
143
144    pub fn to_lsp_range(&self, code: &str) -> LspRange {
145        let start = self.start_to_lsp_position(code);
146        let end = self.end_to_lsp_position(code);
147        LspRange { start, end }
148    }
149}