solscript_lsp/
document.rs

1//! Document management for the language server
2
3use ropey::Rope;
4use solscript_ast::Program;
5
6/// Represents an open document in the editor
7pub struct Document {
8    /// The document text
9    pub text: String,
10    /// Rope for efficient text manipulation
11    pub rope: Rope,
12    /// Document version
13    pub version: i32,
14    /// Cached parsed AST (if parsing succeeded)
15    pub ast: Option<Program>,
16    /// Parse errors (if any)
17    pub parse_errors: Vec<String>,
18    /// Type check errors (if any)
19    pub type_errors: Vec<solscript_typeck::TypeError>,
20}
21
22impl Document {
23    /// Create a new document
24    pub fn new(text: String, version: i32) -> Self {
25        let rope = Rope::from_str(&text);
26        let mut doc = Self {
27            text: text.clone(),
28            rope,
29            version,
30            ast: None,
31            parse_errors: Vec::new(),
32            type_errors: Vec::new(),
33        };
34        doc.analyze();
35        doc
36    }
37
38    /// Update the document content
39    pub fn update(&mut self, text: String, version: i32) {
40        self.text = text.clone();
41        self.rope = Rope::from_str(&text);
42        self.version = version;
43        self.analyze();
44    }
45
46    /// Analyze the document (parse and type check)
47    fn analyze(&mut self) {
48        self.parse_errors.clear();
49        self.type_errors.clear();
50        self.ast = None;
51
52        // Parse
53        match solscript_parser::parse(&self.text) {
54            Ok(program) => {
55                // Type check
56                if let Err(errors) = solscript_typeck::typecheck(&program, &self.text) {
57                    self.type_errors = errors;
58                }
59                self.ast = Some(program);
60            }
61            Err(e) => {
62                self.parse_errors.push(format!("{:?}", e));
63            }
64        }
65    }
66
67    /// Get the byte offset for a position
68    pub fn offset_at(&self, line: u32, character: u32) -> Option<usize> {
69        let line_idx = line as usize;
70        if line_idx >= self.rope.len_lines() {
71            return None;
72        }
73
74        let line_start = self.rope.line_to_byte(line_idx);
75        let line_text = self.rope.line(line_idx);
76        let char_offset = (character as usize).min(line_text.len_chars());
77
78        Some(line_start + char_offset)
79    }
80
81    /// Get the position for a byte offset
82    pub fn position_at(&self, offset: usize) -> (u32, u32) {
83        let line = self.rope.byte_to_line(offset);
84        let line_start = self.rope.line_to_byte(line);
85        let character = offset - line_start;
86        (line as u32, character as u32)
87    }
88
89    /// Get the word at a position
90    pub fn word_at(&self, line: u32, character: u32) -> Option<String> {
91        let offset = self.offset_at(line, character)?;
92
93        // Find word boundaries
94        let bytes = self.text.as_bytes();
95        let mut start = offset;
96        let mut end = offset;
97
98        // Scan backwards to find start of word
99        while start > 0 && is_identifier_char(bytes[start - 1] as char) {
100            start -= 1;
101        }
102
103        // Scan forwards to find end of word
104        while end < bytes.len() && is_identifier_char(bytes[end] as char) {
105            end += 1;
106        }
107
108        if start < end {
109            Some(self.text[start..end].to_string())
110        } else {
111            None
112        }
113    }
114
115    /// Get the line text at a line number
116    pub fn line_text(&self, line: u32) -> Option<String> {
117        let line_idx = line as usize;
118        if line_idx >= self.rope.len_lines() {
119            return None;
120        }
121        Some(self.rope.line(line_idx).to_string())
122    }
123}
124
125fn is_identifier_char(c: char) -> bool {
126    c.is_alphanumeric() || c == '_'
127}