Skip to main content

lspkit_vfs/
document.rs

1//! Concurrent open-document store.
2//!
3//! Tracks open documents by URI with LSP version numbers. Edits are applied
4//! through [`crate::edit`] using the negotiated position encoding. Locks are
5//! always scoped inside synchronous blocks — no guard ever crosses an
6//! `await` boundary.
7
8use std::sync::Arc;
9
10use dashmap::DashMap;
11use ropey::Rope;
12
13use crate::edit::{self, EditError, TextEdit};
14use crate::encoding::PositionEncoding;
15
16/// Opaque URI identifying a document. Generic over the wire form so this
17/// crate does not leak `lsp_types` into its public surface.
18#[non_exhaustive]
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct DocumentUri(String);
21
22impl DocumentUri {
23    /// Wrap a string-form URI.
24    #[must_use]
25    pub fn new(uri: impl Into<String>) -> Self {
26        Self(uri.into())
27    }
28
29    /// Borrow the underlying string.
30    #[must_use]
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34}
35
36/// LSP document version. Bumped by the client on every `didChange`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct DocumentVersion(i32);
39
40impl DocumentVersion {
41    /// Construct a version.
42    #[must_use]
43    pub const fn new(value: i32) -> Self {
44        Self(value)
45    }
46
47    /// Raw integer value.
48    #[must_use]
49    pub const fn get(self) -> i32 {
50        self.0
51    }
52}
53
54/// Errors from VFS operations.
55#[non_exhaustive]
56#[derive(Debug, thiserror::Error)]
57pub enum VfsError {
58    /// No document tracked for the given URI.
59    #[error("document not found: {0}")]
60    NotFound(String),
61    /// Incoming edit applied to stale version.
62    #[error("stale version: have {have}, got {got}")]
63    StaleVersion {
64        /// Version currently stored.
65        have: i32,
66        /// Version supplied by the caller.
67        got: i32,
68    },
69    /// An edit could not be applied.
70    #[error(transparent)]
71    Edit(#[from] EditError),
72}
73
74#[derive(Debug)]
75struct DocumentEntry {
76    rope: Rope,
77    version: DocumentVersion,
78}
79
80/// Concurrent open-document store.
81#[derive(Debug, Default, Clone)]
82pub struct Vfs {
83    docs: Arc<DashMap<DocumentUri, DocumentEntry>>,
84    encoding: PositionEncoding,
85}
86
87impl Vfs {
88    /// New empty VFS using the given position encoding.
89    #[must_use]
90    pub fn new(encoding: PositionEncoding) -> Self {
91        Self {
92            docs: Arc::new(DashMap::new()),
93            encoding,
94        }
95    }
96
97    /// Insert (or replace) a document. Called from `textDocument/didOpen`.
98    pub fn open(&self, uri: DocumentUri, text: &str, version: DocumentVersion) {
99        let entry = DocumentEntry {
100            rope: Rope::from_str(text),
101            version,
102        };
103        self.docs.insert(uri, entry);
104    }
105
106    /// Apply incremental edits. Called from `textDocument/didChange`.
107    ///
108    /// # Errors
109    /// Returns [`VfsError::NotFound`] if the document is unknown,
110    /// [`VfsError::StaleVersion`] if `version` is not strictly greater than
111    /// the stored version, or [`VfsError::Edit`] if an edit's range is invalid.
112    pub fn change(
113        &self,
114        uri: &DocumentUri,
115        edits: &[TextEdit],
116        version: DocumentVersion,
117    ) -> Result<(), VfsError> {
118        let mut entry = self
119            .docs
120            .get_mut(uri)
121            .ok_or_else(|| VfsError::NotFound(uri.as_str().to_owned()))?;
122        if version.get() <= entry.version.get() {
123            return Err(VfsError::StaleVersion {
124                have: entry.version.get(),
125                got: version.get(),
126            });
127        }
128        edit::apply(&mut entry.rope, edits, self.encoding)?;
129        entry.version = version;
130        Ok(())
131    }
132
133    /// Remove a document. Called from `textDocument/didClose`.
134    pub fn close(&self, uri: &DocumentUri) {
135        self.docs.remove(uri);
136    }
137
138    /// Read a document's current text. Returns `None` if not tracked.
139    #[must_use]
140    pub fn text(&self, uri: &DocumentUri) -> Option<String> {
141        self.docs.get(uri).map(|entry| entry.rope.to_string())
142    }
143
144    /// Read a document's current LSP version. Returns `None` if not tracked.
145    #[must_use]
146    pub fn version(&self, uri: &DocumentUri) -> Option<DocumentVersion> {
147        self.docs.get(uri).map(|entry| entry.version)
148    }
149
150    /// The negotiated position encoding.
151    #[must_use]
152    pub fn encoding(&self) -> PositionEncoding {
153        self.encoding
154    }
155
156    /// Number of currently tracked documents.
157    #[must_use]
158    pub fn len(&self) -> usize {
159        self.docs.len()
160    }
161
162    /// `true` if no documents are tracked.
163    #[must_use]
164    pub fn is_empty(&self) -> bool {
165        self.docs.is_empty()
166    }
167}