use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};
use crate::lsp::traits::LspClientTrait;
#[derive(Debug, thiserror::Error)]
pub enum FileManagerError {
#[error("Failed to read file: {0}")]
FileReadError(#[from] std::io::Error),
#[error("LSP error: {0}")]
LspError(#[from] crate::lsp::client::LspError),
#[error("Invalid file path: {0}")]
InvalidPath(String),
}
#[derive(Debug, Clone)]
struct FileEntry {
uri: String,
content_hash: String,
version: i32,
}
pub struct ClangdFileManager {
opened_files: HashMap<PathBuf, FileEntry>,
next_version: i32,
}
impl ClangdFileManager {
pub fn new() -> Self {
Self {
opened_files: HashMap::new(),
next_version: 1,
}
}
pub async fn ensure_file_ready(
&mut self,
path: &Path,
client: &mut impl LspClientTrait,
) -> Result<(), FileManagerError> {
if !client.is_initialized() {
return Err(FileManagerError::LspError(
crate::lsp::client::LspError::NotInitialized,
));
}
let abs_path = path
.canonicalize()
.map_err(|e| FileManagerError::InvalidPath(format!("{}: {}", path.display(), e)))?;
let content = std::fs::read_to_string(&abs_path)?;
let content_hash = Self::compute_hash(&content);
let uri_string = format!("file://{}", abs_path.display());
let uri: lsp_types::Uri = uri_string
.parse()
.map_err(|e| FileManagerError::InvalidPath(format!("Invalid URI: {}", e)))?;
if let Some(entry) = self.opened_files.get(&abs_path) {
if entry.content_hash == content_hash {
debug!("File {} is already open and unchanged", abs_path.display());
return Ok(());
}
info!(
"File {} has changed, sending change notification",
abs_path.display()
);
let new_version = self.next_version;
self.next_version += 1;
client
.change_text_document(uri.clone(), new_version, content)
.await?;
self.opened_files.insert(
abs_path,
FileEntry {
uri: uri_string.clone(),
content_hash,
version: new_version,
},
);
} else {
info!("Opening file {}", abs_path.display());
let version = self.next_version;
self.next_version += 1;
let language_id = Self::get_language_id(&abs_path);
client
.open_text_document(uri, language_id.to_string(), version, content)
.await?;
self.opened_files.insert(
abs_path,
FileEntry {
uri: uri_string.clone(),
content_hash,
version,
},
);
}
Ok(())
}
pub async fn close_file(
&mut self,
path: &Path,
client: &mut impl LspClientTrait,
) -> Result<(), FileManagerError> {
let abs_path = path
.canonicalize()
.map_err(|e| FileManagerError::InvalidPath(format!("{}: {}", path.display(), e)))?;
if let Some(entry) = self.opened_files.remove(&abs_path) {
info!("Closing file {}", abs_path.display());
let uri: lsp_types::Uri = entry
.uri
.parse()
.map_err(|e| FileManagerError::InvalidPath(format!("Invalid URI: {}", e)))?;
client.close_text_document(uri).await?;
Ok(())
} else {
debug!("File {} was not open", abs_path.display());
Ok(())
}
}
pub fn is_file_open(&self, path: &Path) -> bool {
if let Ok(abs_path) = path.canonicalize() {
self.opened_files.contains_key(&abs_path)
} else {
false
}
}
pub fn get_open_files_count(&self) -> usize {
self.opened_files.len()
}
pub async fn close_all_files(
&mut self,
client: &mut impl LspClientTrait,
) -> Result<(), FileManagerError> {
let files: Vec<PathBuf> = self.opened_files.keys().cloned().collect();
for file in files {
if let Err(e) = self.close_file(&file, client).await {
warn!("Failed to close file {}: {}", file.display(), e);
}
}
Ok(())
}
fn compute_hash(content: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("{:x}", hasher.finalize())
}
fn get_language_id(path: &Path) -> &'static str {
match path.extension().and_then(|ext| ext.to_str()) {
Some("c") => "c",
Some("cpp") | Some("cc") | Some("cxx") | Some("c++") => "cpp",
Some("h") | Some("hpp") | Some("hh") | Some("hxx") | Some("h++") => "cpp",
_ => "cpp", }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[cfg(feature = "test-logging")]
#[ctor::ctor]
fn init_test_logging() {
crate::test_utils::logging::init();
}
#[test]
fn test_compute_hash() {
let content1 = "Hello, world!";
let content2 = "Hello, world!";
let content3 = "Hello, World!";
let hash1 = ClangdFileManager::compute_hash(content1);
let hash2 = ClangdFileManager::compute_hash(content2);
let hash3 = ClangdFileManager::compute_hash(content3);
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_language_id_detection() {
use std::path::PathBuf;
assert_eq!(
ClangdFileManager::get_language_id(&PathBuf::from("test.c")),
"c"
);
assert_eq!(
ClangdFileManager::get_language_id(&PathBuf::from("test.cpp")),
"cpp"
);
assert_eq!(
ClangdFileManager::get_language_id(&PathBuf::from("test.cc")),
"cpp"
);
assert_eq!(
ClangdFileManager::get_language_id(&PathBuf::from("test.h")),
"cpp"
);
assert_eq!(
ClangdFileManager::get_language_id(&PathBuf::from("test.hpp")),
"cpp"
);
assert_eq!(
ClangdFileManager::get_language_id(&PathBuf::from("test.txt")),
"cpp"
);
}
#[test]
fn test_file_tracking() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test.cpp");
fs::write(&file_path, "int main() { return 0; }").unwrap();
let manager = ClangdFileManager::new();
assert_eq!(manager.get_open_files_count(), 0);
assert!(!manager.is_file_open(&file_path));
}
}