use std::collections::HashSet;
use std::path::{Path, PathBuf};
use anyhow::Context;
use tracing::debug;
use super::client::path_to_uri;
use super::transport::LspTransport;
use crate::detect::Language;
pub struct FileTracker {
open_files: HashSet<PathBuf>,
language: Language,
}
impl FileTracker {
#[must_use]
pub fn new(language: Language) -> Self {
Self {
open_files: HashSet::new(),
language,
}
}
pub async fn ensure_open(
&mut self,
path: &Path,
transport: &mut LspTransport,
) -> anyhow::Result<()> {
let canonical = std::fs::canonicalize(path)
.with_context(|| format!("file not found: {}", path.display()))?;
if self.open_files.contains(&canonical) {
return Ok(());
}
let uri = path_to_uri(&canonical)?;
let text = std::fs::read_to_string(&canonical)
.with_context(|| format!("failed to read: {}", canonical.display()))?;
let params = serde_json::json!({
"textDocument": {
"uri": uri.as_str(),
"languageId": self.language.name(),
"version": 0,
"text": text,
}
});
transport
.send_notification("textDocument/didOpen", params)
.await?;
debug!("opened file: {}", canonical.display());
self.open_files.insert(canonical);
Ok(())
}
pub async fn open_with_content(
&mut self,
path: &Path,
uri: &str,
content: &str,
transport: &mut LspTransport,
) -> anyhow::Result<()> {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if self.open_files.contains(&canonical) {
return Ok(());
}
let params = serde_json::json!({
"textDocument": {
"uri": uri,
"languageId": self.language.name(),
"version": 0,
"text": content,
}
});
transport
.send_notification("textDocument/didOpen", params)
.await?;
self.open_files.insert(canonical);
Ok(())
}
pub async fn reopen(
&mut self,
path: &Path,
transport: &mut LspTransport,
) -> anyhow::Result<()> {
let canonical = std::fs::canonicalize(path)
.with_context(|| format!("file not found: {}", path.display()))?;
if self.open_files.remove(&canonical) {
let uri = path_to_uri(&canonical)?;
let params = serde_json::json!({
"textDocument": { "uri": uri.as_str() }
});
transport
.send_notification("textDocument/didClose", params)
.await?;
debug!("closed (for reopen): {}", canonical.display());
}
self.ensure_open(path, transport).await
}
pub async fn close(&mut self, path: &Path, transport: &mut LspTransport) -> anyhow::Result<()> {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if !self.open_files.remove(&canonical) {
return Ok(());
}
let uri = path_to_uri(&canonical)?;
let params = serde_json::json!({
"textDocument": {
"uri": uri.as_str(),
}
});
transport
.send_notification("textDocument/didClose", params)
.await?;
debug!("closed file: {}", canonical.display());
Ok(())
}
pub async fn close_all(&mut self, transport: &mut LspTransport) -> anyhow::Result<()> {
let paths: Vec<PathBuf> = self.open_files.drain().collect();
for path in &paths {
let uri = path_to_uri(path)?;
let params = serde_json::json!({
"textDocument": {
"uri": uri.as_str(),
}
});
transport
.send_notification("textDocument/didClose", params)
.await?;
debug!("closed file: {}", path.display());
}
Ok(())
}
#[must_use]
pub fn is_open(&self, path: &Path) -> bool {
std::fs::canonicalize(path)
.map(|c| self.open_files.contains(&c))
.unwrap_or(false)
}
#[must_use]
pub fn open_count(&self) -> usize {
self.open_files.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_tracker_is_empty() {
let tracker = FileTracker::new(Language::Rust);
assert_eq!(tracker.open_count(), 0);
}
#[test]
fn is_open_returns_false_for_unknown_file() {
let tracker = FileTracker::new(Language::Rust);
assert!(!tracker.is_open(Path::new("/tmp/nonexistent.rs")));
}
}