sway_lsp/core/
document.rs

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    /// Asynchronously writes the changes to the file and updates the document.
139    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    /// Update the document at the given [Url] with the Vec of changes returned by the client.
165    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    /// Get the document at the given [Url].
184    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    /// Remove the text document.
194    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    /// Store the text document.
203    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    /// Populate with sway files found in the workspace.
211    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
230/// Manages process-based file locking for multiple files.
231pub 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    /// Marks the specified file as "dirty" by creating a corresponding flag file.
249    ///
250    /// This function ensures the necessary directory structure exists before creating the flag file.
251    /// If the file is already locked, this function will do nothing. This is to reduce the number of
252    /// unnecessary file IO operations.
253    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    /// Removes the corresponding flag file for the specified Url.
266    ///
267    /// If the flag file does not exist, this function will do nothing.
268    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}