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