Skip to main content

iced_code_editor/canvas_editor/
lsp.rs

1//! Minimal LSP types and helpers used by the editor.
2
3/// A zero-based position in an LSP document.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct LspPosition {
6    /// Zero-based line index.
7    pub line: u32,
8    /// Zero-based character index on the line.
9    pub character: u32,
10}
11
12/// Metadata describing the currently edited document.
13#[derive(Debug, Clone)]
14pub struct LspDocument {
15    /// Document URI.
16    pub uri: String,
17    /// Language identifier for syntax services.
18    pub language_id: String,
19    /// Version number used for LSP change notifications.
20    pub version: i32,
21}
22
23impl LspDocument {
24    /// Creates a new LSP document descriptor with version set to 0.
25    pub fn new(uri: impl Into<String>, language_id: impl Into<String>) -> Self {
26        Self { uri: uri.into(), language_id: language_id.into(), version: 0 }
27    }
28}
29
30/// A text range in an LSP document.
31#[derive(Debug, Clone, Copy)]
32pub struct LspRange {
33    /// Range start (inclusive).
34    pub start: LspPosition,
35    /// Range end (exclusive).
36    pub end: LspPosition,
37}
38
39/// A text change described by a range replacement.
40#[derive(Debug, Clone)]
41pub struct LspTextChange {
42    /// Range replaced by the change.
43    pub range: LspRange,
44    /// Inserted text.
45    pub text: String,
46}
47
48/// LSP client hooks invoked by the editor.
49pub trait LspClient {
50    /// Notifies the client that a document was opened.
51    fn did_open(&mut self, _document: &LspDocument, _text: &str) {}
52    /// Notifies the client that the document changed.
53    fn did_change(
54        &mut self,
55        _document: &LspDocument,
56        _changes: &[LspTextChange],
57    ) {
58    }
59    /// Notifies the client that the document was saved.
60    fn did_save(&mut self, _document: &LspDocument, _text: &str) {}
61    /// Notifies the client that the document was closed.
62    fn did_close(&mut self, _document: &LspDocument) {}
63    /// Requests hover information at the given position.
64    fn request_hover(
65        &mut self,
66        _document: &LspDocument,
67        _position: LspPosition,
68    ) {
69    }
70    /// Requests completion items at the given position.
71    fn request_completion(
72        &mut self,
73        _document: &LspDocument,
74        _position: LspPosition,
75    ) {
76    }
77    /// Requests the definition location(s) for the symbol at the given position.
78    ///
79    /// This method is called when the user triggers a "Go to Definition" action
80    /// (e.g., via Ctrl+Click or a context menu). The client implementation should
81    /// send a `textDocument/definition` request to the LSP server.
82    fn request_definition(
83        &mut self,
84        _document: &LspDocument,
85        _position: LspPosition,
86    ) {
87    }
88}
89
90/// Computes a minimal text change between two snapshots.
91///
92/// Returns `None` when the input strings are identical.
93pub fn compute_text_change(old: &str, new: &str) -> Option<LspTextChange> {
94    if old == new {
95        return None;
96    }
97
98    let old_chars: Vec<char> = old.chars().collect();
99    let new_chars: Vec<char> = new.chars().collect();
100    let old_len = old_chars.len();
101    let new_len = new_chars.len();
102
103    let mut prefix = 0;
104    while prefix < old_len
105        && prefix < new_len
106        && old_chars[prefix] == new_chars[prefix]
107    {
108        prefix += 1;
109    }
110
111    let mut suffix = 0;
112    while suffix < old_len.saturating_sub(prefix)
113        && suffix < new_len.saturating_sub(prefix)
114        && old_chars[old_len - 1 - suffix] == new_chars[new_len - 1 - suffix]
115    {
116        suffix += 1;
117    }
118
119    let removed_len = old_len.saturating_sub(prefix + suffix);
120    let inserted: String =
121        new_chars[prefix..new_len.saturating_sub(suffix)].iter().collect();
122
123    let start = position_for_char_index(old, prefix);
124    let end = position_for_char_index(old, prefix + removed_len);
125
126    Some(LspTextChange { range: LspRange { start, end }, text: inserted })
127}
128
129/// Converts a character index into a line/character position.
130fn position_for_char_index(text: &str, target_index: usize) -> LspPosition {
131    let mut line: u32 = 0;
132    let mut character: u32 = 0;
133    for (index, ch) in text.chars().enumerate() {
134        if index == target_index {
135            return LspPosition { line, character };
136        }
137        if ch == '\n' {
138            line = line.saturating_add(1);
139            character = 0;
140        } else {
141            character = character.saturating_add(1);
142        }
143    }
144
145    LspPosition { line, character }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_compute_text_change_none_when_equal() {
154        let change = compute_text_change("abc", "abc");
155        assert!(change.is_none());
156    }
157
158    #[test]
159    fn test_compute_text_change_insertion() {
160        let change = compute_text_change("abc", "abXc");
161        assert!(change.is_some());
162        if let Some(change) = change {
163            assert_eq!(change.text, "X");
164            assert_eq!(
165                change.range.start,
166                LspPosition { line: 0, character: 2 }
167            );
168            assert_eq!(change.range.end, LspPosition { line: 0, character: 2 });
169        }
170    }
171
172    #[test]
173    fn test_compute_text_change_deletion_across_lines() {
174        let change = compute_text_change("a\nbc", "a\nc");
175        assert!(change.is_some());
176        if let Some(change) = change {
177            assert_eq!(change.text, "");
178            assert_eq!(
179                change.range.start,
180                LspPosition { line: 1, character: 0 }
181            );
182            assert_eq!(change.range.end, LspPosition { line: 1, character: 1 });
183        }
184    }
185
186    #[test]
187    fn test_position_for_char_index_end_of_text() {
188        let pos = position_for_char_index("a\nb", 3);
189        assert_eq!(pos, LspPosition { line: 1, character: 1 });
190    }
191}