Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

workspace: Add trailing / to directories on completion when using OpenPathPrompt #25430

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
68 changes: 50 additions & 18 deletions crates/file_finder/src/open_path_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,12 +40,19 @@ impl OpenPathDelegate {
}
}

#[derive(Debug)]
struct DirectoryState {
path: String,
match_candidates: Vec<StringMatchCandidate>,
match_candidates: Vec<CandidateInfo>,
error: Option<SharedString>,
}

#[derive(Debug, Clone)]
struct CandidateInfo {
path: StringMatchCandidate,
is_dir: bool,
}

impl OpenPathPrompt {
pub(crate) fn register(
workspace: &mut Workspace,
Expand Down Expand Up @@ -93,22 +100,33 @@ 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,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> 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
Expand All @@ -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::<Vec<_>>();

Expand Down Expand Up @@ -178,24 +200,25 @@ 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 == "" {
this.update(&mut cx, |this, cx| {
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();
})
.ok();
return;
}

let candidates = match_candidates.iter().map(|m| &m.path).collect::<Vec<_>>();
let matches = fuzzy::match_strings(
match_candidates.as_slice(),
candidates.as_slice(),
&suffix,
false,
100,
Expand All @@ -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,
)
Expand All @@ -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),
)
Expand All @@ -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();
}
Expand Down Expand Up @@ -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())),
)
}

Expand All @@ -307,6 +339,6 @@ impl PickerDelegate for OpenPathDelegate {
}

fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
Arc::from("[directory/]filename.ext")
Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
}
}
18 changes: 12 additions & 6 deletions crates/fuzzy/src/matcher.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
borrow::Cow,
borrow::{Borrow, Cow},
sync::atomic::{self, AtomicBool},
};

Expand Down Expand Up @@ -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<C: MatchCandidate, R, F>(
pub fn match_candidates<C, R, F, T>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
candidates: impl Iterator<Item = T>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
C: MatchCandidate,
T: Borrow<C>,
F: Fn(&C, f64, &Vec<usize>) -> 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;
}

Expand All @@ -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::<Vec<_>>());
}
Expand All @@ -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,
));
}
}
}
Expand Down
21 changes: 13 additions & 8 deletions crates/fuzzy/src/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
};
use gpui::BackgroundExecutor;
use std::{
borrow::Cow,
borrow::{Borrow, Cow},
cmp::{self, Ordering},
iter,
ops::Range,
Expand Down Expand Up @@ -113,14 +113,17 @@ impl Ord for StringMatch {
}
}

pub async fn match_strings(
candidates: &[StringMatchCandidate],
pub async fn match_strings<T>(
candidates: &[T],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
executor: BackgroundExecutor,
) -> Vec<StringMatch> {
) -> Vec<StringMatch>
where
T: Borrow<StringMatchCandidate> + Sync,
{
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
Expand All @@ -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();
}
Expand Down Expand Up @@ -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(),
Expand Down
35 changes: 28 additions & 7 deletions crates/project/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,12 @@ enum EntitySubscription {
SettingsObserver(PendingEntitySubscription<SettingsObserver>),
}

#[derive(Debug, Clone)]
pub struct DirectoryItem {
pub path: PathBuf,
pub is_dir: bool,
}

#[derive(Clone)]
pub enum DirectoryLister {
Project(Entity<Project>),
Expand Down Expand Up @@ -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<Result<Vec<PathBuf>>> {
pub fn list_directory(&self, path: String, cx: &mut App) -> Task<Result<Vec<DirectoryItem>>> {
match self {
DirectoryLister::Project(project) => {
project.update(cx, |project, cx| project.list_directory(path, cx))
Expand All @@ -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)
Expand Down Expand Up @@ -3479,20 +3489,31 @@ impl Project {
&self,
query: String,
cx: &mut Context<Self>,
) -> Task<Result<Vec<PathBuf>>> {
) -> Task<Result<Vec<DirectoryItem>>> {
if self.is_local() {
DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
} else if let Some(session) = self.ssh_client.as_ref() {
let path_buf = PathBuf::from(query);
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")))
Expand Down
10 changes: 10 additions & 0 deletions crates/proto/proto/zed.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading