use anyhow::{Context, Result};
use fs_extra::dir::{self, CopyOptions};
use rusqlite::Connection;
use std::fs;
use std::path::{Path, PathBuf};
pub fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn strip_windows_prefix(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
if let Some(stripped) = path_str.strip_prefix(r"\\?\") {
PathBuf::from(stripped)
} else {
path.to_path_buf()
}
}
pub fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
let options = CopyOptions::new().copy_inside(true);
dir::copy(src, dst, &options)
.with_context(|| format!("Failed to copy {} to {}", src.display(), dst.display()))?;
Ok(())
}
pub fn copy_dir_contents(src: &Path, dst: &Path) -> Result<()> {
let options = CopyOptions::new().content_only(true).overwrite(true);
dir::copy(src, dst, &options).with_context(|| {
format!(
"Failed to copy contents of {} to {}",
src.display(),
dst.display()
)
})?;
Ok(())
}
pub fn count_chat_sessions(workspace_dir: &Path) -> Result<usize> {
let db_path = workspace_dir.join("state.vscdb");
if !db_path.exists() {
return Ok(0);
}
let conn = Connection::open_with_flags(
&db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.with_context(|| format!("Failed to open database: {}", db_path.display()))?;
let composer_data: Option<String> = match conn.query_row(
"SELECT value FROM ItemTable WHERE key = 'composer.composerData'",
[],
|row| row.get(0),
) {
Ok(data) => Some(data),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(e) => {
return Err(e)
.with_context(|| format!("Failed to query chat data from: {}", db_path.display()))
}
};
let Some(data) = composer_data else {
return Ok(0);
};
let json: serde_json::Value = serde_json::from_str(&data)
.with_context(|| format!("Corrupted chat data in: {}", db_path.display()))?;
let count = json
.get("allComposers")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter(|c| {
!c.get("isArchived")
.and_then(|v| v.as_bool())
.unwrap_or(false)
})
.count()
})
.unwrap_or(0);
Ok(count)
}
pub fn calculate_dir_size(path: &Path) -> Result<u64> {
let mut total = 0;
for entry in fs::read_dir(path)?.flatten() {
let metadata = entry.metadata()?;
if metadata.is_file() {
total += metadata.len();
} else if metadata.is_dir() {
total += calculate_dir_size(&entry.path()).unwrap_or(0);
}
}
Ok(total)
}
pub fn find_workspace_dir(project_path: &Path) -> Result<Option<std::path::PathBuf>> {
let workspace_storage_dir = crate::config::workspace_storage_dir()?;
if !workspace_storage_dir.exists() {
return Ok(None);
}
if project_path.exists() {
let project_uri = url::Url::from_file_path(project_path)
.map_err(|_| anyhow::anyhow!("Invalid project path"))?
.to_string();
let project_uri_normalized = normalize_uri_for_comparison(&project_uri);
for entry in fs::read_dir(&workspace_storage_dir)?.flatten() {
if !entry.file_type()?.is_dir() {
continue;
}
let workspace_json = entry.path().join("workspace.json");
if !workspace_json.exists() {
continue;
}
let content = fs::read_to_string(&workspace_json)?;
let ws: serde_json::Value = serde_json::from_str(&content)?;
if let Some(folder) = ws.get("folder").and_then(|v| v.as_str()) {
let folder_normalized = normalize_uri_for_comparison(folder);
if folder_normalized == project_uri_normalized {
return Ok(Some(entry.path()));
}
}
}
}
let search_path = project_path.to_string_lossy();
let search_path_normalized = search_path.trim_end_matches('/');
for entry in fs::read_dir(&workspace_storage_dir)?.flatten() {
if !entry.file_type()?.is_dir() {
continue;
}
let workspace_json = entry.path().join("workspace.json");
if !workspace_json.exists() {
continue;
}
let content = fs::read_to_string(&workspace_json)?;
let ws: serde_json::Value = serde_json::from_str(&content)?;
if let Some(folder) = ws.get("folder").and_then(|v| v.as_str()) {
if let Ok(url) = url::Url::parse(folder) {
if url.scheme() == "vscode-remote" {
let remote_path = url.path().trim_end_matches('/');
if remote_path == search_path_normalized {
return Ok(Some(entry.path()));
}
if let Some(remote_name) = remote_path.rsplit('/').next() {
if let Some(search_name) = search_path_normalized.rsplit(['/', '\\']).next()
{
if remote_name == search_name && !remote_name.is_empty() {
return Ok(Some(entry.path()));
}
}
}
}
}
}
}
Ok(None)
}
fn normalize_uri_for_comparison(uri: &str) -> String {
uri.trim_end_matches('/').to_lowercase().replace("%3a", ":")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_size() {
assert_eq!(format_size(0), "0 B");
assert_eq!(format_size(512), "512 B");
assert_eq!(format_size(1024), "1.0 KB");
assert_eq!(format_size(1536), "1.5 KB");
assert_eq!(format_size(1024 * 1024), "1.0 MB");
assert_eq!(format_size(1024 * 1024 * 1024), "1.0 GB");
}
#[test]
fn test_strip_windows_prefix() {
let result = strip_windows_prefix(Path::new(r"\\?\C:\path\to\project"));
assert_eq!(result, PathBuf::from(r"C:\path\to\project"));
}
#[test]
fn test_strip_windows_prefix_no_prefix() {
let result = strip_windows_prefix(Path::new(r"C:\path\to\project"));
assert_eq!(result, PathBuf::from(r"C:\path\to\project"));
}
#[test]
fn test_strip_windows_prefix_unix() {
let result = strip_windows_prefix(Path::new("/path/to/project"));
assert_eq!(result, PathBuf::from("/path/to/project"));
}
#[test]
fn test_normalize_uri_case_insensitive() {
let upper = normalize_uri_for_comparison("file:///C:/path/to/project");
let lower = normalize_uri_for_comparison("file:///c:/path/to/project");
assert_eq!(upper, lower);
}
#[test]
fn test_normalize_uri_percent_encoding() {
let encoded = normalize_uri_for_comparison("file:///c%3A/path/to/project");
let decoded = normalize_uri_for_comparison("file:///c:/path/to/project");
assert_eq!(encoded, decoded);
}
#[test]
fn test_normalize_uri_trailing_slash() {
let with_slash = normalize_uri_for_comparison("file:///c:/path/");
let without_slash = normalize_uri_for_comparison("file:///c:/path");
assert_eq!(with_slash, without_slash);
}
#[test]
fn test_normalize_uri_unix_paths() {
let result = normalize_uri_for_comparison("file:///Users/me/project/");
assert_eq!(result, "file:///users/me/project");
}
#[test]
fn test_find_workspace_dir_nonexistent() {
let result = find_workspace_dir(Path::new("/nonexistent/path/that/does/not/exist"));
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
}