diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 71203874f69494..b5b29ba83693b4 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -3,7 +3,7 @@ use fuzzy::StringMatchCandidate; use picker::{Picker, PickerDelegate}; use project::DirectoryLister; use std::{ - path::{Path, PathBuf}, + path::{Path, PathBuf, MAIN_SEPARATOR_STR}, sync::{ atomic::{self, AtomicBool}, Arc, @@ -40,12 +40,19 @@ impl OpenPathDelegate { } } +#[derive(Debug)] struct DirectoryState { path: String, - match_candidates: Vec, + match_candidates: Vec, error: Option, } +#[derive(Debug, Clone)] +struct CandidateInfo { + path: StringMatchCandidate, + is_dir: bool, +} + impl OpenPathPrompt { pub(crate) fn register( workspace: &mut Workspace, @@ -93,8 +100,6 @@ impl PickerDelegate for OpenPathDelegate { cx.notify(); } - // todo(windows) - // Is this method woring correctly on Windows? This method uses `/` for path separator. fn update_matches( &mut self, query: String, @@ -102,13 +107,26 @@ impl PickerDelegate for OpenPathDelegate { cx: &mut Context>, ) -> gpui::Task<()> { let lister = self.lister.clone(); - let (mut dir, suffix) = if let Some(index) = query.rfind('/') { - (query[..index].to_string(), query[index + 1..].to_string()) + let query_path = Path::new(&query); + let last_item = query_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(&last_item) { + (dir.to_string(), last_item) } else { (query, String::new()) }; if dir == "" { - dir = "/".to_string(); + #[cfg(not(target_os = "windows"))] + { + dir = "/".to_string(); + } + #[cfg(target_os = "windows")] + { + dir = "C:\\".to_string(); + } } let query = if self @@ -134,12 +152,16 @@ impl PickerDelegate for OpenPathDelegate { this.update(&mut cx, |this, _| { this.delegate.directory_state = Some(match paths { Ok(mut paths) => { - paths.sort_by(|a, b| compare_paths((a, true), (b, true))); + paths.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true))); let match_candidates = paths .iter() .enumerate() - .map(|(ix, path)| { - StringMatchCandidate::new(ix, &path.to_string_lossy()) + .map(|(ix, item)| CandidateInfo { + path: StringMatchCandidate::new( + ix, + &item.path.to_string_lossy(), + ), + is_dir: item.is_dir, }) .collect::>(); @@ -178,7 +200,7 @@ impl PickerDelegate for OpenPathDelegate { }; if !suffix.starts_with('.') { - match_candidates.retain(|m| !m.string.starts_with('.')); + match_candidates.retain(|m| !m.path.string.starts_with('.')); } if suffix == "" { @@ -186,7 +208,7 @@ impl PickerDelegate for OpenPathDelegate { this.delegate.matches.clear(); this.delegate .matches - .extend(match_candidates.iter().map(|m| m.id)); + .extend(match_candidates.iter().map(|m| m.path.id)); cx.notify(); }) @@ -194,8 +216,9 @@ impl PickerDelegate for OpenPathDelegate { return; } + let candidates = match_candidates.iter().map(|m| &m.path).collect::>(); let matches = fuzzy::match_strings( - match_candidates.as_slice(), + candidates.as_slice(), &suffix, false, 100, @@ -217,7 +240,7 @@ impl PickerDelegate for OpenPathDelegate { this.delegate.directory_state.as_ref().and_then(|d| { d.match_candidates .get(*m) - .map(|c| !c.string.starts_with(&suffix)) + .map(|c| !c.path.string.starts_with(&suffix)) }), *m, ) @@ -239,7 +262,16 @@ impl PickerDelegate for OpenPathDelegate { let m = self.matches.get(self.selected_index)?; let directory_state = self.directory_state.as_ref()?; let candidate = directory_state.match_candidates.get(*m)?; - Some(format!("{}/{}", directory_state.path, candidate.string)) + Some(format!( + "{}{}{}", + directory_state.path, + candidate.path.string, + if candidate.is_dir { + MAIN_SEPARATOR_STR + } else { + "" + } + )) }) .unwrap_or(query), ) @@ -260,7 +292,7 @@ impl PickerDelegate for OpenPathDelegate { .resolve_tilde(&directory_state.path, cx) .as_ref(), ) - .join(&candidate.string); + .join(&candidate.path.string); if let Some(tx) = self.tx.take() { tx.send(Some(vec![result])).ok(); } @@ -294,7 +326,7 @@ impl PickerDelegate for OpenPathDelegate { .spacing(ListItemSpacing::Sparse) .inset(true) .toggle_state(selected) - .child(LabelLike::new().child(candidate.string.clone())), + .child(LabelLike::new().child(candidate.path.string.clone())), ) } @@ -307,6 +339,6 @@ impl PickerDelegate for OpenPathDelegate { } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::from("[directory/]filename.ext") + Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext")) } } diff --git a/crates/fuzzy/src/matcher.rs b/crates/fuzzy/src/matcher.rs index 66a480d87a8151..5e520eccb73295 100644 --- a/crates/fuzzy/src/matcher.rs +++ b/crates/fuzzy/src/matcher.rs @@ -1,5 +1,5 @@ use std::{ - borrow::Cow, + borrow::{Borrow, Cow}, sync::atomic::{self, AtomicBool}, }; @@ -50,22 +50,24 @@ impl<'a> Matcher<'a> { /// Filter and score fuzzy match candidates. Results are returned unsorted, in the same order as /// the input candidates. - pub fn match_candidates( + pub fn match_candidates( &mut self, prefix: &[char], lowercase_prefix: &[char], - candidates: impl Iterator, + candidates: impl Iterator, results: &mut Vec, cancel_flag: &AtomicBool, build_match: F, ) where + C: MatchCandidate, + T: Borrow, F: Fn(&C, f64, &Vec) -> R, { let mut candidate_chars = Vec::new(); let mut lowercase_candidate_chars = Vec::new(); for candidate in candidates { - if !candidate.has_chars(self.query_char_bag) { + if !candidate.borrow().has_chars(self.query_char_bag) { continue; } @@ -75,7 +77,7 @@ impl<'a> Matcher<'a> { candidate_chars.clear(); lowercase_candidate_chars.clear(); - for c in candidate.to_string().chars() { + for c in candidate.borrow().to_string().chars() { candidate_chars.push(c); lowercase_candidate_chars.append(&mut c.to_lowercase().collect::>()); } @@ -98,7 +100,11 @@ impl<'a> Matcher<'a> { ); if score > 0.0 { - results.push(build_match(&candidate, score, &self.match_positions)); + results.push(build_match( + candidate.borrow(), + score, + &self.match_positions, + )); } } } diff --git a/crates/fuzzy/src/strings.rs b/crates/fuzzy/src/strings.rs index 458278739ab2a8..419127ed6e6cac 100644 --- a/crates/fuzzy/src/strings.rs +++ b/crates/fuzzy/src/strings.rs @@ -4,7 +4,7 @@ use crate::{ }; use gpui::BackgroundExecutor; use std::{ - borrow::Cow, + borrow::{Borrow, Cow}, cmp::{self, Ordering}, iter, ops::Range, @@ -113,14 +113,17 @@ impl Ord for StringMatch { } } -pub async fn match_strings( - candidates: &[StringMatchCandidate], +pub async fn match_strings( + candidates: &[T], query: &str, smart_case: bool, max_results: usize, cancel_flag: &AtomicBool, executor: BackgroundExecutor, -) -> Vec { +) -> Vec +where + T: Borrow + Sync, +{ if candidates.is_empty() || max_results == 0 { return Default::default(); } @@ -129,10 +132,10 @@ pub async fn match_strings( return candidates .iter() .map(|candidate| StringMatch { - candidate_id: candidate.id, + candidate_id: candidate.borrow().id, score: 0., positions: Default::default(), - string: candidate.string.clone(), + string: candidate.borrow().string.clone(), }) .collect(); } @@ -163,10 +166,12 @@ pub async fn match_strings( matcher.match_candidates( &[], &[], - candidates[segment_start..segment_end].iter(), + candidates[segment_start..segment_end] + .iter() + .map(|c| c.borrow()), results, cancel_flag, - |candidate, score, positions| StringMatch { + |candidate: &&StringMatchCandidate, score, positions| StringMatch { candidate_id: candidate.id, score, positions: positions.clone(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7d51fbc9c69ec1..10c0e80e2747b1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -524,6 +524,12 @@ enum EntitySubscription { SettingsObserver(PendingEntitySubscription), } +#[derive(Debug, Clone)] +pub struct DirectoryItem { + pub path: PathBuf, + pub is_dir: bool, +} + #[derive(Clone)] pub enum DirectoryLister { Project(Entity), @@ -552,10 +558,10 @@ impl DirectoryLister { return worktree.read(cx).abs_path().to_string_lossy().to_string(); } }; - "~/".to_string() + format!("~{}", std::path::MAIN_SEPARATOR_STR) } - pub fn list_directory(&self, path: String, cx: &mut App) -> Task>> { + pub fn list_directory(&self, path: String, cx: &mut App) -> Task>> { match self { DirectoryLister::Project(project) => { project.update(cx, |project, cx| project.list_directory(path, cx)) @@ -568,8 +574,12 @@ impl DirectoryLister { let query = Path::new(expanded.as_ref()); let mut response = fs.read_dir(query).await?; while let Some(path) = response.next().await { - if let Some(file_name) = path?.file_name() { - results.push(PathBuf::from(file_name.to_os_string())); + let path = path?; + if let Some(file_name) = path.file_name() { + results.push(DirectoryItem { + path: PathBuf::from(file_name.to_os_string()), + is_dir: fs.is_dir(&path).await, + }); } } Ok(results) @@ -3479,7 +3489,7 @@ impl Project { &self, query: String, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { if self.is_local() { DirectoryLister::Local(self.fs.clone()).list_directory(query, cx) } else if let Some(session) = self.ssh_client.as_ref() { @@ -3487,12 +3497,23 @@ impl Project { let request = proto::ListRemoteDirectory { dev_server_id: SSH_PROJECT_ID, path: path_buf.to_proto(), + config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }), }; let response = session.read(cx).proto_client().request(request); cx.background_spawn(async move { - let response = response.await?; - Ok(response.entries.into_iter().map(PathBuf::from).collect()) + let proto::ListRemoteDirectoryResponse { + entries, + entry_info, + } = response.await?; + Ok(entries + .into_iter() + .zip(entry_info) + .map(|(entry, info)| DirectoryItem { + path: PathBuf::from(entry), + is_dir: info.is_dir, + }) + .collect()) }) } else { Task::ready(Err(anyhow!("cannot list directory in remote project"))) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 2a708a7e31fe34..c9037342fd0fa2 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -561,13 +561,23 @@ message JoinProject { uint64 project_id = 1; } +message ListRemoteDirectoryConfig { + bool is_dir = 1; +} + message ListRemoteDirectory { uint64 dev_server_id = 1; string path = 2; + ListRemoteDirectoryConfig config = 3; +} + +message EntryInfo { + bool is_dir = 1; } message ListRemoteDirectoryResponse { repeated string entries = 1; + repeated EntryInfo entry_info = 2; } message JoinProjectResponse { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 4f09647e17cc00..fadd603b502f20 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -554,15 +554,29 @@ impl HeadlessProject { ) -> Result { let fs = cx.read_entity(&this, |this, _| this.fs.clone())?; let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string()); + let check_info = envelope + .payload + .config + .as_ref() + .is_some_and(|config| config.is_dir); let mut entries = Vec::new(); + let mut entry_info = Vec::new(); let mut response = fs.read_dir(&expanded).await?; while let Some(path) = response.next().await { - if let Some(file_name) = path?.file_name() { + let path = path?; + if let Some(file_name) = path.file_name() { entries.push(file_name.to_string_lossy().to_string()); + if check_info { + let is_dir = fs.is_dir(&path).await; + entry_info.push(proto::EntryInfo { is_dir }); + } } } - Ok(proto::ListRemoteDirectoryResponse { entries }) + Ok(proto::ListRemoteDirectoryResponse { + entries, + entry_info, + }) } pub async fn handle_get_path_metadata(