1105 lines · 40.5 KB · Rust
RawDownload
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

Lines 1–500 of 1105.