Skip to main content

gruel_lsp/
document.rs

1//! Document store with incremental text sync (ADR-0091).
2//!
3//! Each open file is represented by a [`DocState`] holding the latest text,
4//! version, and a cached [`crate::position::LineMap`]. Text sync uses
5//! `TextDocumentSyncKind::INCREMENTAL` — patches are applied in place.
6
7use std::path::PathBuf;
8
9use lsp_types::{TextDocumentContentChangeEvent, Url};
10
11use crate::position::{LineMap, PositionEncoding, position_to_byte};
12
13/// State for one open or known document.
14#[derive(Debug, Clone)]
15pub struct DocState {
16    pub uri: Url,
17    pub path: PathBuf,
18    pub text: String,
19    pub version: i32,
20    pub line_map: LineMap,
21    /// True iff the editor currently has a buffer open for this file.
22    pub open: bool,
23}
24
25impl DocState {
26    pub fn new(uri: Url, text: String, version: i32, open: bool) -> Self {
27        let path = uri
28            .to_file_path()
29            .unwrap_or_else(|_| PathBuf::from(uri.path()));
30        let line_map = LineMap::new(&text);
31        Self {
32            uri,
33            path,
34            text,
35            version,
36            line_map,
37            open,
38        }
39    }
40
41    /// Apply an LSP incremental change. Returns true on success.
42    pub fn apply_change(
43        &mut self,
44        change: TextDocumentContentChangeEvent,
45        encoding: PositionEncoding,
46    ) -> bool {
47        match change.range {
48            Some(range) => {
49                let start =
50                    position_to_byte(&self.line_map, &self.text, range.start, encoding) as usize;
51                let end =
52                    position_to_byte(&self.line_map, &self.text, range.end, encoding) as usize;
53                if start > end || end > self.text.len() {
54                    return false;
55                }
56                self.text.replace_range(start..end, &change.text);
57            }
58            None => {
59                // Full-document replace.
60                self.text = change.text;
61            }
62        }
63        self.line_map = LineMap::new(&self.text);
64        true
65    }
66
67    pub fn set_text(&mut self, text: String, version: i32) {
68        self.text = text;
69        self.version = version;
70        self.line_map = LineMap::new(&self.text);
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use lsp_types::{Position, Range};
78
79    fn doc(text: &str) -> DocState {
80        DocState::new(
81            Url::parse("file:///tmp/test.gruel").unwrap(),
82            text.to_string(),
83            1,
84            true,
85        )
86    }
87
88    #[test]
89    fn incremental_replace() {
90        let mut d = doc("fn main() -> i32 { 0 }");
91        let change = TextDocumentContentChangeEvent {
92            range: Some(Range {
93                start: Position {
94                    line: 0,
95                    character: 19,
96                },
97                end: Position {
98                    line: 0,
99                    character: 20,
100                },
101            }),
102            range_length: None,
103            text: "42".to_string(),
104        };
105        assert!(d.apply_change(change, PositionEncoding::Utf8));
106        assert_eq!(d.text, "fn main() -> i32 { 42 }");
107    }
108
109    #[test]
110    fn incremental_insert_then_lines_recompute() {
111        let mut d = doc("a\nb\nc");
112        let change = TextDocumentContentChangeEvent {
113            range: Some(Range {
114                start: Position {
115                    line: 1,
116                    character: 1,
117                },
118                end: Position {
119                    line: 1,
120                    character: 1,
121                },
122            }),
123            range_length: None,
124            text: "\nINSERTED".to_string(),
125        };
126        assert!(d.apply_change(change, PositionEncoding::Utf8));
127        assert_eq!(d.text, "a\nb\nINSERTED\nc");
128        // Lines should be 4 now: ["a", "b", "INSERTED", "c"]
129        assert_eq!(d.line_map.line_count(), 4);
130    }
131
132    #[test]
133    fn full_replace() {
134        let mut d = doc("foo");
135        let change = TextDocumentContentChangeEvent {
136            range: None,
137            range_length: None,
138            text: "totally new".to_string(),
139        };
140        assert!(d.apply_change(change, PositionEncoding::Utf8));
141        assert_eq!(d.text, "totally new");
142    }
143}