1use crate::error::{CsmError, Result};
6use crate::models::{ChatSession, SessionWithPath, Workspace, WorkspaceJson};
7use std::path::{Path, PathBuf};
8use urlencoding::decode;
9
10pub type WorkspaceInfo = (String, PathBuf, Option<String>, std::time::SystemTime);
12
13pub fn get_workspace_storage_path() -> Result<PathBuf> {
15 let path = if cfg!(target_os = "windows") {
16 dirs::config_dir().map(|p| p.join("Code").join("User").join("workspaceStorage"))
17 } else if cfg!(target_os = "macos") {
18 dirs::home_dir().map(|p| p.join("Library/Application Support/Code/User/workspaceStorage"))
19 } else {
20 dirs::home_dir().map(|p| p.join(".config/Code/User/workspaceStorage"))
22 };
23
24 path.ok_or(CsmError::StorageNotFound)
25}
26
27pub fn get_global_storage_path() -> Result<PathBuf> {
29 let path = if cfg!(target_os = "windows") {
30 dirs::config_dir().map(|p| p.join("Code").join("User").join("globalStorage"))
31 } else if cfg!(target_os = "macos") {
32 dirs::home_dir().map(|p| p.join("Library/Application Support/Code/User/globalStorage"))
33 } else {
34 dirs::home_dir().map(|p| p.join(".config/Code/User/globalStorage"))
36 };
37
38 path.ok_or(CsmError::StorageNotFound)
39}
40
41pub fn get_empty_window_sessions_path() -> Result<PathBuf> {
44 let global_storage = get_global_storage_path()?;
45 Ok(global_storage.join("emptyWindowChatSessions"))
46}
47
48pub fn decode_workspace_folder(folder_uri: &str) -> String {
50 let mut folder = folder_uri.to_string();
51
52 if folder.starts_with("file:///") {
54 folder = folder[8..].to_string();
55 } else if folder.starts_with("file://") {
56 folder = folder[7..].to_string();
57 }
58
59 if let Ok(decoded) = decode(&folder) {
61 folder = decoded.into_owned();
62 }
63
64 if cfg!(target_os = "windows") {
66 folder = folder.replace('/', "\\");
67 }
68
69 folder
70}
71
72pub fn normalize_path(path: &str) -> String {
74 let path = Path::new(path);
75 if let Ok(canonical) = path.canonicalize() {
76 canonical.to_string_lossy().to_lowercase()
77 } else {
78 let normalized = path.to_string_lossy().to_lowercase();
80 normalized.trim_end_matches(['/', '\\']).to_string()
81 }
82}
83
84pub fn discover_workspaces() -> Result<Vec<Workspace>> {
86 let storage_path = get_workspace_storage_path()?;
87
88 if !storage_path.exists() {
89 return Ok(Vec::new());
90 }
91
92 let mut workspaces = Vec::new();
93
94 for entry in std::fs::read_dir(&storage_path)? {
95 let entry = entry?;
96 let workspace_dir = entry.path();
97
98 if !workspace_dir.is_dir() {
99 continue;
100 }
101
102 let workspace_json_path = workspace_dir.join("workspace.json");
103 if !workspace_json_path.exists() {
104 continue;
105 }
106
107 let project_path = match std::fs::read_to_string(&workspace_json_path) {
109 Ok(content) => match serde_json::from_str::<WorkspaceJson>(&content) {
110 Ok(ws_json) => ws_json.folder.map(|f| decode_workspace_folder(&f)),
111 Err(_) => None,
112 },
113 Err(_) => None,
114 };
115
116 let chat_sessions_path = workspace_dir.join("chatSessions");
117 let has_chat_sessions = chat_sessions_path.exists();
118
119 let chat_session_count = if has_chat_sessions {
120 std::fs::read_dir(&chat_sessions_path)
121 .map(|entries| {
122 entries
123 .filter_map(|e| e.ok())
124 .filter(|e| {
125 e.path()
126 .extension()
127 .map(|ext| ext == "json")
128 .unwrap_or(false)
129 })
130 .count()
131 })
132 .unwrap_or(0)
133 } else {
134 0
135 };
136
137 let last_modified = if has_chat_sessions {
139 std::fs::read_dir(&chat_sessions_path)
140 .ok()
141 .and_then(|entries| {
142 entries
143 .filter_map(|e| e.ok())
144 .filter_map(|e| e.metadata().ok())
145 .filter_map(|m| m.modified().ok())
146 .max()
147 })
148 .map(chrono::DateTime::<Utc>::from)
149 } else {
150 None
151 };
152
153 workspaces.push(Workspace {
154 hash: entry.file_name().to_string_lossy().to_string(),
155 project_path,
156 workspace_path: workspace_dir.clone(),
157 chat_sessions_path,
158 chat_session_count,
159 has_chat_sessions,
160 last_modified,
161 });
162 }
163
164 Ok(workspaces)
165}
166
167pub fn get_workspace_by_hash(hash: &str) -> Result<Option<Workspace>> {
169 let workspaces = discover_workspaces()?;
170 Ok(workspaces
171 .into_iter()
172 .find(|w| w.hash == hash || w.hash.starts_with(hash)))
173}
174
175pub fn get_workspace_by_path(project_path: &str) -> Result<Option<Workspace>> {
177 let workspaces = discover_workspaces()?;
178 let target_path = normalize_path(project_path);
179
180 Ok(workspaces.into_iter().find(|w| {
181 w.project_path
182 .as_ref()
183 .map(|p| normalize_path(p) == target_path)
184 .unwrap_or(false)
185 }))
186}
187
188pub fn find_workspace_by_path(
191 project_path: &str,
192) -> Result<Option<(String, PathBuf, Option<String>)>> {
193 let storage_path = get_workspace_storage_path()?;
194
195 if !storage_path.exists() {
196 return Ok(None);
197 }
198
199 let target_path = normalize_path(project_path);
200 let mut matches: Vec<(String, PathBuf, Option<String>, std::time::SystemTime)> = Vec::new();
201
202 for entry in std::fs::read_dir(&storage_path)? {
203 let entry = entry?;
204 let workspace_dir = entry.path();
205
206 if !workspace_dir.is_dir() {
207 continue;
208 }
209
210 let workspace_json_path = workspace_dir.join("workspace.json");
211 if !workspace_json_path.exists() {
212 continue;
213 }
214
215 if let Ok(content) = std::fs::read_to_string(&workspace_json_path) {
216 if let Ok(ws_json) = serde_json::from_str::<WorkspaceJson>(&content) {
217 if let Some(folder) = &ws_json.folder {
218 let folder_path = decode_workspace_folder(folder);
219 if normalize_path(&folder_path) == target_path {
220 let chat_sessions_dir = workspace_dir.join("chatSessions");
222 let last_modified = if chat_sessions_dir.exists() {
223 std::fs::read_dir(&chat_sessions_dir)
224 .ok()
225 .and_then(|entries| {
226 entries
227 .filter_map(|e| e.ok())
228 .filter_map(|e| e.metadata().ok())
229 .filter_map(|m| m.modified().ok())
230 .max()
231 })
232 .unwrap_or_else(|| {
233 chat_sessions_dir
234 .metadata()
235 .and_then(|m| m.modified())
236 .unwrap_or(std::time::UNIX_EPOCH)
237 })
238 } else {
239 workspace_dir
240 .metadata()
241 .and_then(|m| m.modified())
242 .unwrap_or(std::time::UNIX_EPOCH)
243 };
244
245 matches.push((
246 entry.file_name().to_string_lossy().to_string(),
247 workspace_dir,
248 Some(folder_path),
249 last_modified,
250 ));
251 }
252 }
253 }
254 }
255 }
256
257 matches.sort_by(|a, b| b.3.cmp(&a.3));
259
260 Ok(matches
261 .into_iter()
262 .next()
263 .map(|(id, path, folder, _)| (id, path, folder)))
264}
265
266pub fn find_all_workspaces_for_project(project_name: &str) -> Result<Vec<WorkspaceInfo>> {
268 let storage_path = get_workspace_storage_path()?;
269
270 if !storage_path.exists() {
271 return Ok(Vec::new());
272 }
273
274 let project_name_lower = project_name.to_lowercase();
275 let mut workspaces = Vec::new();
276
277 for entry in std::fs::read_dir(&storage_path)? {
278 let entry = entry?;
279 let workspace_dir = entry.path();
280
281 if !workspace_dir.is_dir() {
282 continue;
283 }
284
285 let workspace_json_path = workspace_dir.join("workspace.json");
286 if !workspace_json_path.exists() {
287 continue;
288 }
289
290 if let Ok(content) = std::fs::read_to_string(&workspace_json_path) {
291 if let Ok(ws_json) = serde_json::from_str::<WorkspaceJson>(&content) {
292 if let Some(folder) = &ws_json.folder {
293 let folder_path = decode_workspace_folder(folder);
294
295 if folder_path.to_lowercase().contains(&project_name_lower) {
296 let chat_sessions_dir = workspace_dir.join("chatSessions");
297
298 let last_modified = if chat_sessions_dir.exists() {
299 std::fs::read_dir(&chat_sessions_dir)
300 .ok()
301 .and_then(|entries| {
302 entries
303 .filter_map(|e| e.ok())
304 .filter_map(|e| e.metadata().ok())
305 .filter_map(|m| m.modified().ok())
306 .max()
307 })
308 .unwrap_or_else(|| {
309 chat_sessions_dir
310 .metadata()
311 .and_then(|m| m.modified())
312 .unwrap_or(std::time::UNIX_EPOCH)
313 })
314 } else {
315 workspace_dir
316 .metadata()
317 .and_then(|m| m.modified())
318 .unwrap_or(std::time::UNIX_EPOCH)
319 };
320
321 workspaces.push((
322 entry.file_name().to_string_lossy().to_string(),
323 workspace_dir,
324 Some(folder_path),
325 last_modified,
326 ));
327 }
328 }
329 }
330 }
331 }
332
333 workspaces.sort_by(|a, b| b.3.cmp(&a.3));
335
336 Ok(workspaces)
337}
338
339pub fn get_chat_sessions_from_workspace(workspace_dir: &Path) -> Result<Vec<SessionWithPath>> {
341 let chat_sessions_dir = workspace_dir.join("chatSessions");
342
343 if !chat_sessions_dir.exists() {
344 return Ok(Vec::new());
345 }
346
347 let mut sessions = Vec::new();
348
349 for entry in std::fs::read_dir(&chat_sessions_dir)? {
350 let entry = entry?;
351 let path = entry.path();
352
353 if path.extension().map(|e| e == "json").unwrap_or(false) {
354 if let Ok(content) = std::fs::read_to_string(&path) {
355 if let Ok(session) = serde_json::from_str::<ChatSession>(&content) {
356 sessions.push(SessionWithPath { path, session });
357 }
358 }
359 }
360 }
361
362 Ok(sessions)
363}
364
365use chrono::Utc;