Show path in file picker and explorer

The file picker and explorer can each be opened in several ways, each
causing it to have a different root path. What's more, the file explorer
can change its path while you have it open.

This can make it very difficult to keep track of the root directory for
these pickers, so we add it as a header.

If desired, we could alternatively make this a configuration option, but
I see little downside to always including it.
pull/12806/head
Paho Lurie-Gregg 2025-02-07 01:52:41 -08:00
parent 0815b52e09
commit 9dc6fb8216
2 changed files with 30 additions and 3 deletions

View File

@ -32,6 +32,7 @@ pub use text::Text;
use helix_view::Editor; use helix_view::Editor;
use tui::text::{Span, Spans}; use tui::text::{Span, Spans};
use std::borrow::Cow;
use std::path::Path; use std::path::Path;
use std::{error::Error, path::PathBuf}; use std::{error::Error, path::PathBuf};
@ -185,6 +186,23 @@ pub fn raw_regex_prompt(
cx.push_layer(Box::new(prompt)); cx.push_layer(Box::new(prompt));
} }
/// Get the relative directory as a string.
///
/// NOTE: Assumes the given path is a directory, and will always output wiht a
/// trailing slash.
fn get_relative_dir(path: &Path) -> Cow<'static, str> {
let path = helix_stdx::path::get_relative_path(path);
if path.components().next().is_none() {
"./".into()
} else {
let mut str = path.to_string_lossy().into_owned();
if !str.ends_with('/') {
str.push('/');
}
str.into()
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct FilePickerData { pub struct FilePickerData {
root: PathBuf, root: PathBuf,
@ -246,7 +264,7 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
let columns = [PickerColumn::new( let columns = [PickerColumn::new(
"path", get_relative_dir(&root),
|item: &PathBuf, data: &FilePickerData| { |item: &PathBuf, data: &FilePickerData| {
let path = item.strip_prefix(&data.root).unwrap_or(item); let path = item.strip_prefix(&data.root).unwrap_or(item);
let mut spans = Vec::with_capacity(3); let mut spans = Vec::with_capacity(3);
@ -274,6 +292,7 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
cx.editor.set_error(err); cx.editor.set_error(err);
} }
}) })
.always_show_headers()
.with_preview(|_editor, path| Some((path.as_path().into(), None))); .with_preview(|_editor, path| Some((path.as_path().into(), None)));
let injector = picker.injector(); let injector = picker.injector();
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
@ -307,7 +326,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std
let directory_content = directory_content(&root)?; let directory_content = directory_content(&root)?;
let columns = [PickerColumn::new( let columns = [PickerColumn::new(
"path", get_relative_dir(&root),
|(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| { |(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| {
let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy(); let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy();
if *is_dir { if *is_dir {
@ -345,6 +364,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std
} }
}, },
) )
.always_show_headers()
.with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))); .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None)));
Ok(picker) Ok(picker)

View File

@ -241,6 +241,7 @@ type DynQueryCallback<T, D> =
pub struct Picker<T: 'static + Send + Sync, D: 'static> { pub struct Picker<T: 'static + Send + Sync, D: 'static> {
columns: Arc<[Column<T, D>]>, columns: Arc<[Column<T, D>]>,
primary_column: usize, primary_column: usize,
always_show_headers: bool,
editor_data: Arc<D>, editor_data: Arc<D>,
version: Arc<AtomicUsize>, version: Arc<AtomicUsize>,
matcher: Nucleo<T>, matcher: Nucleo<T>,
@ -373,6 +374,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
Self { Self {
columns, columns,
primary_column: default_column, primary_column: default_column,
always_show_headers: false,
matcher, matcher,
editor_data, editor_data,
version, version,
@ -424,6 +426,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
self self
} }
pub fn always_show_headers(mut self) -> Self {
self.always_show_headers = true;
self
}
pub fn with_dynamic_query( pub fn with_dynamic_query(
mut self, mut self,
callback: DynQueryCallback<T, D>, callback: DynQueryCallback<T, D>,
@ -818,7 +825,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
.widths(&self.widths); .widths(&self.widths);
// -- Header // -- Header
if self.columns.len() > 1 { if self.always_show_headers || self.columns.len() > 1 {
let active_column = self.query.active_column(self.prompt.position()); let active_column = self.query.active_column(self.prompt.position());
let header_style = cx.editor.theme.get("ui.picker.header"); let header_style = cx.editor.theme.get("ui.picker.header");
let header_column_style = cx.editor.theme.get("ui.picker.header.column"); let header_column_style = cx.editor.theme.get("ui.picker.header.column");