lsp_textdocument/
text_documents.rs

1use crate::FullTextDocument;
2use lsp_types::{
3    notification::{
4        DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification,
5    },
6    DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, PositionEncodingKind,
7    Range, Uri,
8};
9use serde_json::Value;
10use std::collections::BTreeMap;
11
12pub struct TextDocuments {
13    documents: BTreeMap<Uri, FullTextDocument>,
14    default_encoding: PositionEncodingKind,
15}
16
17impl Default for TextDocuments {
18    fn default() -> Self {
19        Self::with_encoding(PositionEncodingKind::UTF16)
20    }
21}
22
23impl TextDocuments {
24    /// Create a text documents
25    ///
26    /// # Examples
27    ///
28    /// Basic usage:
29    ///
30    /// ```
31    /// use lsp_textdocument::TextDocuments;
32    ///
33    /// let text_documents = TextDocuments::new();
34    /// ```
35    pub fn new() -> Self {
36        Self::with_encoding(PositionEncodingKind::UTF16)
37    }
38
39    /// Create a TextDocuments instance with a specific position encoding
40    ///
41    /// This method allows you to specify the position encoding used for character positions
42    /// in text documents. The encoding determines how character offsets are calculated and is
43    /// important for proper LSP communication between client and server.
44    ///
45    /// # Arguments
46    ///
47    /// * `default_encoding` - The position encoding to use. Can be UTF-8, UTF-16, or UTF-32.
48    ///
49    /// # Position Encodings
50    ///
51    /// - **UTF-16**: The default encoding for backward compatibility with LSP 3.16 and earlier.
52    ///   Each UTF-16 code unit counts as one position unit.
53    /// - **UTF-8**: Each byte counts as one position unit. More efficient for ASCII-heavy text.
54    /// - **UTF-32**: Each Unicode code point counts as one position unit.
55    ///
56    /// The encoding should match what was negotiated with the LSP client during initialization.
57    ///
58    /// # Examples
59    ///
60    /// Basic usage with UTF-16 (default):
61    ///
62    /// ```
63    /// use lsp_textdocument::TextDocuments;
64    /// use lsp_types::PositionEncodingKind;
65    ///
66    /// let text_documents = TextDocuments::with_encoding(PositionEncodingKind::UTF16);
67    /// ```
68    ///
69    /// Using UTF-8 encoding for better performance with ASCII text:
70    ///
71    /// ```
72    /// use lsp_textdocument::TextDocuments;
73    /// use lsp_types::PositionEncodingKind;
74    ///
75    /// let text_documents = TextDocuments::with_encoding(PositionEncodingKind::UTF8);
76    /// ```
77    ///
78    /// Using UTF-32 encoding where each Unicode code point is one unit:
79    ///
80    /// ```
81    /// use lsp_textdocument::TextDocuments;
82    /// use lsp_types::PositionEncodingKind;
83    ///
84    /// let text_documents = TextDocuments::with_encoding(PositionEncodingKind::UTF32);
85    /// ```
86    pub fn with_encoding(default_encoding: PositionEncodingKind) -> Self {
87        Self {
88            documents: BTreeMap::new(),
89            default_encoding,
90        }
91    }
92
93    #[allow(clippy::mutable_key_type)]
94    // `Uri` (url::Url) implements interior mutability APIs, but we never mutate keys after
95    // insertion, and map operations rely on its stable ordering. Suppress the lint here.
96    pub fn documents(&self) -> &BTreeMap<Uri, FullTextDocument> {
97        &self.documents
98    }
99
100    /// Returns the default position encoding used for newly created documents.
101    ///
102    /// This is useful for checking which encoding was configured or negotiated
103    /// (for example, during server initialization) when this `TextDocuments`
104    /// instance was created.
105    pub fn default_encoding(&self) -> PositionEncodingKind {
106        self.default_encoding.clone()
107    }
108
109    /// Get specify document by giving Uri
110    ///
111    /// # Examples:
112    ///
113    /// Basic usage:
114    /// ```
115    /// use lsp_textdocument::TextDocuments;
116    /// use lsp_types::Uri;
117    ///
118    /// let text_documents = TextDocuments::new();
119    /// let uri:Uri = "file://example.txt".parse().unwrap();
120    /// text_documents.get_document(&uri);
121    /// ```
122    pub fn get_document(&self, uri: &Uri) -> Option<&FullTextDocument> {
123        self.documents.get(uri)
124    }
125
126    /// Get specify document content by giving Range
127    ///
128    /// # Examples
129    ///
130    /// Basic usage:
131    /// ```no_run
132    /// use lsp_textdocument::TextDocuments;
133    /// use lsp_types::{Uri, Range, Position};
134    ///
135    /// let uri: Uri = "file://example.txt".parse().unwrap();
136    /// let text_documents = TextDocuments::new();
137    ///
138    /// // get document all content
139    /// let content = text_documents.get_document_content(&uri, None);
140    /// assert_eq!(content, Some("hello rust!"));
141    ///
142    /// // get document specify content by range
143    /// let (start, end) = (Position::new(0, 1), Position::new(0, 9));
144    /// let range = Range::new(start, end);
145    /// let sub_content = text_documents.get_document_content(&uri, Some(range));
146    /// assert_eq!(sub_content, Some("ello rus"));
147    /// ```
148    pub fn get_document_content(&self, uri: &Uri, range: Option<Range>) -> Option<&str> {
149        self.documents
150            .get(uri)
151            .map(|document| document.get_content(range))
152    }
153
154    /// Get specify document's language by giving Uri
155    ///
156    /// # Examples
157    ///
158    /// Basic usage:
159    /// ```no_run
160    /// use lsp_textdocument::TextDocuments;
161    /// use lsp_types::Uri;
162    ///
163    /// let text_documents = TextDocuments::new();
164    /// let uri:Uri = "file://example.js".parse().unwrap();
165    /// let language =  text_documents.get_document_language(&uri);
166    /// assert_eq!(language, Some("javascript"));
167    /// ```
168    pub fn get_document_language(&self, uri: &Uri) -> Option<&str> {
169        self.documents
170            .get(uri)
171            .map(|document| document.language_id())
172    }
173
174    /// Listening the notification from client, you just need to pass `method` and `params`
175    ///
176    /// # Examples:
177    ///
178    /// Basic usage:
179    /// ```no_run
180    /// use lsp_textdocument::TextDocuments;
181    ///
182    /// let method = "textDocument/didOpen";
183    /// let params = serde_json::to_value("message produced by client").unwrap();
184    ///
185    /// let mut text_documents = TextDocuments::new();
186    /// let accept: bool = text_documents.listen(method, &params);
187    /// ```
188    pub fn listen(&mut self, method: &str, params: &Value) -> bool {
189        match method {
190            DidOpenTextDocument::METHOD => {
191                let params: DidOpenTextDocumentParams = serde_json::from_value(params.clone())
192                    .expect("Expect receive DidOpenTextDocumentParams");
193                let text_document = params.text_document;
194
195                let document = FullTextDocument::new_with_encoding(
196                    text_document.language_id,
197                    text_document.version,
198                    text_document.text,
199                    // use default encoding negotiated at server init
200                    self.default_encoding.clone(),
201                );
202                self.documents.insert(text_document.uri, document);
203                true
204            }
205            DidChangeTextDocument::METHOD => {
206                let params: DidChangeTextDocumentParams = serde_json::from_value(params.clone())
207                    .expect("Expect receive DidChangeTextDocumentParams");
208
209                if let Some(document) = self.documents.get_mut(&params.text_document.uri) {
210                    let changes = &params.content_changes;
211                    let version = params.text_document.version;
212                    document.update(changes, version);
213                };
214                true
215            }
216            DidCloseTextDocument::METHOD => {
217                let params: DidCloseTextDocumentParams = serde_json::from_value(params.clone())
218                    .expect("Expect receive DidCloseTextDocumentParams");
219
220                self.documents.remove(&params.text_document.uri);
221                true
222            }
223            _ => {
224                // ignore other request
225                false
226            }
227        }
228    }
229}