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}