open_path_prompt.rs

   1pub mod file_finder_settings;
   2
   3#[cfg(test)]
   4mod open_path_prompt_tests;
   5
   6use file_finder_settings::FileFinderSettings;
   7use file_icons::FileIcons;
   8use futures::channel::oneshot;
   9use fuzzy::{CharBag, StringMatch, StringMatchCandidate};
  10use gpui::{HighlightStyle, StyledText, Task};
  11use picker::{Picker, PickerDelegate};
  12use project::{DirectoryItem, DirectoryLister};
  13use settings::Settings;
  14use std::{
  15    path::{self, Path, PathBuf},
  16    sync::{
  17        Arc,
  18        atomic::{self, AtomicBool},
  19    },
  20};
  21use ui::{Context, LabelLike, ListItem, Window};
  22use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
  23use util::{
  24    maybe,
  25    paths::{PathStyle, compare_paths},
  26};
  27use workspace::Workspace;
  28
  29pub struct OpenPathPrompt;
  30
  31pub struct OpenPathDelegate {
  32    tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
  33    lister: DirectoryLister,
  34    selected_index: usize,
  35    directory_state: DirectoryState,
  36    string_matches: Vec<StringMatch>,
  37    cancel_flag: Arc<AtomicBool>,
  38    should_dismiss: bool,
  39    prompt_root: String,
  40    path_style: PathStyle,
  41    replace_prompt: Task<()>,
  42    render_footer:
  43        Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>,
  44    hidden_entries: bool,
  45}
  46
  47impl OpenPathDelegate {
  48    pub fn new(
  49        tx: oneshot::Sender<Option<Vec<PathBuf>>>,
  50        lister: DirectoryLister,
  51        creating_path: bool,
  52        cx: &App,
  53    ) -> Self {
  54        let path_style = lister.path_style(cx);
  55        Self {
  56            tx: Some(tx),
  57            lister,
  58            selected_index: 0,
  59            directory_state: DirectoryState::None {
  60                create: creating_path,
  61            },
  62            string_matches: Vec::new(),
  63            cancel_flag: Arc::new(AtomicBool::new(false)),
  64            should_dismiss: true,
  65            prompt_root: match path_style {
  66                PathStyle::Posix => "/".to_string(),
  67                PathStyle::Windows => "C:\\".to_string(),
  68            },
  69            path_style,
  70            replace_prompt: Task::ready(()),
  71            render_footer: Arc::new(|_, _| None),
  72            hidden_entries: false,
  73        }
  74    }
  75
  76    pub fn with_footer(
  77        mut self,
  78        footer: Arc<
  79            dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static,
  80        >,
  81    ) -> Self {
  82        self.render_footer = footer;
  83        self
  84    }
  85
  86    pub fn show_hidden(mut self) -> Self {
  87        self.hidden_entries = true;
  88        self
  89    }
  90    fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
  91        match &self.directory_state {
  92            DirectoryState::List { entries, .. } => {
  93                let id = self.string_matches.get(selected_match_index)?.candidate_id;
  94                entries.iter().find(|entry| entry.path.id == id).cloned()
  95            }
  96            DirectoryState::Create {
  97                user_input,
  98                entries,
  99                ..
 100            } => {
 101                let mut i = selected_match_index;
 102                if let Some(user_input) = user_input
 103                    && (!user_input.exists || !user_input.is_dir)
 104                {
 105                    if i == 0 {
 106                        return Some(CandidateInfo {
 107                            path: user_input.file.clone(),
 108                            is_dir: false,
 109                        });
 110                    } else {
 111                        i -= 1;
 112                    }
 113                }
 114                let id = self.string_matches.get(i)?.candidate_id;
 115                entries.iter().find(|entry| entry.path.id == id).cloned()
 116            }
 117            DirectoryState::None { .. } => None,
 118        }
 119    }
 120
 121    #[cfg(any(test, feature = "test-support"))]
 122    pub fn collect_match_candidates(&self) -> Vec<String> {
 123        match &self.directory_state {
 124            DirectoryState::List { entries, .. } => self
 125                .string_matches
 126                .iter()
 127                .filter_map(|string_match| {
 128                    entries
 129                        .iter()
 130                        .find(|entry| entry.path.id == string_match.candidate_id)
 131                        .map(|candidate| candidate.path.string.clone())
 132                })
 133                .collect(),
 134            DirectoryState::Create {
 135                user_input,
 136                entries,
 137                ..
 138            } => user_input
 139                .iter()
 140                .filter(|user_input| !user_input.exists || !user_input.is_dir)
 141                .map(|user_input| user_input.file.string.clone())
 142                .chain(self.string_matches.iter().filter_map(|string_match| {
 143                    entries
 144                        .iter()
 145                        .find(|entry| entry.path.id == string_match.candidate_id)
 146                        .map(|candidate| candidate.path.string.clone())
 147                }))
 148                .collect(),
 149            DirectoryState::None { .. } => Vec::new(),
 150        }
 151    }
 152
 153    fn current_dir(&self) -> &'static str {
 154        match self.path_style {
 155            PathStyle::Posix => "./",
 156            PathStyle::Windows => ".\\",
 157        }
 158    }
 159}
 160
 161#[derive(Debug)]
 162enum DirectoryState {
 163    List {
 164        parent_path: String,
 165        entries: Vec<CandidateInfo>,
 166        error: Option<SharedString>,
 167    },
 168    Create {
 169        parent_path: String,
 170        user_input: Option<UserInput>,
 171        entries: Vec<CandidateInfo>,
 172    },
 173    None {
 174        create: bool,
 175    },
 176}
 177
 178#[derive(Debug, Clone)]
 179struct UserInput {
 180    file: StringMatchCandidate,
 181    exists: bool,
 182    is_dir: bool,
 183}
 184
 185#[derive(Debug, Clone)]
 186struct CandidateInfo {
 187    path: StringMatchCandidate,
 188    is_dir: bool,
 189}
 190
 191impl OpenPathPrompt {
 192    pub fn register(
 193        workspace: &mut Workspace,
 194        _window: Option<&mut Window>,
 195        _: &mut Context<Workspace>,
 196    ) {
 197        workspace.set_prompt_for_open_path(Box::new(|workspace, lister, window, cx| {
 198            let (tx, rx) = futures::channel::oneshot::channel();
 199            Self::prompt_for_open_path(workspace, lister, false, None, tx, window, cx);
 200            rx
 201        }));
 202    }
 203
 204    pub fn register_new_path(
 205        workspace: &mut Workspace,
 206        _window: Option<&mut Window>,
 207        _: &mut Context<Workspace>,
 208    ) {
 209        workspace.set_prompt_for_new_path(Box::new(
 210            |workspace, lister, suggested_name, window, cx| {
 211                let (tx, rx) = futures::channel::oneshot::channel();
 212                Self::prompt_for_new_path(workspace, lister, suggested_name, tx, window, cx);
 213                rx
 214            },
 215        ));
 216    }
 217
 218    fn prompt_for_open_path(
 219        workspace: &mut Workspace,
 220        lister: DirectoryLister,
 221        creating_path: bool,
 222        suggested_name: Option<String>,
 223        tx: oneshot::Sender<Option<Vec<PathBuf>>>,
 224        window: &mut Window,
 225        cx: &mut Context<Workspace>,
 226    ) {
 227        workspace.toggle_modal(window, cx, |window, cx| {
 228            let delegate =
 229                OpenPathDelegate::new(tx, lister.clone(), creating_path, cx).show_hidden();
 230            let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
 231            let mut query = lister.default_query(cx);
 232            if let Some(suggested_name) = suggested_name {
 233                query.push_str(&suggested_name);
 234            }
 235            picker.set_query(&query, window, cx);
 236            picker
 237        });
 238    }
 239
 240    fn prompt_for_new_path(
 241        workspace: &mut Workspace,
 242        lister: DirectoryLister,
 243        suggested_name: Option<String>,
 244        tx: oneshot::Sender<Option<Vec<PathBuf>>>,
 245        window: &mut Window,
 246        cx: &mut Context<Workspace>,
 247    ) {
 248        Self::prompt_for_open_path(workspace, lister, true, suggested_name, tx, window, cx);
 249    }
 250}
 251
 252impl PickerDelegate for OpenPathDelegate {
 253    type ListItem = ui::ListItem;
 254
 255    fn match_count(&self) -> usize {
 256        let user_input = if let DirectoryState::Create { user_input, .. } = &self.directory_state {
 257            user_input
 258                .as_ref()
 259                .filter(|input| !input.exists || !input.is_dir)
 260                .into_iter()
 261                .count()
 262        } else {
 263            0
 264        };
 265        self.string_matches.len() + user_input
 266    }
 267
 268    fn selected_index(&self) -> usize {
 269        self.selected_index
 270    }
 271
 272    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 273        self.selected_index = ix;
 274        cx.notify();
 275    }
 276
 277    fn update_matches(
 278        &mut self,
 279        query: String,
 280        window: &mut Window,
 281        cx: &mut Context<Picker<Self>>,
 282    ) -> Task<()> {
 283        let lister = &self.lister;
 284        let input_is_empty = query.is_empty();
 285        let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
 286
 287        let query = match &self.directory_state {
 288            DirectoryState::List { parent_path, .. } => {
 289                if parent_path == &dir {
 290                    None
 291                } else {
 292                    Some(lister.list_directory(dir.clone(), cx))
 293                }
 294            }
 295            DirectoryState::Create {
 296                parent_path,
 297                user_input,
 298                ..
 299            } => {
 300                if parent_path == &dir
 301                    && user_input.as_ref().map(|input| &input.file.string) == Some(&suffix)
 302                {
 303                    None
 304                } else {
 305                    Some(lister.list_directory(dir.clone(), cx))
 306                }
 307            }
 308            DirectoryState::None { .. } => Some(lister.list_directory(dir.clone(), cx)),
 309        };
 310        self.cancel_flag.store(true, atomic::Ordering::Release);
 311        self.cancel_flag = Arc::new(AtomicBool::new(false));
 312        let cancel_flag = self.cancel_flag.clone();
 313        let hidden_entries = self.hidden_entries;
 314        let parent_path_is_root = self.prompt_root == dir;
 315        let current_dir = self.current_dir();
 316        cx.spawn_in(window, async move |this, cx| {
 317            if let Some(query) = query {
 318                let paths = query.await;
 319                if cancel_flag.load(atomic::Ordering::Acquire) {
 320                    return;
 321                }
 322
 323                if this
 324                    .update(cx, |this, _| {
 325                        let new_state = match &this.delegate.directory_state {
 326                            DirectoryState::None { create: false }
 327                            | DirectoryState::List { .. } => match paths {
 328                                Ok(paths) => DirectoryState::List {
 329                                    entries: path_candidates(parent_path_is_root, paths),
 330                                    parent_path: dir.clone(),
 331                                    error: None,
 332                                },
 333                                Err(e) => DirectoryState::List {
 334                                    entries: Vec::new(),
 335                                    parent_path: dir.clone(),
 336                                    error: Some(SharedString::from(e.to_string())),
 337                                },
 338                            },
 339                            DirectoryState::None { create: true }
 340                            | DirectoryState::Create { .. } => match paths {
 341                                Ok(paths) => {
 342                                    let mut entries = path_candidates(parent_path_is_root, paths);
 343                                    let mut exists = false;
 344                                    let mut is_dir = false;
 345                                    let mut new_id = None;
 346                                    entries.retain(|entry| {
 347                                        new_id = new_id.max(Some(entry.path.id));
 348                                        if entry.path.string == suffix {
 349                                            exists = true;
 350                                            is_dir = entry.is_dir;
 351                                        }
 352                                        !exists || is_dir
 353                                    });
 354
 355                                    let new_id = new_id.map(|id| id + 1).unwrap_or(0);
 356                                    let user_input = if suffix.is_empty() {
 357                                        None
 358                                    } else {
 359                                        Some(UserInput {
 360                                            file: StringMatchCandidate::new(new_id, &suffix),
 361                                            exists,
 362                                            is_dir,
 363                                        })
 364                                    };
 365                                    DirectoryState::Create {
 366                                        entries,
 367                                        parent_path: dir.clone(),
 368                                        user_input,
 369                                    }
 370                                }
 371                                Err(_) => DirectoryState::Create {
 372                                    entries: Vec::new(),
 373                                    parent_path: dir.clone(),
 374                                    user_input: Some(UserInput {
 375                                        exists: false,
 376                                        is_dir: false,
 377                                        file: StringMatchCandidate::new(0, &suffix),
 378                                    }),
 379                                },
 380                            },
 381                        };
 382                        this.delegate.directory_state = new_state;
 383                    })
 384                    .is_err()
 385                {
 386                    return;
 387                }
 388            }
 389
 390            let Ok(mut new_entries) =
 391                this.update(cx, |this, _| match &this.delegate.directory_state {
 392                    DirectoryState::List {
 393                        entries,
 394                        error: None,
 395                        ..
 396                    }
 397                    | DirectoryState::Create { entries, .. } => entries.clone(),
 398                    DirectoryState::List { error: Some(_), .. } | DirectoryState::None { .. } => {
 399                        Vec::new()
 400                    }
 401                })
 402            else {
 403                return;
 404            };
 405
 406            if !hidden_entries {
 407                new_entries.retain(|entry| !entry.path.string.starts_with('.'));
 408            }
 409
 410            let max_id = new_entries
 411                .iter()
 412                .map(|entry| entry.path.id)
 413                .max()
 414                .unwrap_or(0);
 415
 416            if suffix.is_empty() {
 417                let should_prepend_with_current_dir = this
 418                    .read_with(cx, |picker, _| {
 419                        !input_is_empty
 420                            && match &picker.delegate.directory_state {
 421                                DirectoryState::List { error, .. } => error.is_none(),
 422                                DirectoryState::Create { .. } => false,
 423                                DirectoryState::None { .. } => false,
 424                            }
 425                    })
 426                    .unwrap_or(false);
 427
 428                let current_dir_in_new_entries = new_entries
 429                    .iter()
 430                    .any(|entry| &entry.path.string == current_dir);
 431
 432                if should_prepend_with_current_dir && !current_dir_in_new_entries {
 433                    new_entries.insert(
 434                        0,
 435                        CandidateInfo {
 436                            path: StringMatchCandidate {
 437                                id: max_id + 1,
 438                                string: current_dir.to_string(),
 439                                char_bag: CharBag::from(current_dir),
 440                            },
 441                            is_dir: true,
 442                        },
 443                    );
 444                }
 445
 446                this.update(cx, |this, cx| {
 447                    this.delegate.selected_index = 0;
 448                    this.delegate.string_matches = new_entries
 449                        .iter()
 450                        .map(|m| StringMatch {
 451                            candidate_id: m.path.id,
 452                            score: 0.0,
 453                            positions: Vec::new(),
 454                            string: m.path.string.clone(),
 455                        })
 456                        .collect();
 457                    this.delegate.directory_state =
 458                        match &this.delegate.directory_state {
 459                            DirectoryState::None { create: false }
 460                            | DirectoryState::List { .. } => DirectoryState::List {
 461                                parent_path: dir.clone(),
 462                                entries: new_entries,
 463                                error: None,
 464                            },
 465                            DirectoryState::None { create: true }
 466                            | DirectoryState::Create { .. } => DirectoryState::Create {
 467                                parent_path: dir.clone(),
 468                                user_input: None,
 469                                entries: new_entries,
 470                            },
 471                        };
 472                    cx.notify();
 473                })
 474                .ok();
 475                return;
 476            }
 477
 478            let Ok(is_create_state) =
 479                this.update(cx, |this, _| match &this.delegate.directory_state {
 480                    DirectoryState::Create { .. } => true,
 481                    DirectoryState::List { .. } => false,
 482                    DirectoryState::None { create } => *create,
 483                })
 484            else {
 485                return;
 486            };
 487
 488            let candidates = new_entries
 489                .iter()
 490                .filter_map(|entry| {
 491                    if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
 492                    {
 493                        None
 494                    } else if !suffix.is_empty() && entry.path.string == current_dir {
 495                        None
 496                    } else {
 497                        Some(&entry.path)
 498                    }
 499                })
 500                .collect::<Vec<_>>();
 501
 502            let matches = fuzzy::match_strings(
 503                candidates.as_slice(),
 504                &suffix,
 505                false,
 506                true,
 507                100,
 508                &cancel_flag,
 509                cx.background_executor().clone(),
 510            )
 511            .await;
 512            if cancel_flag.load(atomic::Ordering::Acquire) {
 513                return;
 514            }
 515
 516            this.update(cx, |this, cx| {
 517                this.delegate.selected_index = 0;
 518                this.delegate.string_matches = matches.clone();
 519                this.delegate.string_matches.sort_by_key(|m| {
 520                    (
 521                        new_entries
 522                            .iter()
 523                            .find(|entry| entry.path.id == m.candidate_id)
 524                            .map(|entry| &entry.path)
 525                            .map(|candidate| !candidate.string.starts_with(&suffix)),
 526                        m.candidate_id,
 527                    )
 528                });
 529                this.delegate.directory_state = match &this.delegate.directory_state {
 530                    DirectoryState::None { create: false } | DirectoryState::List { .. } => {
 531                        DirectoryState::List {
 532                            entries: new_entries,
 533                            parent_path: dir.clone(),
 534                            error: None,
 535                        }
 536                    }
 537                    DirectoryState::None { create: true } => DirectoryState::Create {
 538                        entries: new_entries,
 539                        parent_path: dir.clone(),
 540                        user_input: Some(UserInput {
 541                            file: StringMatchCandidate::new(0, &suffix),
 542                            exists: false,
 543                            is_dir: false,
 544                        }),
 545                    },
 546                    DirectoryState::Create { user_input, .. } => {
 547                        let (new_id, exists, is_dir) = user_input
 548                            .as_ref()
 549                            .map(|input| (input.file.id, input.exists, input.is_dir))
 550                            .unwrap_or_else(|| (0, false, false));
 551                        DirectoryState::Create {
 552                            entries: new_entries,
 553                            parent_path: dir.clone(),
 554                            user_input: Some(UserInput {
 555                                file: StringMatchCandidate::new(new_id, &suffix),
 556                                exists,
 557                                is_dir,
 558                            }),
 559                        }
 560                    }
 561                };
 562
 563                cx.notify();
 564            })
 565            .ok();
 566        })
 567    }
 568
 569    fn confirm_completion(
 570        &mut self,
 571        query: String,
 572        _window: &mut Window,
 573        _: &mut Context<Picker<Self>>,
 574    ) -> Option<String> {
 575        let candidate = self.get_entry(self.selected_index)?;
 576        if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
 577            return None;
 578        }
 579
 580        let path_style = self.path_style;
 581        Some(
 582            maybe!({
 583                match &self.directory_state {
 584                    DirectoryState::Create { parent_path, .. } => Some(format!(
 585                        "{}{}{}",
 586                        parent_path,
 587                        candidate.path.string,
 588                        if candidate.is_dir {
 589                            path_style.primary_separator()
 590                        } else {
 591                            ""
 592                        }
 593                    )),
 594                    DirectoryState::List { parent_path, .. } => Some(format!(
 595                        "{}{}{}",
 596                        parent_path,
 597                        candidate.path.string,
 598                        if candidate.is_dir {
 599                            path_style.primary_separator()
 600                        } else {
 601                            ""
 602                        }
 603                    )),
 604                    DirectoryState::None { .. } => return None,
 605                }
 606            })
 607            .unwrap_or(query),
 608        )
 609    }
 610
 611    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 612        let Some(candidate) = self.get_entry(self.selected_index) else {
 613            return;
 614        };
 615
 616        match &self.directory_state {
 617            DirectoryState::None { .. } => return,
 618            DirectoryState::List { parent_path, .. } => {
 619                let confirmed_path =
 620                    if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
 621                        PathBuf::from(&self.prompt_root)
 622                    } else {
 623                        Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
 624                            .join(&candidate.path.string)
 625                    };
 626                if let Some(tx) = self.tx.take() {
 627                    tx.send(Some(vec![confirmed_path])).ok();
 628                }
 629            }
 630            DirectoryState::Create {
 631                parent_path,
 632                user_input,
 633                ..
 634            } => match user_input {
 635                None => return,
 636                Some(user_input) => {
 637                    if user_input.is_dir {
 638                        return;
 639                    }
 640                    let prompted_path =
 641                        if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
 642                            PathBuf::from(&self.prompt_root)
 643                        } else {
 644                            Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
 645                                .join(&user_input.file.string)
 646                        };
 647                    if user_input.exists {
 648                        self.should_dismiss = false;
 649                        let answer = window.prompt(
 650                            gpui::PromptLevel::Critical,
 651                            &format!("{prompted_path:?} already exists. Do you want to replace it?"),
 652                            Some(
 653                                "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
 654                            ),
 655                            &["Replace", "Cancel"],
 656                            cx
 657                        );
 658                        self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
 659                            let answer = answer.await.ok();
 660                            picker
 661                                .update(cx, |picker, cx| {
 662                                    picker.delegate.should_dismiss = true;
 663                                    if answer != Some(0) {
 664                                        return;
 665                                    }
 666                                    if let Some(tx) = picker.delegate.tx.take() {
 667                                        tx.send(Some(vec![prompted_path])).ok();
 668                                    }
 669                                    cx.emit(gpui::DismissEvent);
 670                                })
 671                                .ok();
 672                        });
 673                        return;
 674                    } else if let Some(tx) = self.tx.take() {
 675                        tx.send(Some(vec![prompted_path])).ok();
 676                    }
 677                }
 678            },
 679        }
 680
 681        cx.emit(gpui::DismissEvent);
 682    }
 683
 684    fn should_dismiss(&self) -> bool {
 685        self.should_dismiss
 686    }
 687
 688    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 689        if let Some(tx) = self.tx.take() {
 690            tx.send(None).ok();
 691        }
 692        cx.emit(gpui::DismissEvent)
 693    }
 694
 695    fn render_match(
 696        &self,
 697        ix: usize,
 698        selected: bool,
 699        window: &mut Window,
 700        cx: &mut Context<Picker<Self>>,
 701    ) -> Option<Self::ListItem> {
 702        let settings = FileFinderSettings::get_global(cx);
 703        let candidate = self.get_entry(ix)?;
 704        let mut match_positions = match &self.directory_state {
 705            DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
 706            DirectoryState::Create { user_input, .. } => {
 707                if let Some(user_input) = user_input {
 708                    if !user_input.exists || !user_input.is_dir {
 709                        if ix == 0 {
 710                            Vec::new()
 711                        } else {
 712                            self.string_matches.get(ix - 1)?.positions.clone()
 713                        }
 714                    } else {
 715                        self.string_matches.get(ix)?.positions.clone()
 716                    }
 717                } else {
 718                    self.string_matches.get(ix)?.positions.clone()
 719                }
 720            }
 721            DirectoryState::None { .. } => Vec::new(),
 722        };
 723
 724        let is_current_dir_candidate = candidate.path.string == self.current_dir();
 725
 726        let file_icon = maybe!({
 727            if !settings.file_icons {
 728                return None;
 729            }
 730
 731            let path = path::Path::new(&candidate.path.string);
 732            let icon = if candidate.is_dir {
 733                if is_current_dir_candidate {
 734                    return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
 735                } else {
 736                    FileIcons::get_folder_icon(false, path, cx)?
 737                }
 738            } else {
 739                FileIcons::get_icon(path, cx)?
 740            };
 741            Some(Icon::from_path(icon).color(Color::Muted))
 742        });
 743
 744        match &self.directory_state {
 745            DirectoryState::List { parent_path, .. } => {
 746                let (label, indices) = if is_current_dir_candidate {
 747                    ("open this directory".to_string(), vec![])
 748                } else if *parent_path == self.prompt_root {
 749                    match_positions.iter_mut().for_each(|position| {
 750                        *position += self.prompt_root.len();
 751                    });
 752                    (
 753                        format!("{}{}", self.prompt_root, candidate.path.string),
 754                        match_positions,
 755                    )
 756                } else {
 757                    (candidate.path.string, match_positions)
 758                };
 759                Some(
 760                    ListItem::new(ix)
 761                        .spacing(ListItemSpacing::Sparse)
 762                        .start_slot::<Icon>(file_icon)
 763                        .inset(true)
 764                        .toggle_state(selected)
 765                        .child(HighlightedLabel::new(label, indices)),
 766                )
 767            }
 768            DirectoryState::Create {
 769                parent_path,
 770                user_input,
 771                ..
 772            } => {
 773                let (label, delta) = if *parent_path == self.prompt_root {
 774                    match_positions.iter_mut().for_each(|position| {
 775                        *position += self.prompt_root.len();
 776                    });
 777                    (
 778                        format!("{}{}", self.prompt_root, candidate.path.string),
 779                        self.prompt_root.len(),
 780                    )
 781                } else {
 782                    (candidate.path.string.clone(), 0)
 783                };
 784
 785                let label_with_highlights = match user_input {
 786                    Some(user_input) => {
 787                        let label_len = label.len();
 788                        if user_input.file.string == candidate.path.string {
 789                            if user_input.exists {
 790                                let label = if user_input.is_dir {
 791                                    label
 792                                } else {
 793                                    format!("{label} (replace)")
 794                                };
 795                                StyledText::new(label)
 796                                    .with_default_highlights(
 797                                        &window.text_style(),
 798                                        vec![(
 799                                            delta..label_len,
 800                                            HighlightStyle::color(Color::Conflict.color(cx)),
 801                                        )],
 802                                    )
 803                                    .into_any_element()
 804                            } else {
 805                                StyledText::new(format!("{label} (create)"))
 806                                    .with_default_highlights(
 807                                        &window.text_style(),
 808                                        vec![(
 809                                            delta..label_len,
 810                                            HighlightStyle::color(Color::Created.color(cx)),
 811                                        )],
 812                                    )
 813                                    .into_any_element()
 814                            }
 815                        } else {
 816                            HighlightedLabel::new(label, match_positions).into_any_element()
 817                        }
 818                    }
 819                    None => HighlightedLabel::new(label, match_positions).into_any_element(),
 820                };
 821
 822                Some(
 823                    ListItem::new(ix)
 824                        .spacing(ListItemSpacing::Sparse)
 825                        .start_slot::<Icon>(file_icon)
 826                        .inset(true)
 827                        .toggle_state(selected)
 828                        .child(LabelLike::new().child(label_with_highlights)),
 829                )
 830            }
 831            DirectoryState::None { .. } => None,
 832        }
 833    }
 834
 835    fn render_footer(
 836        &self,
 837        window: &mut Window,
 838        cx: &mut Context<Picker<Self>>,
 839    ) -> Option<AnyElement> {
 840        (self.render_footer)(window, cx)
 841    }
 842
 843    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
 844        Some(match &self.directory_state {
 845            DirectoryState::Create { .. } => SharedString::from("Type a path…"),
 846            DirectoryState::List {
 847                error: Some(error), ..
 848            } => error.clone(),
 849            DirectoryState::List { .. } | DirectoryState::None { .. } => {
 850                SharedString::from("No such file or directory")
 851            }
 852        })
 853    }
 854
 855    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 856        Arc::from(
 857            format!(
 858                "[directory{}]filename.ext",
 859                self.path_style.primary_separator()
 860            )
 861            .as_str(),
 862        )
 863    }
 864
 865    fn separators_after_indices(&self) -> Vec<usize> {
 866        let Some(m) = self.string_matches.first() else {
 867            return Vec::new();
 868        };
 869        if m.string == self.current_dir() {
 870            vec![0]
 871        } else {
 872            Vec::new()
 873        }
 874    }
 875}
 876
 877fn path_candidates(
 878    parent_path_is_root: bool,
 879    mut children: Vec<DirectoryItem>,
 880) -> Vec<CandidateInfo> {
 881    if parent_path_is_root {
 882        children.push(DirectoryItem {
 883            is_dir: true,
 884            path: PathBuf::default(),
 885        });
 886    }
 887
 888    children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
 889    children
 890        .iter()
 891        .enumerate()
 892        .map(|(ix, item)| CandidateInfo {
 893            path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
 894            is_dir: item.is_dir,
 895        })
 896        .collect()
 897}
 898
 899fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
 900    match path_style {
 901        PathStyle::Posix => {
 902            let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
 903                (query[..index].to_string(), query[index + 1..].to_string())
 904            } else {
 905                (query, String::new())
 906            };
 907            if !dir.ends_with('/') {
 908                dir.push('/');
 909            }
 910            (dir, suffix)
 911        }
 912        PathStyle::Windows => {
 913            let last_sep = query.rfind('\\').into_iter().chain(query.rfind('/')).max();
 914            let (mut dir, suffix) = if let Some(index) = last_sep {
 915                (
 916                    query[..index + 1].to_string(),
 917                    query[index + 1..].to_string(),
 918                )
 919            } else {
 920                (query, String::new())
 921            };
 922            if dir.len() < 3 {
 923                dir = "C:\\".to_string();
 924            }
 925            (dir, suffix)
 926        }
 927    }
 928}
 929
 930#[cfg(test)]
 931mod tests {
 932    use util::paths::PathStyle;
 933
 934    use super::get_dir_and_suffix;
 935
 936    #[test]
 937    fn test_get_dir_and_suffix_with_windows_style() {
 938        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
 939        assert_eq!(dir, "C:\\");
 940        assert_eq!(suffix, "");
 941
 942        let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
 943        assert_eq!(dir, "C:\\");
 944        assert_eq!(suffix, "");
 945
 946        let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
 947        assert_eq!(dir, "C:\\");
 948        assert_eq!(suffix, "");
 949
 950        let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
 951        assert_eq!(dir, "C:\\");
 952        assert_eq!(suffix, "Use");
 953
 954        let (dir, suffix) =
 955            get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
 956        assert_eq!(dir, "C:\\Users\\Junkui\\");
 957        assert_eq!(suffix, "Docum");
 958
 959        let (dir, suffix) =
 960            get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
 961        assert_eq!(dir, "C:\\Users\\Junkui\\");
 962        assert_eq!(suffix, "Documents");
 963
 964        let (dir, suffix) =
 965            get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
 966        assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
 967        assert_eq!(suffix, "");
 968
 969        let (dir, suffix) = get_dir_and_suffix("C:\\root\\.".into(), PathStyle::Windows);
 970        assert_eq!(dir, "C:\\root\\");
 971        assert_eq!(suffix, ".");
 972
 973        let (dir, suffix) = get_dir_and_suffix("C:\\root\\..".into(), PathStyle::Windows);
 974        assert_eq!(dir, "C:\\root\\");
 975        assert_eq!(suffix, "..");
 976
 977        let (dir, suffix) = get_dir_and_suffix("C:\\root\\.hidden".into(), PathStyle::Windows);
 978        assert_eq!(dir, "C:\\root\\");
 979        assert_eq!(suffix, ".hidden");
 980
 981        let (dir, suffix) = get_dir_and_suffix("C:/root/".into(), PathStyle::Windows);
 982        assert_eq!(dir, "C:/root/");
 983        assert_eq!(suffix, "");
 984
 985        let (dir, suffix) = get_dir_and_suffix("C:/root/Use".into(), PathStyle::Windows);
 986        assert_eq!(dir, "C:/root/");
 987        assert_eq!(suffix, "Use");
 988
 989        let (dir, suffix) = get_dir_and_suffix("C:\\root/Use".into(), PathStyle::Windows);
 990        assert_eq!(dir, "C:\\root/");
 991        assert_eq!(suffix, "Use");
 992
 993        let (dir, suffix) = get_dir_and_suffix("C:/root\\.hidden".into(), PathStyle::Windows);
 994        assert_eq!(dir, "C:/root\\");
 995        assert_eq!(suffix, ".hidden");
 996    }
 997
 998    #[test]
 999    fn test_get_dir_and_suffix_with_posix_style() {
1000        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
1001        assert_eq!(dir, "/");
1002        assert_eq!(suffix, "");
1003
1004        let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
1005        assert_eq!(dir, "/");
1006        assert_eq!(suffix, "");
1007
1008        let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
1009        assert_eq!(dir, "/");
1010        assert_eq!(suffix, "Use");
1011
1012        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
1013        assert_eq!(dir, "/Users/Junkui/");
1014        assert_eq!(suffix, "Docum");
1015
1016        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
1017        assert_eq!(dir, "/Users/Junkui/");
1018        assert_eq!(suffix, "Documents");
1019
1020        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
1021        assert_eq!(dir, "/Users/Junkui/Documents/");
1022        assert_eq!(suffix, "");
1023
1024        let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Posix);
1025        assert_eq!(dir, "/root/");
1026        assert_eq!(suffix, ".");
1027
1028        let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Posix);
1029        assert_eq!(dir, "/root/");
1030        assert_eq!(suffix, "..");
1031
1032        let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Posix);
1033        assert_eq!(dir, "/root/");
1034        assert_eq!(suffix, ".hidden");
1035    }
1036}