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 From<&SourceRange> for miette::SourceSpan {
25    fn from(source_range: &SourceRange) -> Self {
26        let length = source_range.end() - source_range.start();
27        let start = miette::SourceOffset::from(source_range.start());
28        Self::new(start, length)
29    }
30}
31
32impl From<SourceRange> for miette::SourceSpan {
33    fn from(source_range: SourceRange) -> Self {
34        Self::from(&source_range)
35    }
36}
37
38impl SourceRange {
39    /// Create a new source range.
40    pub fn new(start: usize, end: usize, module_id: ModuleId) -> Self {
41        Self([start, end, module_id.as_usize()])
42    }
43
44    /// A source range that doesn't correspond to any source code.
45    pub fn synthetic() -> Self {
46        Self::default()
47    }
48
49    /// True if this is a source range that doesn't correspond to any source
50    /// code.
51    pub fn is_synthetic(&self) -> bool {
52        self.start() == 0 && self.end() == 0
53    }
54
55    /// Get the start of the range.
56    pub fn start(&self) -> usize {
57        self.0[0]
58    }
59
60    /// Get the start of the range as a zero-length SourceRange, effectively collapse `self` to it's
61    /// start.
62    pub fn start_as_range(&self) -> Self {
63        Self([self.0[0], self.0[0], self.0[2]])
64    }
65
66    /// Get the end of the range.
67    pub fn end(&self) -> usize {
68        self.0[1]
69    }
70
71    /// Get the module ID of the range.
72    pub fn module_id(&self) -> ModuleId {
73        ModuleId::from_usize(self.0[2])
74    }
75
76    /// Check if the range contains a position.
77    pub fn contains(&self, pos: usize) -> bool {
78        pos >= self.start() && pos <= self.end()
79    }
80
81    pub fn start_to_lsp_position(&self, code: &str) -> LspPosition {
82        // Calculate the line and column of the error from the source range.
83        // Lines are zero indexed in vscode so we need to subtract 1.
84        let mut line = code.get(..self.start()).unwrap_or_default().lines().count();
85        if line > 0 {
86            line = line.saturating_sub(1);
87        }
88        let column = code[..self.start()].lines().last().map(|l| l.len()).unwrap_or_default();
89
90        LspPosition {
91            line: line as u32,
92            character: column as u32,
93        }
94    }
95
96    pub fn end_to_lsp_position(&self, code: &str) -> LspPosition {
97        let lines = code.get(..self.end()).unwrap_or_default().lines();
98        if lines.clone().count() == 0 {
99            return LspPosition { line: 0, character: 0 };
100        }
101
102        // Calculate the line and column of the error from the source range.
103        // Lines are zero indexed in vscode so we need to subtract 1.
104        let line = lines.clone().count() - 1;
105        let column = lines.last().map(|l| l.len()).unwrap_or_default();
106
107        LspPosition {
108            line: line as u32,
109            character: column as u32,
110        }
111    }
112
113    pub fn to_lsp_range(&self, code: &str) -> LspRange {
114        let start = self.start_to_lsp_position(code);
115        let end = self.end_to_lsp_position(code);
116        LspRange { start, end }
117    }
118}