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