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, ¶ms);
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(¶ms.text_document.uri) {
210 let changes = ¶ms.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(¶ms.text_document.uri);
221 true
222 }
223 _ => {
224 // ignore other request
225 false
226 }
227 }
228 }
229}