1use std::{path::PathBuf, sync::Arc};
2
3use crate::{
4 error::{DirectoryError, DocumentError, LanguageServerError},
5 utils::document,
6};
7use dashmap::DashMap;
8use forc_util::fs_locking::PidFileLocking;
9use lsp_types::{Position, Range, TextDocumentContentChangeEvent, Url};
10use sway_utils::get_sway_files;
11use tokio::{fs::File, io::AsyncWriteExt};
12
13#[derive(Debug, Clone)]
14pub struct TextDocument {
15 version: i32,
16 uri: String,
17 content: String,
18 line_offsets: Vec<usize>,
19}
20
21impl TextDocument {
22 pub async fn build_from_path(path: &str) -> Result<Self, DocumentError> {
23 tokio::fs::read_to_string(path)
24 .await
25 .map(|content| {
26 let line_offsets = TextDocument::calculate_line_offsets(&content);
27 Self {
28 version: 1,
29 uri: path.into(),
30 content,
31 line_offsets,
32 }
33 })
34 .map_err(|e| match e.kind() {
35 std::io::ErrorKind::NotFound => {
36 DocumentError::DocumentNotFound { path: path.into() }
37 }
38 std::io::ErrorKind::PermissionDenied => {
39 DocumentError::PermissionDenied { path: path.into() }
40 }
41 _ => DocumentError::IOError {
42 path: path.into(),
43 error: e.to_string(),
44 },
45 })
46 }
47
48 pub fn get_uri(&self) -> &str {
49 &self.uri
50 }
51
52 pub fn get_text(&self) -> &str {
53 &self.content
54 }
55
56 pub fn get_line(&self, line: usize) -> &str {
57 let start = self
58 .line_offsets
59 .get(line)
60 .copied()
61 .unwrap_or(self.content.len());
62 let end = self
63 .line_offsets
64 .get(line + 1)
65 .copied()
66 .unwrap_or(self.content.len());
67 &self.content[start..end]
68 }
69
70 pub fn apply_change(
71 &mut self,
72 change: &TextDocumentContentChangeEvent,
73 ) -> Result<(), DocumentError> {
74 if let Some(range) = change.range {
75 self.validate_range(range)?;
76 let start_index = self.position_to_index(range.start);
77 let end_index = self.position_to_index(range.end);
78 self.content
79 .replace_range(start_index..end_index, &change.text);
80 } else {
81 self.content.clone_from(&change.text);
82 }
83 self.line_offsets = Self::calculate_line_offsets(&self.content);
84 self.version += 1;
85 Ok(())
86 }
87
88 fn validate_range(&self, range: Range) -> Result<(), DocumentError> {
89 let start = self.position_to_index(range.start);
90 let end = self.position_to_index(range.end);
91 if start > end || end > self.content.len() {
92 return Err(DocumentError::InvalidRange { range });
93 }
94 Ok(())
95 }
96
97 fn position_to_index(&self, position: Position) -> usize {
98 let line_offset = self
99 .line_offsets
100 .get(position.line as usize)
101 .copied()
102 .unwrap_or(self.content.len());
103 line_offset + position.character as usize
104 }
105
106 fn calculate_line_offsets(text: &str) -> Vec<usize> {
107 let mut offsets = vec![0];
108 for (i, c) in text.char_indices() {
109 if c == '\n' {
110 offsets.push(i + 1);
111 }
112 }
113 offsets
114 }
115}
116
117pub struct Documents(DashMap<String, TextDocument>);
118
119impl Default for Documents {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125impl Documents {
126 pub fn new() -> Self {
127 Documents(DashMap::new())
128 }
129
130 pub async fn handle_open_file(&self, uri: &Url) {
131 if !self.contains_key(uri.path()) {
132 if let Ok(text_document) = TextDocument::build_from_path(uri.path()).await {
133 let _ = self.store_document(text_document);
134 }
135 }
136 }
137
138 pub async fn write_changes_to_file(
140 &self,
141 uri: &Url,
142 changes: &[TextDocumentContentChangeEvent],
143 ) -> Result<(), LanguageServerError> {
144 let src = self.update_text_document(uri, changes)?;
145
146 let mut file =
147 File::create(uri.path())
148 .await
149 .map_err(|err| DocumentError::UnableToCreateFile {
150 path: uri.path().to_string(),
151 err: err.to_string(),
152 })?;
153
154 file.write_all(src.as_bytes())
155 .await
156 .map_err(|err| DocumentError::UnableToWriteFile {
157 path: uri.path().to_string(),
158 err: err.to_string(),
159 })?;
160
161 Ok(())
162 }
163
164 pub fn update_text_document(
166 &self,
167 uri: &Url,
168 changes: &[TextDocumentContentChangeEvent],
169 ) -> Result<String, DocumentError> {
170 self.try_get_mut(uri.path())
171 .try_unwrap()
172 .ok_or_else(|| DocumentError::DocumentNotFound {
173 path: uri.path().to_string(),
174 })
175 .and_then(|mut document| {
176 for change in changes {
177 document.apply_change(change)?;
178 }
179 Ok(document.get_text().to_string())
180 })
181 }
182
183 pub fn get_text_document(&self, url: &Url) -> Result<TextDocument, DocumentError> {
185 self.try_get(url.path())
186 .try_unwrap()
187 .ok_or_else(|| DocumentError::DocumentNotFound {
188 path: url.path().to_string(),
189 })
190 .map(|document| document.clone())
191 }
192
193 pub fn remove_document(&self, url: &Url) -> Result<TextDocument, DocumentError> {
195 self.remove(url.path())
196 .ok_or_else(|| DocumentError::DocumentNotFound {
197 path: url.path().to_string(),
198 })
199 .map(|(_, text_document)| text_document)
200 }
201
202 pub fn store_document(&self, text_document: TextDocument) -> Result<(), DocumentError> {
204 let uri = text_document.get_uri().to_string();
205 self.insert(uri.clone(), text_document).map_or(Ok(()), |_| {
206 Err(DocumentError::DocumentAlreadyStored { path: uri })
207 })
208 }
209
210 pub async fn store_sway_files_from_temp(
212 &self,
213 temp_dir: PathBuf,
214 ) -> Result<(), LanguageServerError> {
215 for path_str in get_sway_files(temp_dir).iter().filter_map(|fp| fp.to_str()) {
216 let text_doc = TextDocument::build_from_path(path_str).await?;
217 self.store_document(text_doc)?;
218 }
219 Ok(())
220 }
221}
222
223impl std::ops::Deref for Documents {
224 type Target = DashMap<String, TextDocument>;
225 fn deref(&self) -> &Self::Target {
226 &self.0
227 }
228}
229
230pub struct PidLockedFiles {
232 locks: DashMap<Url, Arc<PidFileLocking>>,
233}
234
235impl Default for PidLockedFiles {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241impl PidLockedFiles {
242 pub fn new() -> Self {
243 Self {
244 locks: DashMap::new(),
245 }
246 }
247
248 pub fn mark_file_as_dirty(&self, uri: &Url) -> Result<(), LanguageServerError> {
254 if !self.locks.contains_key(uri) {
255 let path = document::get_path_from_url(uri)?;
256 let file_lock = Arc::new(PidFileLocking::lsp(path));
257 file_lock
258 .lock()
259 .map_err(|e| DirectoryError::LspLocksDirFailed(e.to_string()))?;
260 self.locks.insert(uri.clone(), file_lock);
261 }
262 Ok(())
263 }
264
265 pub fn remove_dirty_flag(&self, uri: &Url) -> Result<(), LanguageServerError> {
269 if let Some((uri, file_lock)) = self.locks.remove(uri) {
270 file_lock
271 .release()
272 .map_err(|err| DocumentError::UnableToRemoveFile {
273 path: uri.path().to_string(),
274 err: err.to_string(),
275 })?;
276 }
277 Ok(())
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use sway_lsp_test_utils::get_absolute_path;
285
286 #[tokio::test]
287 async fn build_from_path_returns_text_document() {
288 let path = get_absolute_path("sway-lsp/tests/fixtures/cats.txt");
289 let result = TextDocument::build_from_path(&path).await;
290 assert!(result.is_ok(), "result = {result:?}");
291 let document = result.unwrap();
292 assert_eq!(document.version, 1);
293 assert_eq!(document.uri, path);
294 assert!(!document.content.is_empty());
295 assert!(!document.line_offsets.is_empty());
296 }
297
298 #[tokio::test]
299 async fn build_from_path_returns_document_not_found_error() {
300 let path = get_absolute_path("not/a/real/file/path");
301 let result = TextDocument::build_from_path(&path)
302 .await
303 .expect_err("expected DocumentNotFound");
304 assert_eq!(result, DocumentError::DocumentNotFound { path });
305 }
306
307 #[tokio::test]
308 async fn store_document_returns_empty_tuple() {
309 let documents = Documents::new();
310 let path = get_absolute_path("sway-lsp/tests/fixtures/cats.txt");
311 let document = TextDocument::build_from_path(&path).await.unwrap();
312 let result = documents.store_document(document);
313 assert!(result.is_ok());
314 }
315
316 #[tokio::test]
317 async fn store_document_returns_document_already_stored_error() {
318 let documents = Documents::new();
319 let path = get_absolute_path("sway-lsp/tests/fixtures/cats.txt");
320 let document = TextDocument::build_from_path(&path).await.unwrap();
321 documents
322 .store_document(document)
323 .expect("expected successfully stored");
324 let document = TextDocument::build_from_path(&path).await.unwrap();
325 let result = documents
326 .store_document(document)
327 .expect_err("expected DocumentAlreadyStored");
328 assert_eq!(result, DocumentError::DocumentAlreadyStored { path });
329 }
330
331 #[test]
332 fn get_line_returns_correct_line() {
333 let content = "line1\nline2\nline3".to_string();
334 let line_offsets = TextDocument::calculate_line_offsets(&content);
335 let document = TextDocument {
336 version: 1,
337 uri: "test.sw".into(),
338 content,
339 line_offsets,
340 };
341 assert_eq!(document.get_line(0), "line1\n");
342 assert_eq!(document.get_line(1), "line2\n");
343 assert_eq!(document.get_line(2), "line3");
344 }
345
346 #[test]
347 fn apply_change_updates_content_correctly() {
348 let content = "Hello, world!".to_string();
349 let line_offsets = TextDocument::calculate_line_offsets(&content);
350 let mut document = TextDocument {
351 version: 1,
352 uri: "test.sw".into(),
353 content,
354 line_offsets,
355 };
356 let change = TextDocumentContentChangeEvent {
357 range: Some(Range::new(Position::new(0, 7), Position::new(0, 12))),
358 range_length: None,
359 text: "Rust".into(),
360 };
361 document.apply_change(&change).unwrap();
362 assert_eq!(document.get_text(), "Hello, Rust!");
363 }
364
365 #[test]
366 fn position_to_index_works_correctly() {
367 let content = "line1\nline2\nline3".to_string();
368 let line_offsets = TextDocument::calculate_line_offsets(&content);
369 let document = TextDocument {
370 version: 1,
371 uri: "test.sw".into(),
372 content,
373 line_offsets,
374 };
375 assert_eq!(document.position_to_index(Position::new(1, 2)), 8);
376 }
377}