opened_documents.rs

  1use dashmap::DashMap;
  2use std::path::{Path, PathBuf};
  3use std::sync::Arc;
  4use std::time::Instant;
  5
  6use crate::buf_pool::{BufGuard, BufPool};
  7
  8/// Represents an opened document tracked by the LSP client.
  9#[derive(Debug)]
 10pub struct OpenedDocument {
 11    pub version: u32,
 12    pub content: BufGuard<String>,
 13    pub last_accessed: Instant,
 14}
 15
 16/// Thread-safe tracker for opened LSP documents.
 17///
 18/// IMPORTANT: All paths passed to this type must be canonical to avoid
 19/// duplicate entries (e.g., "/foo//bar" vs "/foo/bar").
 20#[derive(Clone)]
 21pub struct OpenedDocuments {
 22    docs: Arc<DashMap<PathBuf, OpenedDocument>>,
 23    string_pool: BufPool<String>,
 24}
 25
 26impl OpenedDocuments {
 27    pub fn new() -> Self {
 28        Self {
 29            docs: Arc::new(DashMap::new()),
 30            string_pool: BufPool::with_capacity(50),
 31        }
 32    }
 33
 34    /// Validate that a path appears to be canonical.
 35    /// A canonical path must be absolute and not contain `.` or `..` components.
 36    fn validate_canonical(path: &Path) -> anyhow::Result<()> {
 37        if !path.is_absolute() {
 38            anyhow::bail!("Path must be canonical (absolute): {}", path.display());
 39        }
 40
 41        // Check for . or .. components
 42        for component in path.components() {
 43            match component {
 44                std::path::Component::CurDir | std::path::Component::ParentDir => {
 45                    anyhow::bail!("Path must be canonical (no . or .. components): {}", path.display());
 46                }
 47                _ => {}
 48            }
 49        }
 50
 51        Ok(())
 52    }
 53
 54    /// Check if a document is currently opened.
 55    pub fn is_open(&self, path: impl AsRef<Path>) -> anyhow::Result<bool> {
 56        let path = path.as_ref();
 57        Self::validate_canonical(path)?;
 58        Ok(self.docs.contains_key(path))
 59    }
 60
 61    /// Get the version number of an opened document.
 62    pub fn get_version(&self, path: impl AsRef<Path>) -> anyhow::Result<Option<u32>> {
 63        let path = path.as_ref();
 64        Self::validate_canonical(path)?;
 65        Ok(self.docs.get(path).map(|doc| doc.version))
 66    }
 67
 68    /// Insert a new opened document. Path must be canonical.
 69    pub fn insert(&self, path: PathBuf, content: &str, version: u32) -> anyhow::Result<()> {
 70        Self::validate_canonical(&path)?;
 71        let mut buf = self.string_pool.checkout();
 72        buf.push_str(content);
 73        self.docs.insert(
 74            path,
 75            OpenedDocument {
 76                version,
 77                content: buf,
 78                last_accessed: Instant::now(),
 79            },
 80        );
 81        Ok(())
 82    }
 83
 84    /// Update the last_accessed timestamp for a document.
 85    pub fn touch(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
 86        let path = path.as_ref();
 87        Self::validate_canonical(path)?;
 88        if let Some(mut doc) = self.docs.get_mut(path) {
 89            doc.last_accessed = Instant::now();
 90        }
 91        Ok(())
 92    }
 93
 94    /// Remove a document from tracking.
 95    pub fn remove(&self, path: impl AsRef<Path>) -> anyhow::Result<Option<OpenedDocument>> {
 96        let path = path.as_ref();
 97        Self::validate_canonical(path)?;
 98        Ok(self.docs.remove(path).map(|(_, doc)| doc))
 99    }
100
101    /// Evict the least recently used documents, keeping only `keep_count` documents.
102    /// Returns the paths of evicted documents.
103    pub fn evict_lru(&self, keep_count: usize) -> Vec<PathBuf> {
104        let current_count = self.docs.len();
105        if current_count <= keep_count {
106            return Vec::new();
107        }
108
109        let evict_count = current_count - keep_count;
110
111        // Collect all documents with their access times
112        let mut docs_with_time: Vec<(PathBuf, Instant)> = self
113            .docs
114            .iter()
115            .map(|entry| (entry.key().clone(), entry.value().last_accessed))
116            .collect();
117
118        // Sort by last_accessed (oldest first)
119        docs_with_time.sort_by_key(|(_, time)| *time);
120
121        // Evict the oldest documents
122        let mut evicted = Vec::new();
123        for (path, _) in docs_with_time.into_iter().take(evict_count) {
124            if self.docs.remove(&path).is_some() {
125                evicted.push(path);
126            }
127        }
128
129        evicted
130    }
131}