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    /// Check if the range contains a position.
98    pub fn contains(&self, pos: usize) -> bool {
99        pos >= self.start() && pos <= self.end()
100    }
101
102    /// Check if the range contains another range.  Modules must match.
103    pub(crate) fn contains_range(&self, other: &Self) -> bool {
104        self.module_id() == other.module_id() && self.start() <= other.start() && self.end() >= other.end()
105    }
106
107    pub fn start_to_lsp_position(&self, code: &str) -> LspPosition {
108        // Calculate the line and column of the error from the source range.
109        // Lines are zero indexed in vscode so we need to subtract 1.
110        let mut line = code.get(..self.start()).unwrap_or_default().lines().count();
111        if line > 0 {
112            line = line.saturating_sub(1);
113        }
114        let column = code[..self.start()].lines().last().map(|l| l.len()).unwrap_or_default();
115
116        LspPosition {
117            line: line as u32,
118            character: column as u32,
119        }
120    }
121
122    pub fn end_to_lsp_position(&self, code: &str) -> LspPosition {
123        let lines = code.get(..self.end()).unwrap_or_default().lines();
124        if lines.clone().count() == 0 {
125            return LspPosition { line: 0, character: 0 };
126        }
127
128        // Calculate the line and column of the error from the source range.
129        // Lines are zero indexed in vscode so we need to subtract 1.
130        let line = lines.clone().count() - 1;
131        let column = lines.last().map(|l| l.len()).unwrap_or_default();
132
133        LspPosition {
134            line: line as u32,
135            character: column as u32,
136        }
137    }
138
139    pub fn to_lsp_range(&self, code: &str) -> LspRange {
140        let start = self.start_to_lsp_position(code);
141        let end = self.end_to_lsp_position(code);
142        LspRange { start, end }
143    }
144}