pub mod file_finder_settings; #[cfg(test)]mod open_path_prompt_tests; use file_finder_settings::FileFinderSettings;use file_icons::FileIcons;use futures::channel::oneshot;use fuzzy::{CharBag, StringMatch, StringMatchCandidate};use gpui::{HighlightStyle, StyledText, Task};use picker::{Picker, PickerDelegate};use project::{DirectoryItem, DirectoryLister};use project_panel::project_panel_settings::ProjectPanelSettings;use settings::{ProjectPanelSortMode, Settings};use std::{ path::{self, Path, PathBuf}, sync::{ Arc, atomic::{self, AtomicBool}, },};use ui::{Context, LabelLike, ListItem, Window};use ui::{HighlightedLabel, ListItemSpacing, prelude::*};use util::{ maybe, paths::{PathStyle, compare_paths},};use workspace::Workspace; pub struct OpenPathPrompt; pub struct OpenPathDelegate { tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>, lister: DirectoryLister, selected_index: usize, directory_state: DirectoryState, string_matches: Vec<StringMatch>, cancel_flag: Arc<AtomicBool>, should_dismiss: bool, prompt_root: String, path_style: PathStyle, replace_prompt: Task<()>, render_footer: Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>, hidden_entries: bool, sort_mode: ProjectPanelSortMode,} impl OpenPathDelegate { pub fn new( tx: oneshot::Sender<Option<Vec<PathBuf>>>, lister: DirectoryLister, creating_path: bool, cx: &App, ) -> Self { let path_style = lister.path_style(cx); let sort_mode = ProjectPanelSettings::try_get(cx) .map(|s| s.sort_mode) .unwrap_or_default(); Self { tx: Some(tx), lister, selected_index: 0, directory_state: DirectoryState::None { create: creating_path, }, string_matches: Vec::new(), cancel_flag: Arc::new(AtomicBool::new(false)), should_dismiss: true, prompt_root: match path_style { PathStyle::Unix => "/".to_string(), PathStyle::Windows => "C:\\".to_string(), }, path_style, replace_prompt: Task::ready(()), render_footer: Arc::new(|_, _| None), hidden_entries: false, sort_mode, } } pub fn with_footer( mut self, footer: Arc< dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static, >, ) -> Self { self.render_footer = footer; self } pub fn show_hidden(mut self) -> Self { self.hidden_entries = true; self } 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 && (!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> { match &self.directory_state { DirectoryState::List { entries, .. } => 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::Create { user_input, entries, .. } => user_input .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(), } } fn current_dir(&self) -> &'static str { match self.path_style { PathStyle::Unix => "./", PathStyle::Windows => ".\\", } }} #[derive(Debug)]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)]struct CandidateInfo { path: StringMatchCandidate, is_dir: bool,} impl OpenPathPrompt { pub fn register( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { 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, false, None, tx, window, cx); rx })); } pub fn register_new_path( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { workspace.set_prompt_for_new_path(Box::new( |workspace, lister, suggested_name, window, cx| { let (tx, rx) = futures::channel::oneshot::channel(); Self::prompt_for_new_path(workspace, lister, suggested_name, tx, window, cx); rx }, )); } fn prompt_for_open_path( workspace: &mut Workspace, lister: DirectoryLister, creating_path: bool, suggested_name: Option<String>, 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(), creating_path, cx).show_hidden(); let picker = Picker::uniform_list(delegate, window, cx); let mut query = lister.default_query(cx); if let Some(suggested_name) = suggested_name { query.push_str(&suggested_name); } picker.set_query(&query, window, cx); picker }); } fn prompt_for_new_path( workspace: &mut Workspace, lister: DirectoryLister, suggested_name: Option<String>, tx: oneshot::Sender<Option<Vec<PathBuf>>>, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::prompt_for_open_path(workspace, lister, true, suggested_name, tx, window, cx); }} impl PickerDelegate for OpenPathDelegate { type ListItem = ui::ListItem; fn name() -> &'static str { "open path prompt" } fn match_count(&self) -> usize { 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 { self.selected_index } fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) { self.selected_index = ix; cx.notify(); } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Task<()> { let lister = &self.lister; let input_is_empty = query.is_empty(); let (dir, suffix) = get_dir_and_suffix(query, self.path_style); 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::Release); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); let hidden_entries = self.hidden_entries; let parent_path_is_root = self.prompt_root == dir; let current_dir = self.current_dir(); let sort_mode = self.sort_mode; cx.spawn_in(window, async move |this, cx| { if let Some(query) = query { let paths = query.await; if cancel_flag.load(atomic::Ordering::Acquire) { return; } 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(parent_path_is_root, paths, sort_mode), 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(parent_path_is_root, paths, sort_mode); 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 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() } }) else { return; }; if !hidden_entries { new_entries.retain(|entry| !entry.path.string.starts_with('.')); } let max_id = new_entries .iter() .map(|entry| entry.path.id) .max() .unwrap_or(0); if suffix.is_empty() { let should_prepend_with_current_dir = this .read_with(cx, |picker, _| { !input_is_empty && match &picker.delegate.directory_state { DirectoryState::List { error, .. } => error.is_none(), DirectoryState::Create { .. } => false, DirectoryState::None { .. } => false, } }) .unwrap_or(false); let current_dir_in_new_entries = new_entries .iter() .any(|entry| &entry.path.string == current_dir); if should_prepend_with_current_dir && !current_dir_in_new_entries { new_entries.insert( 0, CandidateInfo { path: StringMatchCandidate { id: max_id + 1, string: current_dir.to_string(), char_bag: CharBag::from(current_dir), }, is_dir: true, }, ); } this.update(cx, |this, cx| { 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 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_entriesLines 1–500 of 1105. Load more
pub mod file_finder_settings; #[cfg(test)] mod open_path_prompt_tests; use file_finder_settings::FileFinderSettings; use file_icons::FileIcons; use futures::channel::oneshot; use fuzzy::{CharBag, StringMatch, StringMatchCandidate}; use gpui::{HighlightStyle, StyledText, Task}; use picker::{Picker, PickerDelegate}; use project::{DirectoryItem, DirectoryLister}; use project_panel::project_panel_settings::ProjectPanelSettings; use settings::{ProjectPanelSortMode, Settings}; use std::{ path::{self, Path, PathBuf}, sync::{ Arc, atomic::{self, AtomicBool}, }, }; use ui::{Context, LabelLike, ListItem, Window}; use ui::{HighlightedLabel, ListItemSpacing, prelude::*}; use util::{ maybe, paths::{PathStyle, compare_paths}, }; use workspace::Workspace; pub struct OpenPathPrompt; pub struct OpenPathDelegate { tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>, lister: DirectoryLister, selected_index: usize, directory_state: DirectoryState, string_matches: Vec<StringMatch>, cancel_flag: Arc<AtomicBool>, should_dismiss: bool, prompt_root: String, path_style: PathStyle, replace_prompt: Task<()>, render_footer: Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>, hidden_entries: bool, sort_mode: ProjectPanelSortMode, } impl OpenPathDelegate { pub fn new( tx: oneshot::Sender<Option<Vec<PathBuf>>>, lister: DirectoryLister, creating_path: bool, cx: &App, ) -> Self { let path_style = lister.path_style(cx); let sort_mode = ProjectPanelSettings::try_get(cx) .map(|s| s.sort_mode) .unwrap_or_default(); Self { tx: Some(tx), lister, selected_index: 0, directory_state: DirectoryState::None { create: creating_path, }, string_matches: Vec::new(), cancel_flag: Arc::new(AtomicBool::new(false)), should_dismiss: true, prompt_root: match path_style { PathStyle::Unix => "/".to_string(), PathStyle::Windows => "C:\\".to_string(), }, path_style, replace_prompt: Task::ready(()), render_footer: Arc::new(|_, _| None), hidden_entries: false, sort_mode, } } pub fn with_footer( mut self, footer: Arc< dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static, >, ) -> Self { self.render_footer = footer; self } pub fn show_hidden(mut self) -> Self { self.hidden_entries = true; self } 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 && (!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> { match &self.directory_state { DirectoryState::List { entries, .. } => 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::Create { user_input, entries, .. } => user_input .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(), } } fn current_dir(&self) -> &'static str { match self.path_style { PathStyle::Unix => "./", PathStyle::Windows => ".\\", } } } #[derive(Debug)] 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)] struct CandidateInfo { path: StringMatchCandidate, is_dir: bool, } impl OpenPathPrompt { pub fn register( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { 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, false, None, tx, window, cx); rx })); } pub fn register_new_path( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { workspace.set_prompt_for_new_path(Box::new( |workspace, lister, suggested_name, window, cx| { let (tx, rx) = futures::channel::oneshot::channel(); Self::prompt_for_new_path(workspace, lister, suggested_name, tx, window, cx); rx }, )); } fn prompt_for_open_path( workspace: &mut Workspace, lister: DirectoryLister, creating_path: bool, suggested_name: Option<String>, 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(), creating_path, cx).show_hidden(); let picker = Picker::uniform_list(delegate, window, cx); let mut query = lister.default_query(cx); if let Some(suggested_name) = suggested_name { query.push_str(&suggested_name); } picker.set_query(&query, window, cx); picker }); } fn prompt_for_new_path( workspace: &mut Workspace, lister: DirectoryLister, suggested_name: Option<String>, tx: oneshot::Sender<Option<Vec<PathBuf>>>, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::prompt_for_open_path(workspace, lister, true, suggested_name, tx, window, cx); } } impl PickerDelegate for OpenPathDelegate { type ListItem = ui::ListItem; fn name() -> &'static str { "open path prompt" } fn match_count(&self) -> usize { 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 { self.selected_index } fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) { self.selected_index = ix; cx.notify(); } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Task<()> { let lister = &self.lister; let input_is_empty = query.is_empty(); let (dir, suffix) = get_dir_and_suffix(query, self.path_style); 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::Release); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); let hidden_entries = self.hidden_entries; let parent_path_is_root = self.prompt_root == dir; let current_dir = self.current_dir(); let sort_mode = self.sort_mode; cx.spawn_in(window, async move |this, cx| { if let Some(query) = query { let paths = query.await; if cancel_flag.load(atomic::Ordering::Acquire) { return; } 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(parent_path_is_root, paths, sort_mode), 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(parent_path_is_root, paths, sort_mode); 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 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() } }) else { return; }; if !hidden_entries { new_entries.retain(|entry| !entry.path.string.starts_with('.')); } let max_id = new_entries .iter() .map(|entry| entry.path.id) .max() .unwrap_or(0); if suffix.is_empty() { let should_prepend_with_current_dir = this .read_with(cx, |picker, _| { !input_is_empty && match &picker.delegate.directory_state { DirectoryState::List { error, .. } => error.is_none(), DirectoryState::Create { .. } => false, DirectoryState::None { .. } => false, } }) .unwrap_or(false); let current_dir_in_new_entries = new_entries .iter() .any(|entry| &entry.path.string == current_dir); if should_prepend_with_current_dir && !current_dir_in_new_entries { new_entries.insert( 0, CandidateInfo { path: StringMatchCandidate { id: max_id + 1, string: current_dir.to_string(), char_bag: CharBag::from(current_dir), }, is_dir: true, }, ); } this.update(cx, |this, cx| { 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 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