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