use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
pub fn resolve_workspace_root(workspace_root: &str) -> String {
std::fs::canonicalize(workspace_root)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| workspace_root.to_string())
}
pub fn socket_path_for_workspace(workspace_root: &str) -> PathBuf {
let canonical = resolve_workspace_root(workspace_root);
let hash = hash_string(&canonical);
let slug = workspace_slug(&canonical);
let lspz_dir = lspz_cache_dir();
lspz_dir.join(format!("{slug}-{hash:016x}.sock"))
}
fn hash_string(s: &str) -> u64 {
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
fn workspace_slug(path: &str) -> String {
std::path::Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".into())
}
pub fn lspz_cache_dir() -> PathBuf {
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("lspz")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_socket_path_stable() {
let a = socket_path_for_workspace("/home/user/project");
let b = socket_path_for_workspace("/home/user/project");
assert_eq!(a, b);
}
#[test]
fn test_socket_path_different_workspaces() {
let a = socket_path_for_workspace("/home/user/project-a");
let b = socket_path_for_workspace("/home/user/project-b");
assert_ne!(a, b);
}
#[test]
fn test_slug_from_path() {
assert_eq!(workspace_slug("/home/user/my-project"), "my-project");
assert_eq!(workspace_slug("/"), "unknown"); assert_eq!(workspace_slug("/no_trailing_slash"), "no_trailing_slash");
}
#[test]
fn test_hash_deterministic() {
assert_eq!(hash_string("hello"), hash_string("hello"));
}
#[test]
fn test_socket_path_ignores_trailing_slash() {
let dir = tempfile::tempdir().unwrap();
let canonical = dir.path().canonicalize().unwrap();
let with_slash = format!("{}/", canonical.display());
let a = socket_path_for_workspace(canonical.to_str().unwrap());
let b = socket_path_for_workspace(&with_slash);
assert_eq!(a, b, "trailing slash must not change socket");
}
#[test]
fn test_socket_path_resolves_dotdot() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("sub")).unwrap();
let canonical = dir.path().canonicalize().unwrap();
let via_dotdot = canonical.join("sub").join("..");
let a = socket_path_for_workspace(canonical.to_str().unwrap());
let b = socket_path_for_workspace(via_dotdot.to_str().unwrap());
assert_eq!(a, b, "'..' segments must not change socket");
}
#[test]
fn test_resolve_nonexistent_falls_back() {
let raw = "/this/path/does/not/exist/lspz-xyz";
let resolved = resolve_workspace_root(raw);
assert_eq!(resolved, raw);
}
}