Detailed changes
@@ -101,7 +101,10 @@ pub fn init(cx: &mut App) {
directories: true,
multiple: false,
},
- DirectoryLister::Local(workspace.app_state().fs.clone()),
+ DirectoryLister::Local(
+ workspace.project().clone(),
+ workspace.app_state().fs.clone(),
+ ),
window,
cx,
);
@@ -4,7 +4,6 @@ mod file_finder_tests;
mod open_path_prompt_tests;
pub mod file_finder_settings;
-mod new_path_prompt;
mod open_path_prompt;
use futures::future::join_all;
@@ -20,7 +19,6 @@ use gpui::{
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
Window, actions,
};
-use new_path_prompt::NewPathPrompt;
use open_path_prompt::OpenPathPrompt;
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -85,8 +83,8 @@ pub fn init_settings(cx: &mut App) {
pub fn init(cx: &mut App) {
init_settings(cx);
cx.observe_new(FileFinder::register).detach();
- cx.observe_new(NewPathPrompt::register).detach();
cx.observe_new(OpenPathPrompt::register).detach();
+ cx.observe_new(OpenPathPrompt::register_new_path).detach();
}
impl FileFinder {
@@ -1,526 +0,0 @@
-use futures::channel::oneshot;
-use fuzzy::PathMatch;
-use gpui::{Entity, HighlightStyle, StyledText};
-use picker::{Picker, PickerDelegate};
-use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
-use std::{
- path::{Path, PathBuf},
- sync::{
- Arc,
- atomic::{self, AtomicBool},
- },
-};
-use ui::{Context, ListItem, Window};
-use ui::{LabelLike, ListItemSpacing, highlight_ranges, prelude::*};
-use util::ResultExt;
-use workspace::Workspace;
-
-pub(crate) struct NewPathPrompt;
-
-#[derive(Debug, Clone)]
-struct Match {
- path_match: Option<PathMatch>,
- suffix: Option<String>,
-}
-
-impl Match {
- fn entry<'a>(&'a self, project: &'a Project, cx: &'a App) -> Option<&'a Entry> {
- if let Some(suffix) = &self.suffix {
- let (worktree, path) = if let Some(path_match) = &self.path_match {
- (
- project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx),
- path_match.path.join(suffix),
- )
- } else {
- (project.worktrees(cx).next(), PathBuf::from(suffix))
- };
-
- worktree.and_then(|worktree| worktree.read(cx).entry_for_path(path))
- } else if let Some(path_match) = &self.path_match {
- let worktree =
- project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?;
- worktree.read(cx).entry_for_path(path_match.path.as_ref())
- } else {
- None
- }
- }
-
- fn is_dir(&self, project: &Project, cx: &App) -> bool {
- self.entry(project, cx).is_some_and(|e| e.is_dir())
- || self.suffix.as_ref().is_some_and(|s| s.ends_with('/'))
- }
-
- fn relative_path(&self) -> String {
- if let Some(path_match) = &self.path_match {
- if let Some(suffix) = &self.suffix {
- format!(
- "{}/{}",
- path_match.path.to_string_lossy(),
- suffix.trim_end_matches('/')
- )
- } else {
- path_match.path.to_string_lossy().to_string()
- }
- } else if let Some(suffix) = &self.suffix {
- suffix.trim_end_matches('/').to_string()
- } else {
- "".to_string()
- }
- }
-
- fn project_path(&self, project: &Project, cx: &App) -> Option<ProjectPath> {
- let worktree_id = if let Some(path_match) = &self.path_match {
- WorktreeId::from_usize(path_match.worktree_id)
- } else if let Some(worktree) = project.visible_worktrees(cx).find(|worktree| {
- worktree
- .read(cx)
- .root_entry()
- .is_some_and(|entry| entry.is_dir())
- }) {
- worktree.read(cx).id()
- } else {
- // todo(): we should find_or_create a workspace.
- return None;
- };
-
- let path = PathBuf::from(self.relative_path());
-
- Some(ProjectPath {
- worktree_id,
- path: Arc::from(path),
- })
- }
-
- fn existing_prefix(&self, project: &Project, cx: &App) -> Option<PathBuf> {
- let worktree = project.worktrees(cx).next()?.read(cx);
- let mut prefix = PathBuf::new();
- let parts = self.suffix.as_ref()?.split('/');
- for part in parts {
- if worktree.entry_for_path(prefix.join(&part)).is_none() {
- return Some(prefix);
- }
- prefix = prefix.join(part);
- }
-
- None
- }
-
- fn styled_text(&self, project: &Project, window: &Window, cx: &App) -> StyledText {
- let mut text = "./".to_string();
- let mut highlights = Vec::new();
- let mut offset = text.len();
-
- let separator = '/';
- let dir_indicator = "[β¦]";
-
- if let Some(path_match) = &self.path_match {
- text.push_str(&path_match.path.to_string_lossy());
- let mut whole_path = PathBuf::from(path_match.path_prefix.to_string());
- whole_path = whole_path.join(path_match.path.clone());
- for (range, style) in highlight_ranges(
- &whole_path.to_string_lossy(),
- &path_match.positions,
- gpui::HighlightStyle::color(Color::Accent.color(cx)),
- ) {
- highlights.push((range.start + offset..range.end + offset, style))
- }
- text.push(separator);
- offset = text.len();
-
- if let Some(suffix) = &self.suffix {
- text.push_str(suffix);
- let entry = self.entry(project, cx);
- let color = if let Some(entry) = entry {
- if entry.is_dir() {
- Color::Accent
- } else {
- Color::Conflict
- }
- } else {
- Color::Created
- };
- highlights.push((
- offset..offset + suffix.len(),
- HighlightStyle::color(color.color(cx)),
- ));
- offset += suffix.len();
- if entry.is_some_and(|e| e.is_dir()) {
- text.push(separator);
- offset += separator.len_utf8();
-
- text.push_str(dir_indicator);
- highlights.push((
- offset..offset + dir_indicator.len(),
- HighlightStyle::color(Color::Muted.color(cx)),
- ));
- }
- } else {
- text.push_str(dir_indicator);
- highlights.push((
- offset..offset + dir_indicator.len(),
- HighlightStyle::color(Color::Muted.color(cx)),
- ))
- }
- } else if let Some(suffix) = &self.suffix {
- text.push_str(suffix);
- let existing_prefix_len = self
- .existing_prefix(project, cx)
- .map(|prefix| prefix.to_string_lossy().len())
- .unwrap_or(0);
-
- if existing_prefix_len > 0 {
- highlights.push((
- offset..offset + existing_prefix_len,
- HighlightStyle::color(Color::Accent.color(cx)),
- ));
- }
- highlights.push((
- offset + existing_prefix_len..offset + suffix.len(),
- HighlightStyle::color(if self.entry(project, cx).is_some() {
- Color::Conflict.color(cx)
- } else {
- Color::Created.color(cx)
- }),
- ));
- offset += suffix.len();
- if suffix.ends_with('/') {
- text.push_str(dir_indicator);
- highlights.push((
- offset..offset + dir_indicator.len(),
- HighlightStyle::color(Color::Muted.color(cx)),
- ));
- }
- }
-
- StyledText::new(text).with_default_highlights(&window.text_style().clone(), highlights)
- }
-}
-
-pub struct NewPathDelegate {
- project: Entity<Project>,
- tx: Option<oneshot::Sender<Option<ProjectPath>>>,
- selected_index: usize,
- matches: Vec<Match>,
- last_selected_dir: Option<String>,
- cancel_flag: Arc<AtomicBool>,
- should_dismiss: bool,
-}
-
-impl NewPathPrompt {
- pub(crate) fn register(
- workspace: &mut Workspace,
- _window: Option<&mut Window>,
- _cx: &mut Context<Workspace>,
- ) {
- workspace.set_prompt_for_new_path(Box::new(|workspace, window, cx| {
- let (tx, rx) = futures::channel::oneshot::channel();
- Self::prompt_for_new_path(workspace, tx, window, cx);
- rx
- }));
- }
-
- fn prompt_for_new_path(
- workspace: &mut Workspace,
- tx: oneshot::Sender<Option<ProjectPath>>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) {
- let project = workspace.project().clone();
- workspace.toggle_modal(window, cx, |window, cx| {
- let delegate = NewPathDelegate {
- project,
- tx: Some(tx),
- selected_index: 0,
- matches: vec![],
- cancel_flag: Arc::new(AtomicBool::new(false)),
- last_selected_dir: None,
- should_dismiss: true,
- };
-
- Picker::uniform_list(delegate, window, cx).width(rems(34.))
- });
- }
-}
-
-impl PickerDelegate for NewPathDelegate {
- type ListItem = ui::ListItem;
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(
- &mut self,
- ix: usize,
- _: &mut Window,
- cx: &mut Context<picker::Picker<Self>>,
- ) {
- self.selected_index = ix;
- cx.notify();
- }
-
- fn update_matches(
- &mut self,
- query: String,
- window: &mut Window,
- cx: &mut Context<picker::Picker<Self>>,
- ) -> gpui::Task<()> {
- let query = query
- .trim()
- .trim_start_matches("./")
- .trim_start_matches('/');
-
- let (dir, suffix) = if let Some(index) = query.rfind('/') {
- let suffix = if index + 1 < query.len() {
- Some(query[index + 1..].to_string())
- } else {
- None
- };
- (query[0..index].to_string(), suffix)
- } else {
- (query.to_string(), None)
- };
-
- let worktrees = self
- .project
- .read(cx)
- .visible_worktrees(cx)
- .collect::<Vec<_>>();
- let include_root_name = worktrees.len() > 1;
- let candidate_sets = worktrees
- .into_iter()
- .map(|worktree| {
- let worktree = worktree.read(cx);
- PathMatchCandidateSet {
- snapshot: worktree.snapshot(),
- include_ignored: worktree
- .root_entry()
- .map_or(false, |entry| entry.is_ignored),
- include_root_name,
- candidates: project::Candidates::Directories,
- }
- })
- .collect::<Vec<_>>();
-
- self.cancel_flag.store(true, atomic::Ordering::Relaxed);
- self.cancel_flag = Arc::new(AtomicBool::new(false));
-
- let cancel_flag = self.cancel_flag.clone();
- let query = query.to_string();
- let prefix = dir.clone();
- cx.spawn_in(window, async move |picker, cx| {
- let matches = fuzzy::match_path_sets(
- candidate_sets.as_slice(),
- &dir,
- None,
- false,
- 100,
- &cancel_flag,
- cx.background_executor().clone(),
- )
- .await;
- let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
- if did_cancel {
- return;
- }
- picker
- .update(cx, |picker, cx| {
- picker
- .delegate
- .set_search_matches(query, prefix, suffix, matches, cx)
- })
- .log_err();
- })
- }
-
- fn confirm_completion(
- &mut self,
- _: String,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<String> {
- self.confirm_update_query(window, cx)
- }
-
- fn confirm_update_query(
- &mut self,
- _: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<String> {
- let m = self.matches.get(self.selected_index)?;
- if m.is_dir(self.project.read(cx), cx) {
- let path = m.relative_path();
- let result = format!("{}/", path);
- self.last_selected_dir = Some(path);
- Some(result)
- } else {
- None
- }
- }
-
- fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
- let Some(m) = self.matches.get(self.selected_index) else {
- return;
- };
-
- let exists = m.entry(self.project.read(cx), cx).is_some();
- if exists {
- self.should_dismiss = false;
- let answer = window.prompt(
- gpui::PromptLevel::Critical,
- &format!("{} already exists. Do you want to replace it?", m.relative_path()),
- Some(
- "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
- ),
- &["Replace", "Cancel"],
- cx);
- let m = m.clone();
- cx.spawn_in(window, async move |picker, cx| {
- let answer = answer.await.ok();
- picker
- .update(cx, |picker, cx| {
- picker.delegate.should_dismiss = true;
- if answer != Some(0) {
- return;
- }
- if let Some(path) = m.project_path(picker.delegate.project.read(cx), cx) {
- if let Some(tx) = picker.delegate.tx.take() {
- tx.send(Some(path)).ok();
- }
- }
- cx.emit(gpui::DismissEvent);
- })
- .ok();
- })
- .detach();
- return;
- }
-
- if let Some(path) = m.project_path(self.project.read(cx), cx) {
- if let Some(tx) = self.tx.take() {
- tx.send(Some(path)).ok();
- }
- }
- cx.emit(gpui::DismissEvent);
- }
-
- fn should_dismiss(&self) -> bool {
- self.should_dismiss
- }
-
- fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
- if let Some(tx) = self.tx.take() {
- tx.send(None).ok();
- }
- cx.emit(gpui::DismissEvent)
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- window: &mut Window,
- cx: &mut Context<picker::Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let m = self.matches.get(ix)?;
-
- Some(
- ListItem::new(ix)
- .spacing(ListItemSpacing::Sparse)
- .inset(true)
- .toggle_state(selected)
- .child(LabelLike::new().child(m.styled_text(self.project.read(cx), window, cx))),
- )
- }
-
- fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
- Some("Type a path...".into())
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- Arc::from("[directory/]filename.ext")
- }
-}
-
-impl NewPathDelegate {
- fn set_search_matches(
- &mut self,
- query: String,
- prefix: String,
- suffix: Option<String>,
- matches: Vec<PathMatch>,
- cx: &mut Context<Picker<Self>>,
- ) {
- cx.notify();
- if query.is_empty() {
- self.matches = self
- .project
- .read(cx)
- .worktrees(cx)
- .flat_map(|worktree| {
- let worktree_id = worktree.read(cx).id();
- worktree
- .read(cx)
- .child_entries(Path::new(""))
- .filter_map(move |entry| {
- entry.is_dir().then(|| Match {
- path_match: Some(PathMatch {
- score: 1.0,
- positions: Default::default(),
- worktree_id: worktree_id.to_usize(),
- path: entry.path.clone(),
- path_prefix: "".into(),
- is_dir: entry.is_dir(),
- distance_to_relative_ancestor: 0,
- }),
- suffix: None,
- })
- })
- })
- .collect();
-
- return;
- }
-
- let mut directory_exists = false;
-
- self.matches = matches
- .into_iter()
- .map(|m| {
- if m.path.as_ref().to_string_lossy() == prefix {
- directory_exists = true
- }
- Match {
- path_match: Some(m),
- suffix: suffix.clone(),
- }
- })
- .collect();
-
- if !directory_exists {
- if suffix.is_none()
- || self
- .last_selected_dir
- .as_ref()
- .is_some_and(|d| query.starts_with(d))
- {
- self.matches.insert(
- 0,
- Match {
- path_match: None,
- suffix: Some(query.clone()),
- },
- )
- } else {
- self.matches.push(Match {
- path_match: None,
- suffix: Some(query.clone()),
- })
- }
- }
- }
-}
@@ -2,6 +2,7 @@ use crate::file_finder_settings::FileFinderSettings;
use file_icons::FileIcons;
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{HighlightStyle, StyledText, Task};
use picker::{Picker, PickerDelegate};
use project::{DirectoryItem, DirectoryLister};
use settings::Settings;
@@ -12,61 +13,136 @@ use std::{
atomic::{self, AtomicBool},
},
};
-use ui::{Context, ListItem, Window};
+use ui::{Context, LabelLike, ListItem, Window};
use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
use util::{maybe, paths::compare_paths};
use workspace::Workspace;
pub(crate) struct OpenPathPrompt;
+#[cfg(target_os = "windows")]
+const PROMPT_ROOT: &str = "C:\\";
+#[cfg(not(target_os = "windows"))]
+const PROMPT_ROOT: &str = "/";
+
+#[derive(Debug)]
pub struct OpenPathDelegate {
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
lister: DirectoryLister,
selected_index: usize,
- directory_state: Option<DirectoryState>,
- matches: Vec<usize>,
+ directory_state: DirectoryState,
string_matches: Vec<StringMatch>,
cancel_flag: Arc<AtomicBool>,
should_dismiss: bool,
+ replace_prompt: Task<()>,
}
impl OpenPathDelegate {
- pub fn new(tx: oneshot::Sender<Option<Vec<PathBuf>>>, lister: DirectoryLister) -> Self {
+ pub fn new(
+ tx: oneshot::Sender<Option<Vec<PathBuf>>>,
+ lister: DirectoryLister,
+ creating_path: bool,
+ ) -> Self {
Self {
tx: Some(tx),
lister,
selected_index: 0,
- directory_state: None,
- matches: Vec::new(),
+ directory_state: DirectoryState::None {
+ create: creating_path,
+ },
string_matches: Vec::new(),
cancel_flag: Arc::new(AtomicBool::new(false)),
should_dismiss: true,
+ replace_prompt: Task::ready(()),
+ }
+ }
+
+ fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
+ match &self.directory_state {
+ DirectoryState::List { entries, .. } => {
+ let id = self.string_matches.get(selected_match_index)?.candidate_id;
+ entries.iter().find(|entry| entry.path.id == id).cloned()
+ }
+ DirectoryState::Create {
+ user_input,
+ entries,
+ ..
+ } => {
+ let mut i = selected_match_index;
+ if let Some(user_input) = user_input {
+ if !user_input.exists || !user_input.is_dir {
+ if i == 0 {
+ return Some(CandidateInfo {
+ path: user_input.file.clone(),
+ is_dir: false,
+ });
+ } else {
+ i -= 1;
+ }
+ }
+ }
+ let id = self.string_matches.get(i)?.candidate_id;
+ entries.iter().find(|entry| entry.path.id == id).cloned()
+ }
+ DirectoryState::None { .. } => None,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn collect_match_candidates(&self) -> Vec<String> {
- if let Some(state) = self.directory_state.as_ref() {
- self.matches
+ match &self.directory_state {
+ DirectoryState::List { entries, .. } => self
+ .string_matches
.iter()
- .filter_map(|&index| {
- state
- .match_candidates
- .get(index)
+ .filter_map(|string_match| {
+ entries
+ .iter()
+ .find(|entry| entry.path.id == string_match.candidate_id)
.map(|candidate| candidate.path.string.clone())
})
- .collect()
- } else {
- Vec::new()
+ .collect(),
+ DirectoryState::Create {
+ user_input,
+ entries,
+ ..
+ } => user_input
+ .into_iter()
+ .filter(|user_input| !user_input.exists || !user_input.is_dir)
+ .map(|user_input| user_input.file.string.clone())
+ .chain(self.string_matches.iter().filter_map(|string_match| {
+ entries
+ .iter()
+ .find(|entry| entry.path.id == string_match.candidate_id)
+ .map(|candidate| candidate.path.string.clone())
+ }))
+ .collect(),
+ DirectoryState::None { .. } => Vec::new(),
}
}
}
#[derive(Debug)]
-struct DirectoryState {
- path: String,
- match_candidates: Vec<CandidateInfo>,
- error: Option<SharedString>,
+enum DirectoryState {
+ List {
+ parent_path: String,
+ entries: Vec<CandidateInfo>,
+ error: Option<SharedString>,
+ },
+ Create {
+ parent_path: String,
+ user_input: Option<UserInput>,
+ entries: Vec<CandidateInfo>,
+ },
+ None {
+ create: bool,
+ },
+}
+
+#[derive(Debug, Clone)]
+struct UserInput {
+ file: StringMatchCandidate,
+ exists: bool,
+ is_dir: bool,
}
#[derive(Debug, Clone)]
@@ -83,7 +159,19 @@ impl OpenPathPrompt {
) {
workspace.set_prompt_for_open_path(Box::new(|workspace, lister, window, cx| {
let (tx, rx) = futures::channel::oneshot::channel();
- Self::prompt_for_open_path(workspace, lister, tx, window, cx);
+ Self::prompt_for_open_path(workspace, lister, false, tx, window, cx);
+ rx
+ }));
+ }
+
+ pub(crate) fn register_new_path(
+ workspace: &mut Workspace,
+ _window: Option<&mut Window>,
+ _: &mut Context<Workspace>,
+ ) {
+ workspace.set_prompt_for_new_path(Box::new(|workspace, lister, window, cx| {
+ let (tx, rx) = futures::channel::oneshot::channel();
+ Self::prompt_for_open_path(workspace, lister, true, tx, window, cx);
rx
}));
}
@@ -91,13 +179,13 @@ impl OpenPathPrompt {
fn prompt_for_open_path(
workspace: &mut Workspace,
lister: DirectoryLister,
+ creating_path: bool,
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |window, cx| {
- let delegate = OpenPathDelegate::new(tx, lister.clone());
-
+ let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
let query = lister.default_query(cx);
picker.set_query(query, window, cx);
@@ -110,7 +198,16 @@ impl PickerDelegate for OpenPathDelegate {
type ListItem = ui::ListItem;
fn match_count(&self) -> usize {
- self.matches.len()
+ let user_input = if let DirectoryState::Create { user_input, .. } = &self.directory_state {
+ user_input
+ .as_ref()
+ .filter(|input| !input.exists || !input.is_dir)
+ .into_iter()
+ .count()
+ } else {
+ 0
+ };
+ self.string_matches.len() + user_input
}
fn selected_index(&self) -> usize {
@@ -127,127 +224,196 @@ impl PickerDelegate for OpenPathDelegate {
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
- ) -> gpui::Task<()> {
- let lister = self.lister.clone();
- let query_path = Path::new(&query);
- let last_item = query_path
+ ) -> Task<()> {
+ let lister = &self.lister;
+ let last_item = Path::new(&query)
.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)
+ .to_string_lossy();
+ let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
+ (dir.to_string(), last_item.into_owned())
} else {
(query, String::new())
};
-
if dir == "" {
- #[cfg(not(target_os = "windows"))]
- {
- dir = "/".to_string();
- }
- #[cfg(target_os = "windows")]
- {
- dir = "C:\\".to_string();
- }
+ dir = PROMPT_ROOT.to_string();
}
- let query = if self
- .directory_state
- .as_ref()
- .map_or(false, |s| s.path == dir)
- {
- None
- } else {
- Some(lister.list_directory(dir.clone(), cx))
+ let query = match &self.directory_state {
+ DirectoryState::List { parent_path, .. } => {
+ if parent_path == &dir {
+ None
+ } else {
+ Some(lister.list_directory(dir.clone(), cx))
+ }
+ }
+ DirectoryState::Create {
+ parent_path,
+ user_input,
+ ..
+ } => {
+ if parent_path == &dir
+ && user_input.as_ref().map(|input| &input.file.string) == Some(&suffix)
+ {
+ None
+ } else {
+ Some(lister.list_directory(dir.clone(), cx))
+ }
+ }
+ DirectoryState::None { .. } => Some(lister.list_directory(dir.clone(), cx)),
};
- self.cancel_flag.store(true, atomic::Ordering::Relaxed);
+ self.cancel_flag.store(true, atomic::Ordering::Release);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
cx.spawn_in(window, async move |this, cx| {
if let Some(query) = query {
let paths = query.await;
- if cancel_flag.load(atomic::Ordering::Relaxed) {
+ if cancel_flag.load(atomic::Ordering::Acquire) {
return;
}
- this.update(cx, |this, _| {
- this.delegate.directory_state = Some(match paths {
- Ok(mut paths) => {
- if dir == "/" {
- paths.push(DirectoryItem {
- is_dir: true,
- path: Default::default(),
- });
- }
-
- paths.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
- let match_candidates = paths
- .iter()
- .enumerate()
- .map(|(ix, item)| CandidateInfo {
- path: StringMatchCandidate::new(
- ix,
- &item.path.to_string_lossy(),
- ),
- is_dir: item.is_dir,
- })
- .collect::<Vec<_>>();
-
- DirectoryState {
- match_candidates,
- path: dir,
- error: None,
- }
- }
- Err(err) => DirectoryState {
- match_candidates: vec![],
- path: dir,
- error: Some(err.to_string().into()),
- },
- });
- })
- .ok();
+ if this
+ .update(cx, |this, _| {
+ let new_state = match &this.delegate.directory_state {
+ DirectoryState::None { create: false }
+ | DirectoryState::List { .. } => match paths {
+ Ok(paths) => DirectoryState::List {
+ entries: path_candidates(&dir, paths),
+ parent_path: dir.clone(),
+ error: None,
+ },
+ Err(e) => DirectoryState::List {
+ entries: Vec::new(),
+ parent_path: dir.clone(),
+ error: Some(SharedString::from(e.to_string())),
+ },
+ },
+ DirectoryState::None { create: true }
+ | DirectoryState::Create { .. } => match paths {
+ Ok(paths) => {
+ let mut entries = path_candidates(&dir, paths);
+ let mut exists = false;
+ let mut is_dir = false;
+ let mut new_id = None;
+ entries.retain(|entry| {
+ new_id = new_id.max(Some(entry.path.id));
+ if entry.path.string == suffix {
+ exists = true;
+ is_dir = entry.is_dir;
+ }
+ !exists || is_dir
+ });
+
+ let new_id = new_id.map(|id| id + 1).unwrap_or(0);
+ let user_input = if suffix.is_empty() {
+ None
+ } else {
+ Some(UserInput {
+ file: StringMatchCandidate::new(new_id, &suffix),
+ exists,
+ is_dir,
+ })
+ };
+ DirectoryState::Create {
+ entries,
+ parent_path: dir.clone(),
+ user_input,
+ }
+ }
+ Err(_) => DirectoryState::Create {
+ entries: Vec::new(),
+ parent_path: dir.clone(),
+ user_input: Some(UserInput {
+ exists: false,
+ is_dir: false,
+ file: StringMatchCandidate::new(0, &suffix),
+ }),
+ },
+ },
+ };
+ this.delegate.directory_state = new_state;
+ })
+ .is_err()
+ {
+ return;
+ }
}
- let match_candidates = this
- .update(cx, |this, cx| {
- let directory_state = this.delegate.directory_state.as_ref()?;
- if directory_state.error.is_some() {
- this.delegate.matches.clear();
- this.delegate.selected_index = 0;
- cx.notify();
- return None;
+ let Ok(mut new_entries) =
+ this.update(cx, |this, _| match &this.delegate.directory_state {
+ DirectoryState::List {
+ entries,
+ error: None,
+ ..
+ }
+ | DirectoryState::Create { entries, .. } => entries.clone(),
+ DirectoryState::List { error: Some(_), .. } | DirectoryState::None { .. } => {
+ Vec::new()
}
-
- Some(directory_state.match_candidates.clone())
})
- .unwrap_or(None);
-
- let Some(mut match_candidates) = match_candidates else {
+ else {
return;
};
if !suffix.starts_with('.') {
- match_candidates.retain(|m| !m.path.string.starts_with('.'));
+ new_entries.retain(|entry| !entry.path.string.starts_with('.'));
}
-
- if suffix == "" {
+ if suffix.is_empty() {
this.update(cx, |this, cx| {
- this.delegate.matches.clear();
- this.delegate.string_matches.clear();
- this.delegate
- .matches
- .extend(match_candidates.iter().map(|m| m.path.id));
-
+ this.delegate.selected_index = 0;
+ this.delegate.string_matches = new_entries
+ .iter()
+ .map(|m| StringMatch {
+ candidate_id: m.path.id,
+ score: 0.0,
+ positions: Vec::new(),
+ string: m.path.string.clone(),
+ })
+ .collect();
+ this.delegate.directory_state =
+ match &this.delegate.directory_state {
+ DirectoryState::None { create: false }
+ | DirectoryState::List { .. } => DirectoryState::List {
+ parent_path: dir.clone(),
+ entries: new_entries,
+ error: None,
+ },
+ DirectoryState::None { create: true }
+ | DirectoryState::Create { .. } => DirectoryState::Create {
+ parent_path: dir.clone(),
+ user_input: None,
+ entries: new_entries,
+ },
+ };
cx.notify();
})
.ok();
return;
}
- let candidates = match_candidates.iter().map(|m| &m.path).collect::<Vec<_>>();
+ let Ok(is_create_state) =
+ this.update(cx, |this, _| match &this.delegate.directory_state {
+ DirectoryState::Create { .. } => true,
+ DirectoryState::List { .. } => false,
+ DirectoryState::None { create } => *create,
+ })
+ else {
+ return;
+ };
+
+ let candidates = new_entries
+ .iter()
+ .filter_map(|entry| {
+ if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
+ {
+ None
+ } else {
+ Some(&entry.path)
+ }
+ })
+ .collect::<Vec<_>>();
+
let matches = fuzzy::match_strings(
candidates.as_slice(),
&suffix,
@@ -257,27 +423,57 @@ impl PickerDelegate for OpenPathDelegate {
cx.background_executor().clone(),
)
.await;
- if cancel_flag.load(atomic::Ordering::Relaxed) {
+ if cancel_flag.load(atomic::Ordering::Acquire) {
return;
}
this.update(cx, |this, cx| {
- this.delegate.matches.clear();
+ this.delegate.selected_index = 0;
this.delegate.string_matches = matches.clone();
- this.delegate
- .matches
- .extend(matches.into_iter().map(|m| m.candidate_id));
- this.delegate.matches.sort_by_key(|m| {
+ this.delegate.string_matches.sort_by_key(|m| {
(
- this.delegate.directory_state.as_ref().and_then(|d| {
- d.match_candidates
- .get(*m)
- .map(|c| !c.path.string.starts_with(&suffix))
- }),
- *m,
+ new_entries
+ .iter()
+ .find(|entry| entry.path.id == m.candidate_id)
+ .map(|entry| &entry.path)
+ .map(|candidate| !candidate.string.starts_with(&suffix)),
+ m.candidate_id,
)
});
- this.delegate.selected_index = 0;
+ this.delegate.directory_state = match &this.delegate.directory_state {
+ DirectoryState::None { create: false } | DirectoryState::List { .. } => {
+ DirectoryState::List {
+ entries: new_entries,
+ parent_path: dir.clone(),
+ error: None,
+ }
+ }
+ DirectoryState::None { create: true } => DirectoryState::Create {
+ entries: new_entries,
+ parent_path: dir.clone(),
+ user_input: Some(UserInput {
+ file: StringMatchCandidate::new(0, &suffix),
+ exists: false,
+ is_dir: false,
+ }),
+ },
+ DirectoryState::Create { user_input, .. } => {
+ let (new_id, exists, is_dir) = user_input
+ .as_ref()
+ .map(|input| (input.file.id, input.exists, input.is_dir))
+ .unwrap_or_else(|| (0, false, false));
+ DirectoryState::Create {
+ entries: new_entries,
+ parent_path: dir.clone(),
+ user_input: Some(UserInput {
+ file: StringMatchCandidate::new(new_id, &suffix),
+ exists,
+ is_dir,
+ }),
+ }
+ }
+ };
+
cx.notify();
})
.ok();
@@ -290,49 +486,107 @@ impl PickerDelegate for OpenPathDelegate {
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<String> {
+ let candidate = self.get_entry(self.selected_index)?;
Some(
maybe!({
- 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.path.string,
- if candidate.is_dir {
- MAIN_SEPARATOR_STR
- } else {
- ""
- }
- ))
+ match &self.directory_state {
+ DirectoryState::Create { parent_path, .. } => Some(format!(
+ "{}{}{}",
+ parent_path,
+ candidate.path.string,
+ if candidate.is_dir {
+ MAIN_SEPARATOR_STR
+ } else {
+ ""
+ }
+ )),
+ DirectoryState::List { parent_path, .. } => Some(format!(
+ "{}{}{}",
+ parent_path,
+ candidate.path.string,
+ if candidate.is_dir {
+ MAIN_SEPARATOR_STR
+ } else {
+ ""
+ }
+ )),
+ DirectoryState::None { .. } => return None,
+ }
})
.unwrap_or(query),
)
}
- fn confirm(&mut self, _: bool, _: &mut Window, cx: &mut Context<Picker<Self>>) {
- let Some(m) = self.matches.get(self.selected_index) else {
+ fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ let Some(candidate) = self.get_entry(self.selected_index) else {
return;
};
- let Some(directory_state) = self.directory_state.as_ref() else {
- return;
- };
- let Some(candidate) = directory_state.match_candidates.get(*m) else {
- return;
- };
- let result = if directory_state.path == "/" && candidate.path.string.is_empty() {
- PathBuf::from("/")
- } else {
- Path::new(
- self.lister
- .resolve_tilde(&directory_state.path, cx)
- .as_ref(),
- )
- .join(&candidate.path.string)
- };
- if let Some(tx) = self.tx.take() {
- tx.send(Some(vec![result])).ok();
+
+ match &self.directory_state {
+ DirectoryState::None { .. } => return,
+ DirectoryState::List { parent_path, .. } => {
+ let confirmed_path =
+ if parent_path == PROMPT_ROOT && candidate.path.string.is_empty() {
+ PathBuf::from(PROMPT_ROOT)
+ } else {
+ Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
+ .join(&candidate.path.string)
+ };
+ if let Some(tx) = self.tx.take() {
+ tx.send(Some(vec![confirmed_path])).ok();
+ }
+ }
+ DirectoryState::Create {
+ parent_path,
+ user_input,
+ ..
+ } => match user_input {
+ None => return,
+ Some(user_input) => {
+ if user_input.is_dir {
+ return;
+ }
+ let prompted_path =
+ if parent_path == PROMPT_ROOT && user_input.file.string.is_empty() {
+ PathBuf::from(PROMPT_ROOT)
+ } else {
+ Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
+ .join(&user_input.file.string)
+ };
+ if user_input.exists {
+ self.should_dismiss = false;
+ let answer = window.prompt(
+ gpui::PromptLevel::Critical,
+ &format!("{prompted_path:?} already exists. Do you want to replace it?"),
+ Some(
+ "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
+ ),
+ &["Replace", "Cancel"],
+ cx
+ );
+ self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
+ let answer = answer.await.ok();
+ picker
+ .update(cx, |picker, cx| {
+ picker.delegate.should_dismiss = true;
+ if answer != Some(0) {
+ return;
+ }
+ if let Some(tx) = picker.delegate.tx.take() {
+ tx.send(Some(vec![prompted_path])).ok();
+ }
+ cx.emit(gpui::DismissEvent);
+ })
+ .ok();
+ });
+ return;
+ } else if let Some(tx) = self.tx.take() {
+ tx.send(Some(vec![prompted_path])).ok();
+ }
+ }
+ },
}
+
cx.emit(gpui::DismissEvent);
}
@@ -351,19 +605,30 @@ impl PickerDelegate for OpenPathDelegate {
&self,
ix: usize,
selected: bool,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let settings = FileFinderSettings::get_global(cx);
- let m = self.matches.get(ix)?;
- let directory_state = self.directory_state.as_ref()?;
- let candidate = directory_state.match_candidates.get(*m)?;
- let highlight_positions = self
- .string_matches
- .iter()
- .find(|string_match| string_match.candidate_id == *m)
- .map(|string_match| string_match.positions.clone())
- .unwrap_or_default();
+ let candidate = self.get_entry(ix)?;
+ let match_positions = match &self.directory_state {
+ DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
+ DirectoryState::Create { user_input, .. } => {
+ if let Some(user_input) = user_input {
+ if !user_input.exists || !user_input.is_dir {
+ if ix == 0 {
+ Vec::new()
+ } else {
+ self.string_matches.get(ix - 1)?.positions.clone()
+ }
+ } else {
+ self.string_matches.get(ix)?.positions.clone()
+ }
+ } else {
+ self.string_matches.get(ix)?.positions.clone()
+ }
+ }
+ DirectoryState::None { .. } => Vec::new(),
+ };
let file_icon = maybe!({
if !settings.file_icons {
@@ -378,34 +643,128 @@ impl PickerDelegate for OpenPathDelegate {
Some(Icon::from_path(icon).color(Color::Muted))
});
- Some(
- ListItem::new(ix)
- .spacing(ListItemSpacing::Sparse)
- .start_slot::<Icon>(file_icon)
- .inset(true)
- .toggle_state(selected)
- .child(HighlightedLabel::new(
- if directory_state.path == "/" {
- format!("/{}", candidate.path.string)
- } else {
- candidate.path.string.clone()
- },
- highlight_positions,
- )),
- )
+ match &self.directory_state {
+ DirectoryState::List { parent_path, .. } => Some(
+ ListItem::new(ix)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot::<Icon>(file_icon)
+ .inset(true)
+ .toggle_state(selected)
+ .child(HighlightedLabel::new(
+ if parent_path == PROMPT_ROOT {
+ format!("{}{}", PROMPT_ROOT, candidate.path.string)
+ } else {
+ candidate.path.string.clone()
+ },
+ match_positions,
+ )),
+ ),
+ DirectoryState::Create {
+ parent_path,
+ user_input,
+ ..
+ } => {
+ let (label, delta) = if parent_path == PROMPT_ROOT {
+ (
+ format!("{}{}", PROMPT_ROOT, candidate.path.string),
+ PROMPT_ROOT.len(),
+ )
+ } else {
+ (candidate.path.string.clone(), 0)
+ };
+ let label_len = label.len();
+
+ let label_with_highlights = match user_input {
+ Some(user_input) => {
+ if user_input.file.string == candidate.path.string {
+ if user_input.exists {
+ let label = if user_input.is_dir {
+ label
+ } else {
+ format!("{label} (replace)")
+ };
+ StyledText::new(label)
+ .with_default_highlights(
+ &window.text_style().clone(),
+ vec![(
+ delta..delta + label_len,
+ HighlightStyle::color(Color::Conflict.color(cx)),
+ )],
+ )
+ .into_any_element()
+ } else {
+ StyledText::new(format!("{label} (create)"))
+ .with_default_highlights(
+ &window.text_style().clone(),
+ vec![(
+ delta..delta + label_len,
+ HighlightStyle::color(Color::Created.color(cx)),
+ )],
+ )
+ .into_any_element()
+ }
+ } else {
+ let mut highlight_positions = match_positions;
+ highlight_positions.iter_mut().for_each(|position| {
+ *position += delta;
+ });
+ HighlightedLabel::new(label, highlight_positions).into_any_element()
+ }
+ }
+ None => {
+ let mut highlight_positions = match_positions;
+ highlight_positions.iter_mut().for_each(|position| {
+ *position += delta;
+ });
+ HighlightedLabel::new(label, highlight_positions).into_any_element()
+ }
+ };
+
+ Some(
+ ListItem::new(ix)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot::<Icon>(file_icon)
+ .inset(true)
+ .toggle_state(selected)
+ .child(LabelLike::new().child(label_with_highlights)),
+ )
+ }
+ DirectoryState::None { .. } => return None,
+ }
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
- let text = if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone())
- {
- error
- } else {
- "No such file or directory".into()
- };
- Some(text)
+ Some(match &self.directory_state {
+ DirectoryState::Create { .. } => SharedString::from("Type a pathβ¦"),
+ DirectoryState::List {
+ error: Some(error), ..
+ } => error.clone(),
+ DirectoryState::List { .. } | DirectoryState::None { .. } => {
+ SharedString::from("No such file or directory")
+ }
+ })
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
}
}
+
+fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Vec<CandidateInfo> {
+ if *parent_path == PROMPT_ROOT {
+ children.push(DirectoryItem {
+ is_dir: true,
+ path: PathBuf::default(),
+ });
+ }
+
+ children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
+ children
+ .iter()
+ .enumerate()
+ .map(|(ix, item)| CandidateInfo {
+ path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
+ is_dir: item.is_dir,
+ })
+ .collect()
+}
@@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, cx);
+ let (picker, cx) = build_open_path_prompt(project, false, cx);
let query = path!("/root");
insert_query(query, &picker, cx).await;
@@ -111,7 +111,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, cx);
+ let (picker, cx) = build_open_path_prompt(project, false, cx);
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
let query = path!("/root");
@@ -204,7 +204,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, cx);
+ let (picker, cx) = build_open_path_prompt(project, false, cx);
// Support both forward and backward slashes.
let query = "C:/root/";
@@ -251,6 +251,54 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
);
}
+#[gpui::test]
+async fn test_new_path_prompt(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/root"),
+ json!({
+ "a1": "A1",
+ "a2": "A2",
+ "a3": "A3",
+ "dir1": {},
+ "dir2": {
+ "c": "C",
+ "d1": "D1",
+ "d2": "D2",
+ "d3": "D3",
+ "dir3": {},
+ "dir4": {}
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
+
+ let (picker, cx) = build_open_path_prompt(project, true, cx);
+
+ insert_query(path!("/root"), &picker, cx).await;
+ assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
+
+ insert_query(path!("/root/d"), &picker, cx).await;
+ assert_eq!(
+ collect_match_candidates(&picker, cx),
+ vec!["d", "dir1", "dir2"]
+ );
+
+ insert_query(path!("/root/dir1"), &picker, cx).await;
+ assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1"]);
+
+ insert_query(path!("/root/dir12"), &picker, cx).await;
+ assert_eq!(collect_match_candidates(&picker, cx), vec!["dir12"]);
+
+ insert_query(path!("/root/dir1"), &picker, cx).await;
+ assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1"]);
+}
+
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
@@ -266,11 +314,12 @@ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
fn build_open_path_prompt(
project: Entity<Project>,
+ creating_path: bool,
cx: &mut TestAppContext,
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
let (tx, _) = futures::channel::oneshot::channel();
let lister = project::DirectoryLister::Project(project.clone());
- let delegate = OpenPathDelegate::new(tx, lister.clone());
+ let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
(
@@ -770,13 +770,26 @@ pub struct DirectoryItem {
#[derive(Clone)]
pub enum DirectoryLister {
Project(Entity<Project>),
- Local(Arc<dyn Fs>),
+ Local(Entity<Project>, Arc<dyn Fs>),
+}
+
+impl std::fmt::Debug for DirectoryLister {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ DirectoryLister::Project(project) => {
+ write!(f, "DirectoryLister::Project({project:?})")
+ }
+ DirectoryLister::Local(project, _) => {
+ write!(f, "DirectoryLister::Local({project:?})")
+ }
+ }
+ }
}
impl DirectoryLister {
pub fn is_local(&self, cx: &App) -> bool {
match self {
- DirectoryLister::Local(_) => true,
+ DirectoryLister::Local(..) => true,
DirectoryLister::Project(project) => project.read(cx).is_local(),
}
}
@@ -790,12 +803,28 @@ impl DirectoryLister {
}
pub fn default_query(&self, cx: &mut App) -> String {
- if let DirectoryLister::Project(project) = self {
- if let Some(worktree) = project.read(cx).visible_worktrees(cx).next() {
- return worktree.read(cx).abs_path().to_string_lossy().to_string();
+ let separator = std::path::MAIN_SEPARATOR_STR;
+ match self {
+ DirectoryLister::Project(project) => project,
+ DirectoryLister::Local(project, _) => project,
+ }
+ .read(cx)
+ .visible_worktrees(cx)
+ .next()
+ .map(|worktree| worktree.read(cx).abs_path())
+ .map(|dir| dir.to_string_lossy().to_string())
+ .or_else(|| std::env::home_dir().map(|dir| dir.to_string_lossy().to_string()))
+ .map(|mut s| {
+ s.push_str(separator);
+ s
+ })
+ .unwrap_or_else(|| {
+ if cfg!(target_os = "windows") {
+ format!("C:{separator}")
+ } else {
+ format!("~{separator}")
}
- };
- format!("~{}", std::path::MAIN_SEPARATOR_STR)
+ })
}
pub fn list_directory(&self, path: String, cx: &mut App) -> Task<Result<Vec<DirectoryItem>>> {
@@ -803,7 +832,7 @@ impl DirectoryLister {
DirectoryLister::Project(project) => {
project.update(cx, |project, cx| project.list_directory(path, cx))
}
- DirectoryLister::Local(fs) => {
+ DirectoryLister::Local(_, fs) => {
let fs = fs.clone();
cx.background_spawn(async move {
let mut results = vec![];
@@ -4049,7 +4078,7 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<Vec<DirectoryItem>>> {
if self.is_local() {
- DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
+ DirectoryLister::Local(cx.entity(), 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 {
@@ -147,7 +147,7 @@ impl ProjectPicker {
) -> Entity<Self> {
let (tx, rx) = oneshot::channel();
let lister = project::DirectoryLister::Project(project.clone());
- let delegate = file_finder::OpenPathDelegate::new(tx, lister);
+ let delegate = file_finder::OpenPathDelegate::new(tx, lister, false);
let picker = cx.new(|cx| {
let picker = Picker::uniform_list(delegate, window, cx)
@@ -25,7 +25,7 @@ use gpui::{
use itertools::Itertools;
use language::DiagnosticSeverity;
use parking_lot::Mutex;
-use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
+use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, WorktreeId};
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{Settings, SettingsStore};
@@ -1921,24 +1921,56 @@ impl Pane {
})?
.await?;
} else if can_save_as && is_singleton {
- let abs_path = pane.update_in(cx, |pane, window, cx| {
+ let new_path = pane.update_in(cx, |pane, window, cx| {
pane.activate_item(item_ix, true, true, window, cx);
pane.workspace.update(cx, |workspace, cx| {
- workspace.prompt_for_new_path(window, cx)
+ let lister = if workspace.project().read(cx).is_local() {
+ DirectoryLister::Local(
+ workspace.project().clone(),
+ workspace.app_state().fs.clone(),
+ )
+ } else {
+ DirectoryLister::Project(workspace.project().clone())
+ };
+ workspace.prompt_for_new_path(lister, window, cx)
})
})??;
- if let Some(abs_path) = abs_path.await.ok().flatten() {
+ let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next()
+ else {
+ return Ok(false);
+ };
+
+ let project_path = pane
+ .update(cx, |pane, cx| {
+ pane.project
+ .update(cx, |project, cx| {
+ project.find_or_create_worktree(new_path, true, cx)
+ })
+ .ok()
+ })
+ .ok()
+ .flatten();
+ let save_task = if let Some(project_path) = project_path {
+ let (worktree, path) = project_path.await?;
+ let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
+ let new_path = ProjectPath {
+ worktree_id,
+ path: path.into(),
+ };
+
pane.update_in(cx, |pane, window, cx| {
- if let Some(item) = pane.item_for_path(abs_path.clone(), cx) {
+ if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
pane.remove_item(item.item_id(), false, false, window, cx);
}
- item.save_as(project, abs_path, window, cx)
+ item.save_as(project, new_path, window, cx)
})?
- .await?;
} else {
return Ok(false);
- }
+ };
+
+ save_task.await?;
+ return Ok(true);
}
}
@@ -899,9 +899,10 @@ pub enum OpenVisible {
type PromptForNewPath = Box<
dyn Fn(
&mut Workspace,
+ DirectoryLister,
&mut Window,
&mut Context<Workspace>,
- ) -> oneshot::Receiver<Option<ProjectPath>>,
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
>;
type PromptForOpenPath = Box<
@@ -1874,25 +1875,25 @@ impl Workspace {
let (tx, rx) = oneshot::channel();
let abs_path = cx.prompt_for_paths(path_prompt_options);
- cx.spawn_in(window, async move |this, cx| {
+ cx.spawn_in(window, async move |workspace, cx| {
let Ok(result) = abs_path.await else {
return Ok(());
};
match result {
Ok(result) => {
- tx.send(result).log_err();
+ tx.send(result).ok();
}
Err(err) => {
- let rx = this.update_in(cx, |this, window, cx| {
- this.show_portal_error(err.to_string(), cx);
- let prompt = this.on_prompt_for_open_path.take().unwrap();
- let rx = prompt(this, lister, window, cx);
- this.on_prompt_for_open_path = Some(prompt);
+ let rx = workspace.update_in(cx, |workspace, window, cx| {
+ workspace.show_portal_error(err.to_string(), cx);
+ let prompt = workspace.on_prompt_for_open_path.take().unwrap();
+ let rx = prompt(workspace, lister, window, cx);
+ workspace.on_prompt_for_open_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
- tx.send(path).log_err();
+ tx.send(path).ok();
}
}
};
@@ -1906,77 +1907,58 @@ impl Workspace {
pub fn prompt_for_new_path(
&mut self,
+ lister: DirectoryLister,
window: &mut Window,
cx: &mut Context<Self>,
- ) -> oneshot::Receiver<Option<ProjectPath>> {
- if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+ if self.project.read(cx).is_via_collab()
+ || self.project.read(cx).is_via_ssh()
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
{
let prompt = self.on_prompt_for_new_path.take().unwrap();
- let rx = prompt(self, window, cx);
+ let rx = prompt(self, lister, window, cx);
self.on_prompt_for_new_path = Some(prompt);
return rx;
}
let (tx, rx) = oneshot::channel();
- cx.spawn_in(window, async move |this, cx| {
- let abs_path = this.update(cx, |this, cx| {
- let mut relative_to = this
+ cx.spawn_in(window, async move |workspace, cx| {
+ let abs_path = workspace.update(cx, |workspace, cx| {
+ let relative_to = workspace
.most_recent_active_path(cx)
- .and_then(|p| p.parent().map(|p| p.to_path_buf()));
- if relative_to.is_none() {
- let project = this.project.read(cx);
- relative_to = project
- .visible_worktrees(cx)
- .filter_map(|worktree| {
+ .and_then(|p| p.parent().map(|p| p.to_path_buf()))
+ .or_else(|| {
+ let project = workspace.project.read(cx);
+ project.visible_worktrees(cx).find_map(|worktree| {
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
})
- .next()
- };
-
- cx.prompt_for_new_path(&relative_to.unwrap_or_else(|| PathBuf::from("")))
+ })
+ .or_else(std::env::home_dir)
+ .unwrap_or_else(|| PathBuf::from(""));
+ cx.prompt_for_new_path(&relative_to)
})?;
let abs_path = match abs_path.await? {
Ok(path) => path,
Err(err) => {
- let rx = this.update_in(cx, |this, window, cx| {
- this.show_portal_error(err.to_string(), cx);
+ let rx = workspace.update_in(cx, |workspace, window, cx| {
+ workspace.show_portal_error(err.to_string(), cx);
- let prompt = this.on_prompt_for_new_path.take().unwrap();
- let rx = prompt(this, window, cx);
- this.on_prompt_for_new_path = Some(prompt);
+ let prompt = workspace.on_prompt_for_new_path.take().unwrap();
+ let rx = prompt(workspace, lister, window, cx);
+ workspace.on_prompt_for_new_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
- tx.send(path).log_err();
+ tx.send(path).ok();
}
return anyhow::Ok(());
}
};
- let project_path = abs_path.and_then(|abs_path| {
- this.update(cx, |this, cx| {
- this.project.update(cx, |project, cx| {
- project.find_or_create_worktree(abs_path, true, cx)
- })
- })
- .ok()
- });
-
- if let Some(project_path) = project_path {
- let (worktree, path) = project_path.await?;
- let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
- tx.send(Some(ProjectPath {
- worktree_id,
- path: path.into(),
- }))
- .ok();
- } else {
- tx.send(None).ok();
- }
+ tx.send(abs_path.map(|path| vec![path])).ok();
anyhow::Ok(())
})
- .detach_and_log_err(cx);
+ .detach();
rx
}
@@ -503,7 +503,10 @@ fn register_actions(
directories: true,
multiple: true,
},
- DirectoryLister::Local(workspace.app_state().fs.clone()),
+ DirectoryLister::Local(
+ workspace.project().clone(),
+ workspace.app_state().fs.clone(),
+ ),
window,
cx,
);