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 = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx);
 229            let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
 230            let mut query = lister.default_query(cx);
 231            if let Some(suggested_name) = suggested_name {
 232                query.push_str(&suggested_name);
 233            }
 234            picker.set_query(&query, window, cx);
 235            picker
 236        });
 237    }
 238
 239    fn prompt_for_new_path(
 240        workspace: &mut Workspace,
 241        lister: DirectoryLister,
 242        suggested_name: Option<String>,
 243        tx: oneshot::Sender<Option<Vec<PathBuf>>>,
 244        window: &mut Window,
 245        cx: &mut Context<Workspace>,
 246    ) {
 247        Self::prompt_for_open_path(workspace, lister, true, suggested_name, tx, window, cx);
 248    }
 249}
 250
 251impl PickerDelegate for OpenPathDelegate {
 252    type ListItem = ui::ListItem;
 253
 254    fn match_count(&self) -> usize {
 255        let user_input = if let DirectoryState::Create { user_input, .. } = &self.directory_state {
 256            user_input
 257                .as_ref()
 258                .filter(|input| !input.exists || !input.is_dir)
 259                .into_iter()
 260                .count()
 261        } else {
 262            0
 263        };
 264        self.string_matches.len() + user_input
 265    }
 266
 267    fn selected_index(&self) -> usize {
 268        self.selected_index
 269    }
 270
 271    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 272        self.selected_index = ix;
 273        cx.notify();
 274    }
 275
 276    fn update_matches(
 277        &mut self,
 278        query: String,
 279        window: &mut Window,
 280        cx: &mut Context<Picker<Self>>,
 281    ) -> Task<()> {
 282        let lister = &self.lister;
 283        let input_is_empty = query.is_empty();
 284        let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
 285
 286        let query = match &self.directory_state {
 287            DirectoryState::List { parent_path, .. } => {
 288                if parent_path == &dir {
 289                    None
 290                } else {
 291                    Some(lister.list_directory(dir.clone(), cx))
 292                }
 293            }
 294            DirectoryState::Create {
 295                parent_path,
 296                user_input,
 297                ..
 298            } => {
 299                if parent_path == &dir
 300                    && user_input.as_ref().map(|input| &input.file.string) == Some(&suffix)
 301                {
 302                    None
 303                } else {
 304                    Some(lister.list_directory(dir.clone(), cx))
 305                }
 306            }
 307            DirectoryState::None { .. } => Some(lister.list_directory(dir.clone(), cx)),
 308        };
 309        self.cancel_flag.store(true, atomic::Ordering::Release);
 310        self.cancel_flag = Arc::new(AtomicBool::new(false));
 311        let cancel_flag = self.cancel_flag.clone();
 312        let hidden_entries = self.hidden_entries;
 313        let parent_path_is_root = self.prompt_root == dir;
 314        let current_dir = self.current_dir();
 315        cx.spawn_in(window, async move |this, cx| {
 316            if let Some(query) = query {
 317                let paths = query.await;
 318                if cancel_flag.load(atomic::Ordering::Acquire) {
 319                    return;
 320                }
 321
 322                if this
 323                    .update(cx, |this, _| {
 324                        let new_state = match &this.delegate.directory_state {
 325                            DirectoryState::None { create: false }
 326                            | DirectoryState::List { .. } => match paths {
 327                                Ok(paths) => DirectoryState::List {
 328                                    entries: path_candidates(parent_path_is_root, paths),
 329                                    parent_path: dir.clone(),
 330                                    error: None,
 331                                },
 332                                Err(e) => DirectoryState::List {
 333                                    entries: Vec::new(),
 334                                    parent_path: dir.clone(),
 335                                    error: Some(SharedString::from(e.to_string())),
 336                                },
 337                            },
 338                            DirectoryState::None { create: true }
 339                            | DirectoryState::Create { .. } => match paths {
 340                                Ok(paths) => {
 341                                    let mut entries = path_candidates(parent_path_is_root, paths);
 342                                    let mut exists = false;
 343                                    let mut is_dir = false;
 344                                    let mut new_id = None;
 345                                    entries.retain(|entry| {
 346                                        new_id = new_id.max(Some(entry.path.id));
 347                                        if entry.path.string == suffix {
 348                                            exists = true;
 349                                            is_dir = entry.is_dir;
 350                                        }
 351                                        !exists || is_dir
 352                                    });
 353
 354                                    let new_id = new_id.map(|id| id + 1).unwrap_or(0);
 355                                    let user_input = if suffix.is_empty() {
 356                                        None
 357                                    } else {
 358                                        Some(UserInput {
 359                                            file: StringMatchCandidate::new(new_id, &suffix),
 360                                            exists,
 361                                            is_dir,
 362                                        })
 363                                    };
 364                                    DirectoryState::Create {
 365                                        entries,
 366                                        parent_path: dir.clone(),
 367                                        user_input,
 368                                    }
 369                                }
 370                                Err(_) => DirectoryState::Create {
 371                                    entries: Vec::new(),
 372                                    parent_path: dir.clone(),
 373                                    user_input: Some(UserInput {
 374                                        exists: false,
 375                                        is_dir: false,
 376                                        file: StringMatchCandidate::new(0, &suffix),
 377                                    }),
 378                                },
 379                            },
 380                        };
 381                        this.delegate.directory_state = new_state;
 382                    })
 383                    .is_err()
 384                {
 385                    return;
 386                }
 387            }
 388
 389            let Ok(mut new_entries) =
 390                this.update(cx, |this, _| match &this.delegate.directory_state {
 391                    DirectoryState::List {
 392                        entries,
 393                        error: None,
 394                        ..
 395                    }
 396                    | DirectoryState::Create { entries, .. } => entries.clone(),
 397                    DirectoryState::List { error: Some(_), .. } | DirectoryState::None { .. } => {
 398                        Vec::new()
 399                    }
 400                })
 401            else {
 402                return;
 403            };
 404
 405            let max_id = new_entries
 406                .iter()
 407                .map(|entry| entry.path.id)
 408                .max()
 409                .unwrap_or(0);
 410            if !suffix.starts_with('.') && !hidden_entries {
 411                new_entries.retain(|entry| !entry.path.string.starts_with('.'));
 412            }
 413
 414            if suffix.is_empty() {
 415                let should_prepend_with_current_dir = this
 416                    .read_with(cx, |picker, _| {
 417                        !input_is_empty
 418                            && match &picker.delegate.directory_state {
 419                                DirectoryState::List { error, .. } => error.is_none(),
 420                                DirectoryState::Create { .. } => false,
 421                                DirectoryState::None { .. } => false,
 422                            }
 423                    })
 424                    .unwrap_or(false);
 425
 426                let current_dir_in_new_entries = new_entries
 427                    .iter()
 428                    .any(|entry| &entry.path.string == current_dir);
 429
 430                if should_prepend_with_current_dir && !current_dir_in_new_entries {
 431                    new_entries.insert(
 432                        0,
 433                        CandidateInfo {
 434                            path: StringMatchCandidate {
 435                                id: max_id + 1,
 436                                string: current_dir.to_string(),
 437                                char_bag: CharBag::from(current_dir),
 438                            },
 439                            is_dir: true,
 440                        },
 441                    );
 442                }
 443
 444                this.update(cx, |this, cx| {
 445                    this.delegate.selected_index = 0;
 446                    this.delegate.string_matches = new_entries
 447                        .iter()
 448                        .map(|m| StringMatch {
 449                            candidate_id: m.path.id,
 450                            score: 0.0,
 451                            positions: Vec::new(),
 452                            string: m.path.string.clone(),
 453                        })
 454                        .collect();
 455                    this.delegate.directory_state =
 456                        match &this.delegate.directory_state {
 457                            DirectoryState::None { create: false }
 458                            | DirectoryState::List { .. } => DirectoryState::List {
 459                                parent_path: dir.clone(),
 460                                entries: new_entries,
 461                                error: None,
 462                            },
 463                            DirectoryState::None { create: true }
 464                            | DirectoryState::Create { .. } => DirectoryState::Create {
 465                                parent_path: dir.clone(),
 466                                user_input: None,
 467                                entries: new_entries,
 468                            },
 469                        };
 470                    cx.notify();
 471                })
 472                .ok();
 473                return;
 474            }
 475
 476            let Ok(is_create_state) =
 477                this.update(cx, |this, _| match &this.delegate.directory_state {
 478                    DirectoryState::Create { .. } => true,
 479                    DirectoryState::List { .. } => false,
 480                    DirectoryState::None { create } => *create,
 481                })
 482            else {
 483                return;
 484            };
 485
 486            let candidates = new_entries
 487                .iter()
 488                .filter_map(|entry| {
 489                    if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
 490                    {
 491                        None
 492                    } else {
 493                        Some(&entry.path)
 494                    }
 495                })
 496                .collect::<Vec<_>>();
 497
 498            let matches = fuzzy::match_strings(
 499                candidates.as_slice(),
 500                &suffix,
 501                false,
 502                true,
 503                100,
 504                &cancel_flag,
 505                cx.background_executor().clone(),
 506            )
 507            .await;
 508            if cancel_flag.load(atomic::Ordering::Acquire) {
 509                return;
 510            }
 511
 512            this.update(cx, |this, cx| {
 513                this.delegate.selected_index = 0;
 514                this.delegate.string_matches = matches.clone();
 515                this.delegate.string_matches.sort_by_key(|m| {
 516                    (
 517                        new_entries
 518                            .iter()
 519                            .find(|entry| entry.path.id == m.candidate_id)
 520                            .map(|entry| &entry.path)
 521                            .map(|candidate| !candidate.string.starts_with(&suffix)),
 522                        m.candidate_id,
 523                    )
 524                });
 525                this.delegate.directory_state = match &this.delegate.directory_state {
 526                    DirectoryState::None { create: false } | DirectoryState::List { .. } => {
 527                        DirectoryState::List {
 528                            entries: new_entries,
 529                            parent_path: dir.clone(),
 530                            error: None,
 531                        }
 532                    }
 533                    DirectoryState::None { create: true } => DirectoryState::Create {
 534                        entries: new_entries,
 535                        parent_path: dir.clone(),
 536                        user_input: Some(UserInput {
 537                            file: StringMatchCandidate::new(0, &suffix),
 538                            exists: false,
 539                            is_dir: false,
 540                        }),
 541                    },
 542                    DirectoryState::Create { user_input, .. } => {
 543                        let (new_id, exists, is_dir) = user_input
 544                            .as_ref()
 545                            .map(|input| (input.file.id, input.exists, input.is_dir))
 546                            .unwrap_or_else(|| (0, false, false));
 547                        DirectoryState::Create {
 548                            entries: new_entries,
 549                            parent_path: dir.clone(),
 550                            user_input: Some(UserInput {
 551                                file: StringMatchCandidate::new(new_id, &suffix),
 552                                exists,
 553                                is_dir,
 554                            }),
 555                        }
 556                    }
 557                };
 558
 559                cx.notify();
 560            })
 561            .ok();
 562        })
 563    }
 564
 565    fn confirm_completion(
 566        &mut self,
 567        query: String,
 568        _window: &mut Window,
 569        _: &mut Context<Picker<Self>>,
 570    ) -> Option<String> {
 571        let candidate = self.get_entry(self.selected_index)?;
 572        if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
 573            return None;
 574        }
 575
 576        let path_style = self.path_style;
 577        Some(
 578            maybe!({
 579                match &self.directory_state {
 580                    DirectoryState::Create { parent_path, .. } => Some(format!(
 581                        "{}{}{}",
 582                        parent_path,
 583                        candidate.path.string,
 584                        if candidate.is_dir {
 585                            path_style.primary_separator()
 586                        } else {
 587                            ""
 588                        }
 589                    )),
 590                    DirectoryState::List { parent_path, .. } => Some(format!(
 591                        "{}{}{}",
 592                        parent_path,
 593                        candidate.path.string,
 594                        if candidate.is_dir {
 595                            path_style.primary_separator()
 596                        } else {
 597                            ""
 598                        }
 599                    )),
 600                    DirectoryState::None { .. } => return None,
 601                }
 602            })
 603            .unwrap_or(query),
 604        )
 605    }
 606
 607    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 608        let Some(candidate) = self.get_entry(self.selected_index) else {
 609            return;
 610        };
 611
 612        match &self.directory_state {
 613            DirectoryState::None { .. } => return,
 614            DirectoryState::List { parent_path, .. } => {
 615                let confirmed_path =
 616                    if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
 617                        PathBuf::from(&self.prompt_root)
 618                    } else {
 619                        Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
 620                            .join(&candidate.path.string)
 621                    };
 622                if let Some(tx) = self.tx.take() {
 623                    tx.send(Some(vec![confirmed_path])).ok();
 624                }
 625            }
 626            DirectoryState::Create {
 627                parent_path,
 628                user_input,
 629                ..
 630            } => match user_input {
 631                None => return,
 632                Some(user_input) => {
 633                    if user_input.is_dir {
 634                        return;
 635                    }
 636                    let prompted_path =
 637                        if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
 638                            PathBuf::from(&self.prompt_root)
 639                        } else {
 640                            Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
 641                                .join(&user_input.file.string)
 642                        };
 643                    if user_input.exists {
 644                        self.should_dismiss = false;
 645                        let answer = window.prompt(
 646                            gpui::PromptLevel::Critical,
 647                            &format!("{prompted_path:?} already exists. Do you want to replace it?"),
 648                            Some(
 649                                "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
 650                            ),
 651                            &["Replace", "Cancel"],
 652                            cx
 653                        );
 654                        self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
 655                            let answer = answer.await.ok();
 656                            picker
 657                                .update(cx, |picker, cx| {
 658                                    picker.delegate.should_dismiss = true;
 659                                    if answer != Some(0) {
 660                                        return;
 661                                    }
 662                                    if let Some(tx) = picker.delegate.tx.take() {
 663                                        tx.send(Some(vec![prompted_path])).ok();
 664                                    }
 665                                    cx.emit(gpui::DismissEvent);
 666                                })
 667                                .ok();
 668                        });
 669                        return;
 670                    } else if let Some(tx) = self.tx.take() {
 671                        tx.send(Some(vec![prompted_path])).ok();
 672                    }
 673                }
 674            },
 675        }
 676
 677        cx.emit(gpui::DismissEvent);
 678    }
 679
 680    fn should_dismiss(&self) -> bool {
 681        self.should_dismiss
 682    }
 683
 684    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 685        if let Some(tx) = self.tx.take() {
 686            tx.send(None).ok();
 687        }
 688        cx.emit(gpui::DismissEvent)
 689    }
 690
 691    fn render_match(
 692        &self,
 693        ix: usize,
 694        selected: bool,
 695        window: &mut Window,
 696        cx: &mut Context<Picker<Self>>,
 697    ) -> Option<Self::ListItem> {
 698        let settings = FileFinderSettings::get_global(cx);
 699        let candidate = self.get_entry(ix)?;
 700        let mut match_positions = match &self.directory_state {
 701            DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
 702            DirectoryState::Create { user_input, .. } => {
 703                if let Some(user_input) = user_input {
 704                    if !user_input.exists || !user_input.is_dir {
 705                        if ix == 0 {
 706                            Vec::new()
 707                        } else {
 708                            self.string_matches.get(ix - 1)?.positions.clone()
 709                        }
 710                    } else {
 711                        self.string_matches.get(ix)?.positions.clone()
 712                    }
 713                } else {
 714                    self.string_matches.get(ix)?.positions.clone()
 715                }
 716            }
 717            DirectoryState::None { .. } => Vec::new(),
 718        };
 719
 720        let is_current_dir_candidate = candidate.path.string == self.current_dir();
 721
 722        let file_icon = maybe!({
 723            if !settings.file_icons {
 724                return None;
 725            }
 726
 727            let path = path::Path::new(&candidate.path.string);
 728            let icon = if candidate.is_dir {
 729                if is_current_dir_candidate {
 730                    return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
 731                } else {
 732                    FileIcons::get_folder_icon(false, path, cx)?
 733                }
 734            } else {
 735                FileIcons::get_icon(path, cx)?
 736            };
 737            Some(Icon::from_path(icon).color(Color::Muted))
 738        });
 739
 740        match &self.directory_state {
 741            DirectoryState::List { parent_path, .. } => {
 742                let (label, indices) = if is_current_dir_candidate {
 743                    ("open this directory".to_string(), vec![])
 744                } else if *parent_path == self.prompt_root {
 745                    match_positions.iter_mut().for_each(|position| {
 746                        *position += self.prompt_root.len();
 747                    });
 748                    (
 749                        format!("{}{}", self.prompt_root, candidate.path.string),
 750                        match_positions,
 751                    )
 752                } else {
 753                    (candidate.path.string, match_positions)
 754                };
 755                Some(
 756                    ListItem::new(ix)
 757                        .spacing(ListItemSpacing::Sparse)
 758                        .start_slot::<Icon>(file_icon)
 759                        .inset(true)
 760                        .toggle_state(selected)
 761                        .child(HighlightedLabel::new(label, indices)),
 762                )
 763            }
 764            DirectoryState::Create {
 765                parent_path,
 766                user_input,
 767                ..
 768            } => {
 769                let (label, delta) = if *parent_path == self.prompt_root {
 770                    match_positions.iter_mut().for_each(|position| {
 771                        *position += self.prompt_root.len();
 772                    });
 773                    (
 774                        format!("{}{}", self.prompt_root, candidate.path.string),
 775                        self.prompt_root.len(),
 776                    )
 777                } else {
 778                    (candidate.path.string.clone(), 0)
 779                };
 780
 781                let label_with_highlights = match user_input {
 782                    Some(user_input) => {
 783                        let label_len = label.len();
 784                        if user_input.file.string == candidate.path.string {
 785                            if user_input.exists {
 786                                let label = if user_input.is_dir {
 787                                    label
 788                                } else {
 789                                    format!("{label} (replace)")
 790                                };
 791                                StyledText::new(label)
 792                                    .with_default_highlights(
 793                                        &window.text_style(),
 794                                        vec![(
 795                                            delta..label_len,
 796                                            HighlightStyle::color(Color::Conflict.color(cx)),
 797                                        )],
 798                                    )
 799                                    .into_any_element()
 800                            } else {
 801                                StyledText::new(format!("{label} (create)"))
 802                                    .with_default_highlights(
 803                                        &window.text_style(),
 804                                        vec![(
 805                                            delta..label_len,
 806                                            HighlightStyle::color(Color::Created.color(cx)),
 807                                        )],
 808                                    )
 809                                    .into_any_element()
 810                            }
 811                        } else {
 812                            HighlightedLabel::new(label, match_positions).into_any_element()
 813                        }
 814                    }
 815                    None => HighlightedLabel::new(label, match_positions).into_any_element(),
 816                };
 817
 818                Some(
 819                    ListItem::new(ix)
 820                        .spacing(ListItemSpacing::Sparse)
 821                        .start_slot::<Icon>(file_icon)
 822                        .inset(true)
 823                        .toggle_state(selected)
 824                        .child(LabelLike::new().child(label_with_highlights)),
 825                )
 826            }
 827            DirectoryState::None { .. } => None,
 828        }
 829    }
 830
 831    fn render_footer(
 832        &self,
 833        window: &mut Window,
 834        cx: &mut Context<Picker<Self>>,
 835    ) -> Option<AnyElement> {
 836        (self.render_footer)(window, cx)
 837    }
 838
 839    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
 840        Some(match &self.directory_state {
 841            DirectoryState::Create { .. } => SharedString::from("Type a path…"),
 842            DirectoryState::List {
 843                error: Some(error), ..
 844            } => error.clone(),
 845            DirectoryState::List { .. } | DirectoryState::None { .. } => {
 846                SharedString::from("No such file or directory")
 847            }
 848        })
 849    }
 850
 851    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 852        Arc::from(
 853            format!(
 854                "[directory{}]filename.ext",
 855                self.path_style.primary_separator()
 856            )
 857            .as_str(),
 858        )
 859    }
 860
 861    fn separators_after_indices(&self) -> Vec<usize> {
 862        let Some(m) = self.string_matches.first() else {
 863            return Vec::new();
 864        };
 865        if m.string == self.current_dir() {
 866            vec![0]
 867        } else {
 868            Vec::new()
 869        }
 870    }
 871}
 872
 873fn path_candidates(
 874    parent_path_is_root: bool,
 875    mut children: Vec<DirectoryItem>,
 876) -> Vec<CandidateInfo> {
 877    if parent_path_is_root {
 878        children.push(DirectoryItem {
 879            is_dir: true,
 880            path: PathBuf::default(),
 881        });
 882    }
 883
 884    children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
 885    children
 886        .iter()
 887        .enumerate()
 888        .map(|(ix, item)| CandidateInfo {
 889            path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
 890            is_dir: item.is_dir,
 891        })
 892        .collect()
 893}
 894
 895#[cfg(target_os = "windows")]
 896fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
 897    let last_item = Path::new(&query)
 898        .file_name()
 899        .unwrap_or_default()
 900        .to_string_lossy();
 901    let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
 902        (dir.to_string(), last_item.into_owned())
 903    } else {
 904        (query.to_string(), String::new())
 905    };
 906    match path_style {
 907        PathStyle::Posix => {
 908            if dir.is_empty() {
 909                dir = "/".to_string();
 910            }
 911        }
 912        PathStyle::Windows => {
 913            if dir.len() < 3 {
 914                dir = "C:\\".to_string();
 915            }
 916        }
 917    }
 918    (dir, suffix)
 919}
 920
 921#[cfg(not(target_os = "windows"))]
 922fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
 923    match path_style {
 924        PathStyle::Posix => {
 925            let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
 926                (query[..index].to_string(), query[index + 1..].to_string())
 927            } else {
 928                (query, String::new())
 929            };
 930            if !dir.ends_with('/') {
 931                dir.push('/');
 932            }
 933            (dir, suffix)
 934        }
 935        PathStyle::Windows => {
 936            let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
 937                (query[..index].to_string(), query[index + 1..].to_string())
 938            } else {
 939                (query, String::new())
 940            };
 941            if dir.len() < 3 {
 942                dir = "C:\\".to_string();
 943            }
 944            if !dir.ends_with('\\') {
 945                dir.push('\\');
 946            }
 947            (dir, suffix)
 948        }
 949    }
 950}
 951
 952#[cfg(test)]
 953mod tests {
 954    use util::paths::PathStyle;
 955
 956    use super::get_dir_and_suffix;
 957
 958    #[test]
 959    fn test_get_dir_and_suffix_with_windows_style() {
 960        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
 961        assert_eq!(dir, "C:\\");
 962        assert_eq!(suffix, "");
 963
 964        let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
 965        assert_eq!(dir, "C:\\");
 966        assert_eq!(suffix, "");
 967
 968        let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
 969        assert_eq!(dir, "C:\\");
 970        assert_eq!(suffix, "");
 971
 972        let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
 973        assert_eq!(dir, "C:\\");
 974        assert_eq!(suffix, "Use");
 975
 976        let (dir, suffix) =
 977            get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
 978        assert_eq!(dir, "C:\\Users\\Junkui\\");
 979        assert_eq!(suffix, "Docum");
 980
 981        let (dir, suffix) =
 982            get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
 983        assert_eq!(dir, "C:\\Users\\Junkui\\");
 984        assert_eq!(suffix, "Documents");
 985
 986        let (dir, suffix) =
 987            get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
 988        assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
 989        assert_eq!(suffix, "");
 990    }
 991
 992    #[test]
 993    fn test_get_dir_and_suffix_with_posix_style() {
 994        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
 995        assert_eq!(dir, "/");
 996        assert_eq!(suffix, "");
 997
 998        let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
 999        assert_eq!(dir, "/");
1000        assert_eq!(suffix, "");
1001
1002        let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
1003        assert_eq!(dir, "/");
1004        assert_eq!(suffix, "Use");
1005
1006        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
1007        assert_eq!(dir, "/Users/Junkui/");
1008        assert_eq!(suffix, "Docum");
1009
1010        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
1011        assert_eq!(dir, "/Users/Junkui/");
1012        assert_eq!(suffix, "Documents");
1013
1014        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
1015        assert_eq!(dir, "/Users/Junkui/Documents/");
1016        assert_eq!(suffix, "");
1017    }
1018}