crates/cli/ents-lens/src/document.rs
document.rshistorycomment on this file
| 1 | //! The lens's view of the client's open buffers, and the file-URI ↔ |
| 2 | //! repository-path arithmetic that ties an LSP document to the anchor |
| 3 | //! paths `refs/meta/comments/*` records. |
| 4 | //! |
| 5 | //! The lens caches nothing derived — no projection, no lens, no diagnostic |
| 6 | //! survives a comment-ref mutation (`lens.lenses`) — but it must remember |
| 7 | //! the *buffer text* the client has sent, because the client owns the only |
| 8 | //! copy of a document's unsaved content: `textDocument/didChange` ships an |
| 9 | //! edit, never the file, and the disk still holds the old bytes. Holding |
| 10 | //! the latest buffer per open URI is what lets projection target the bytes |
| 11 | //! the user is actually looking at (`lens.working-tree`), and it is dropped |
| 12 | //! the moment the client closes the document. |
| 13 | |
| 14 | use std::collections::HashMap; |
| 15 | use std::path::{Path, PathBuf}; |
| 16 | |
| 17 | use lsp_types::Url; |
| 18 | |
| 19 | /// The client's open text documents, keyed by URI, each holding the latest |
| 20 | /// full text the client has sent (`textDocumentSync` is full-text, so every |
| 21 | /// change replaces the whole buffer). |
| 22 | #[derive(Debug, Default)] |
| 23 | pub struct Documents { |
| 24 | open: HashMap<Url, String>, |
| 25 | } |
| 26 | |
| 27 | impl Documents { |
| 28 | /// Record (or replace) the full text of the document at `uri` — the |
| 29 | /// `didOpen`/`didChange` handler's whole job. |
| 30 | pub fn set(&mut self, uri: Url, text: String) { |
| 31 | self.open.insert(uri, text); |
| 32 | } |
| 33 | |
| 34 | /// Forget the document at `uri` — `didClose`; projection falls back to |
| 35 | /// the on-disk bytes afterward. |
| 36 | pub fn remove(&mut self, uri: &Url) { |
| 37 | self.open.remove(uri); |
| 38 | } |
| 39 | |
| 40 | /// The latest buffer text for `uri`, if the client has it open. |
| 41 | #[must_use] |
| 42 | pub fn text(&self, uri: &Url) -> Option<&str> { |
| 43 | self.open.get(uri).map(String::as_str) |
| 44 | } |
| 45 | |
| 46 | /// Every open document's URI — the server republishes diagnostics for |
| 47 | /// these after a comment-ref mutation. |
| 48 | #[must_use] |
| 49 | pub fn open_uris(&self) -> Vec<Url> { |
| 50 | self.open.keys().cloned().collect() |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | /// The repository-relative, forward-slashed path a file URI names inside |
| 55 | /// the working tree at `workdir`, or `None` when the URI is not a file |
| 56 | /// under it — the key that matches an [`ents_anchor::Anchor`]'s own |
| 57 | /// recorded path. |
| 58 | /// |
| 59 | /// Both sides are canonicalized when possible (the working tree always |
| 60 | /// exists; an open document usually does), so a symlinked temp directory |
| 61 | /// like macOS's `/var` → `/private/var` does not defeat the prefix match; |
| 62 | /// when the document has no on-disk form yet, the raw paths are compared. |
| 63 | #[must_use] |
| 64 | pub fn relative_path(workdir: &Path, uri: &Url) -> Option<String> { |
| 65 | let file = uri.to_file_path().ok()?; |
| 66 | let file_canon = canonical(&file); |
| 67 | let workdir_canon = canonical(workdir); |
| 68 | let rel = file_canon.strip_prefix(&workdir_canon).ok()?; |
| 69 | Some(rel.to_string_lossy().replace('\\', "/")) |
| 70 | } |
| 71 | |
| 72 | /// Resolve `path` through symlinks even when its leaf does not exist yet, |
| 73 | /// by canonicalizing the deepest ancestor that does and re-appending the |
| 74 | /// rest — so a not-yet-saved document under a symlinked temp directory |
| 75 | /// (macOS's `/var` → `/private/var`) still shares a prefix with the |
| 76 | /// canonicalized working tree. |
| 77 | fn canonical(path: &Path) -> PathBuf { |
| 78 | if let Ok(resolved) = path.canonicalize() { |
| 79 | return resolved; |
| 80 | } |
| 81 | match (path.parent(), path.file_name()) { |
| 82 | (Some(parent), Some(name)) => canonical(parent).join(name), |
| 83 | _ => path.to_owned(), |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /// The absolute file URI for `path` — used to open the compose template |
| 88 | /// with `window/showDocument`. |
| 89 | #[must_use] |
| 90 | pub fn file_uri(path: &Path) -> Option<Url> { |
| 91 | Url::from_file_path(path).ok() |
| 92 | } |