ccf_gpui_widgets/utils/
path.rs1use std::path::{Path, PathBuf};
21
22#[derive(Debug, Clone)]
24pub struct PathInfo {
25 pub existing_canonical: PathBuf,
27 pub non_existing_suffix: PathBuf,
29 pub full_path: PathBuf,
31}
32
33impl PathInfo {
34 pub fn fully_exists(&self) -> bool {
36 self.non_existing_suffix.as_os_str().is_empty()
37 }
38
39 pub fn full_path_string(&self) -> String {
41 self.full_path.to_string_lossy().to_string()
42 }
43
44 pub fn empty() -> Self {
46 Self {
47 existing_canonical: PathBuf::new(),
48 non_existing_suffix: PathBuf::new(),
49 full_path: PathBuf::new(),
50 }
51 }
52}
53
54impl Default for PathInfo {
55 fn default() -> Self {
56 Self::empty()
57 }
58}
59
60pub fn parse_path(input: &str) -> PathInfo {
84 if input.is_empty() {
85 return PathInfo::empty();
86 }
87
88 let expanded = expand_tilde(input);
90 let path = Path::new(&expanded);
91
92 let absolute = if path.is_absolute() {
94 path.to_path_buf()
95 } else {
96 std::env::current_dir()
98 .map(|cwd| cwd.join(path))
99 .unwrap_or_else(|err| {
100 log::warn!("Could not determine current directory: {}. Using relative path.", err);
101 path.to_path_buf()
102 })
103 };
104
105 let (existing, suffix) = find_existing_prefix(&absolute);
107
108 let canonical = existing
110 .canonicalize()
111 .unwrap_or_else(|err| {
112 log::warn!("Could not canonicalize path {:?}: {}", existing, err);
113 existing.clone()
114 });
115
116 let full = if suffix.as_os_str().is_empty() {
118 canonical.clone()
119 } else {
120 canonical.join(&suffix)
121 };
122
123 PathInfo {
124 existing_canonical: canonical,
125 non_existing_suffix: suffix,
126 full_path: full,
127 }
128}
129
130#[cfg(feature = "file-picker")]
132pub fn expand_tilde(path: &str) -> String {
133 if path.starts_with("~/") || path == "~" {
134 if let Some(home) = dirs::home_dir() {
135 if path == "~" {
136 return home.to_string_lossy().to_string();
137 } else {
138 return home.join(&path[2..]).to_string_lossy().to_string();
139 }
140 }
141 }
142 path.to_string()
143}
144
145#[cfg(not(feature = "file-picker"))]
147pub fn expand_tilde(path: &str) -> String {
148 if path.starts_with("~/") || path == "~" {
149 if let Ok(home) = std::env::var("HOME") {
150 if path == "~" {
151 return home;
152 } else {
153 return format!("{}/{}", home, &path[2..]);
154 }
155 }
156 }
157 path.to_string()
158}
159
160fn build_suffix(components: &[std::ffi::OsString]) -> PathBuf {
162 components
163 .iter()
164 .rev()
165 .fold(PathBuf::new(), |acc, comp| acc.join(comp))
166}
167
168fn find_existing_prefix(path: &Path) -> (PathBuf, PathBuf) {
170 let mut current = path.to_path_buf();
171 let mut suffix_components = Vec::new();
172
173 loop {
174 if current.exists() {
175 return (current, build_suffix(&suffix_components));
176 }
177
178 match current.file_name() {
179 Some(component) => {
180 suffix_components.push(component.to_os_string());
181 current = current
182 .parent()
183 .map(|p| p.to_path_buf())
184 .unwrap_or_else(|| PathBuf::from("/"));
185 }
186 None => {
187 return (PathBuf::from("/"), build_suffix(&suffix_components));
188 }
189 }
190
191 if current.as_os_str().is_empty() || current == Path::new("/") {
192 return (current, build_suffix(&suffix_components));
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_expand_tilde() {
203 let expanded = expand_tilde("~");
205 assert!(!expanded.is_empty());
206 assert!(!expanded.starts_with('~') || expanded == "~"); let with_path = expand_tilde("~/Documents");
209 assert!(with_path.ends_with("/Documents") || with_path == "~/Documents");
210 }
211
212 #[test]
213 fn test_parse_existing_path() {
214 let temp = std::env::temp_dir();
215 let info = parse_path(temp.to_str().unwrap());
216 assert!(info.fully_exists());
217 assert_eq!(info.non_existing_suffix, PathBuf::new());
218 }
219
220 #[test]
221 fn test_parse_non_existing_file() {
222 let temp = std::env::temp_dir();
223 let non_existing = temp.join("this_file_should_not_exist_12345.txt");
224 let info = parse_path(non_existing.to_str().unwrap());
225
226 assert!(!info.fully_exists());
227 assert_eq!(info.non_existing_suffix, PathBuf::from("this_file_should_not_exist_12345.txt"));
228 }
229
230 #[test]
231 fn test_empty_path() {
232 let info = parse_path("");
233 assert!(info.full_path.as_os_str().is_empty());
234 }
235}