Skip to content

Commit

Permalink
Provide file-picker navigation
Browse files Browse the repository at this point in the history
We provide the following new keybindings while in a picker:
* `C-e` - Change picker root to one directory in, based on selection
* `C-a` - Change picker root to one directory out

These can be especially useful when combined with the command
`file_picker_in_current_buffer_directory` when navigating library
files.

That is, with this change the following flow is enabled:
1. Perform a `goto_defition` on a symbol defined in an external library.
2. Perform `file_picker_in_current_buffer_directory`, opening a picker
   in the external library.
3. Browse the external library with `C-e` and `C-a`, which is not
   possible without this change.

To accomplish this, we need access to a `FilePickerConfig` not just
when building the picker, but also later, so we add an associated
`Item::Config` type, which is simply `()` for everything except
`PathBuf`.

To make it easier to follow what is going on when navigating the
picker, we set the status bar to the picker's root directory on change.
  • Loading branch information
paholg committed Mar 14, 2024
1 parent 3915b04 commit 2225190
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 52 deletions.
2 changes: 2 additions & 0 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ Keys to use within picker. Remapping currently not supported.
| `Ctrl-v` | Open vertically |
| `Ctrl-t` | Toggle preview |
| `Escape`, `Ctrl-c` | Close picker |
| `Ctrl-e` | Change picker root to one directory in, based on selection |
| `Ctrl-a` | Change picker root to one directory out |

## Prompt

Expand Down
2 changes: 1 addition & 1 deletion helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ impl Application {

// If the first file is a directory, skip it and open a picker
if let Some((first, _)) = files_it.next_if(|(p, _)| p.is_dir()) {
let picker = ui::file_picker(first, &config.load().editor);
let picker = ui::file_picker(first, editor.config().file_picker);
compositor.push(Box::new(overlaid(picker)));
}

Expand Down
19 changes: 12 additions & 7 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,7 @@ fn goto_file_impl(cx: &mut Context, action: Action) {

let path = &rel_path.join(p);
if path.is_dir() {
let picker = ui::file_picker(path.into(), &cx.editor.config());
let picker = ui::file_picker(path.into(), cx.editor.config().file_picker);
cx.push_layer(Box::new(overlaid(picker)));
} else if let Err(e) = cx.editor.open(path, action) {
cx.editor.set_error(format!("Open file failed: {:?}", e));
Expand Down Expand Up @@ -1250,7 +1250,7 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
Ok(_) | Err(_) => {
let path = &rel_path.join(url.path());
if path.is_dir() {
let picker = ui::file_picker(path.into(), &cx.editor.config());
let picker = ui::file_picker(path.into(), cx.editor.config().file_picker);
cx.push_layer(Box::new(overlaid(picker)));
} else if let Err(e) = cx.editor.open(path, action) {
cx.editor.set_error(format!("Open file failed: {:?}", e));
Expand Down Expand Up @@ -2204,6 +2204,7 @@ fn global_search(cx: &mut Context) {

impl ui::menu::Item for FileResult {
type Data = Option<PathBuf>;
type Config = ();

fn format(&self, current_path: &Self::Data) -> Row {
let relative_path = helix_stdx::path::get_relative_path(&self.path)
Expand Down Expand Up @@ -2741,7 +2742,7 @@ fn file_picker(cx: &mut Context) {
cx.editor.set_error("Workspace directory does not exist");
return;
}
let picker = ui::file_picker(root, &cx.editor.config());
let picker = ui::file_picker(root, cx.editor.config().file_picker);
cx.push_layer(Box::new(overlaid(picker)));
}

Expand All @@ -2758,7 +2759,7 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
}
};

let picker = ui::file_picker(path, &cx.editor.config());
let picker = ui::file_picker(path, cx.editor.config().file_picker);
cx.push_layer(Box::new(overlaid(picker)));
}

Expand All @@ -2769,7 +2770,7 @@ fn file_picker_in_current_directory(cx: &mut Context) {
.set_error("Current working directory does not exist");
return;
}
let picker = ui::file_picker(cwd, &cx.editor.config());
let picker = ui::file_picker(cwd, cx.editor.config().file_picker);
cx.push_layer(Box::new(overlaid(picker)));
}

Expand All @@ -2786,6 +2787,7 @@ fn buffer_picker(cx: &mut Context) {

impl ui::menu::Item for BufferMeta {
type Data = ();
type Config = ();

fn format(&self, _data: &Self::Data) -> Row {
let path = self
Expand Down Expand Up @@ -2827,7 +2829,7 @@ fn buffer_picker(cx: &mut Context) {
// mru
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));

let picker = Picker::new(items, (), |cx, meta, action| {
let picker = Picker::new((), items, (), |cx, meta, action| {
cx.editor.switch(meta.id, action);
})
.with_preview(|editor, meta| {
Expand All @@ -2853,6 +2855,7 @@ fn jumplist_picker(cx: &mut Context) {

impl ui::menu::Item for JumpMeta {
type Data = ();
type Config = ();

fn format(&self, _data: &Self::Data) -> Row {
let path = self
Expand Down Expand Up @@ -2905,6 +2908,7 @@ fn jumplist_picker(cx: &mut Context) {
};

let picker = Picker::new(
(),
cx.editor
.tree
.views()
Expand Down Expand Up @@ -2935,6 +2939,7 @@ fn jumplist_picker(cx: &mut Context) {

impl ui::menu::Item for MappableCommand {
type Data = ReverseKeymap;
type Config = ();

fn format(&self, keymap: &Self::Data) -> Row {
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
Expand Down Expand Up @@ -2981,7 +2986,7 @@ pub fn command_palette(cx: &mut Context) {
}
}));

let picker = Picker::new(commands, keymap, move |cx, command, _action| {
let picker = Picker::new((), commands, keymap, move |cx, command, _action| {
let mut ctx = Context {
register,
count,
Expand Down
8 changes: 6 additions & 2 deletions helix-term/src/commands/dap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select

impl ui::menu::Item for StackFrame {
type Data = ();
type Config = ();

fn format(&self, _data: &Self::Data) -> Row {
self.name.as_str().into() // TODO: include thread_states in the label
Expand All @@ -32,6 +33,7 @@ impl ui::menu::Item for StackFrame {

impl ui::menu::Item for DebugTemplate {
type Data = ();
type Config = ();

fn format(&self, _data: &Self::Data) -> Row {
self.name.as_str().into()
Expand All @@ -40,6 +42,7 @@ impl ui::menu::Item for DebugTemplate {

impl ui::menu::Item for Thread {
type Data = ThreadStates;
type Config = ();

fn format(&self, thread_states: &Self::Data) -> Row {
format!(
Expand Down Expand Up @@ -73,7 +76,7 @@ fn thread_picker(
let debugger = debugger!(editor);

let thread_states = debugger.thread_states.clone();
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
let picker = Picker::new((), threads, thread_states, move |cx, thread, _action| {
callback_fn(cx.editor, thread)
})
.with_preview(move |editor, thread| {
Expand Down Expand Up @@ -269,6 +272,7 @@ pub fn dap_launch(cx: &mut Context) {
let templates = config.templates.clone();

cx.push_layer(Box::new(overlaid(Picker::new(
(),
templates,
(),
|cx, template, _action| {
Expand Down Expand Up @@ -735,7 +739,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {

let frames = debugger.stack_frames[&thread_id].clone();

let picker = Picker::new(frames, (), move |cx, frame, _action| {
let picker = Picker::new((), frames, (), move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find
let pos = debugger.stack_frames[&thread_id]
Expand Down
10 changes: 8 additions & 2 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ macro_rules! language_server_with_feature {
impl ui::menu::Item for lsp::Location {
/// Current working directory.
type Data = PathBuf;
type Config = ();

fn format(&self, cwdir: &Self::Data) -> Row {
// The preallocation here will overallocate a few characters since it will account for the
Expand Down Expand Up @@ -105,6 +106,7 @@ struct SymbolInformationItem {
impl ui::menu::Item for SymbolInformationItem {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
type Config = ();

fn format(&self, current_doc_path: &Self::Data) -> Row {
if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
Expand Down Expand Up @@ -141,6 +143,7 @@ struct PickerDiagnostic {

impl ui::menu::Item for PickerDiagnostic {
type Data = (DiagnosticStyles, DiagnosticsFormat);
type Config = ();

fn format(&self, (styles, format): &Self::Data) -> Row {
let mut style = self
Expand Down Expand Up @@ -246,7 +249,7 @@ type SymbolPicker = Picker<SymbolInformationItem>;

fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
Picker::new(symbols, current_path, move |cx, item, action| {
Picker::new((), symbols, current_path, move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
Expand Down Expand Up @@ -295,6 +298,7 @@ fn diag_picker(
};

Picker::new(
(),
flat_diag,
(styles, format),
move |cx,
Expand Down Expand Up @@ -502,6 +506,7 @@ struct CodeActionOrCommandItem {

impl ui::menu::Item for CodeActionOrCommandItem {
type Data = ();
type Config = ();
fn format(&self, _data: &Self::Data) -> Row {
match &self.lsp_item {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
Expand Down Expand Up @@ -752,6 +757,7 @@ pub fn code_action(cx: &mut Context) {

impl ui::menu::Item for lsp::Command {
type Data = ();
type Config = ();
fn format(&self, _data: &Self::Data) -> Row {
self.title.as_str().into()
}
Expand Down Expand Up @@ -822,7 +828,7 @@ fn goto_impl(
}
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
let picker = Picker::new((), locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
})
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
Expand Down
6 changes: 3 additions & 3 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,14 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
ensure!(!args.is_empty(), "wrong argument count");
for arg in args {
let (path, pos) = args::parse_file(arg);
let path = helix_stdx::path::expand_tilde(path);
let path = helix_stdx::path::expand_tilde(path).into_owned();
// If the path is a directory, open a file picker on that directory and update the status
// message
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path.into_owned(), &editor.config());
let picker = ui::file_picker(path, editor.config().file_picker);
compositor.push(Box::new(overlaid(picker)));
},
));
Expand Down Expand Up @@ -1391,7 +1391,7 @@ fn lsp_workspace_command(
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
let picker = ui::Picker::new((), commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, language_server_id, command.clone());
});
compositor.push(Box::new(overlaid(picker)))
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use helix_lsp::{lsp, util, OffsetEncoding};

impl menu::Item for CompletionItem {
type Data = ();
type Config = ();
fn sort_text(&self, data: &Self::Data) -> Cow<str> {
self.filter_text(data)
}
Expand Down
19 changes: 16 additions & 3 deletions helix-term/src/ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ use tui::{
pub use tui::widgets::{Cell, Row};

use helix_view::{
editor::SmartTabConfig,
editor::{FilePickerConfig, SmartTabConfig},
graphics::{Margin, Rect},
Editor,
};
use tui::layout::Constraint;

pub trait Item: Sync + Send + 'static {
pub trait Item: Sync + Send + Sized + 'static {
/// Additional editor state that is used for label calculation.
type Data: Sync + Send + 'static;
type Config;

fn format(&self, data: &Self::Data) -> Row;

Expand All @@ -36,18 +37,30 @@ pub trait Item: Sync + Send + 'static {
let label: String = self.format(data).cell_text().collect();
label.into()
}

fn goto_parent(_picker: &mut Picker<Self>, _cx: &mut Context) {}
fn goto_child(_picker: &mut Picker<Self>, _cx: &mut Context) {}
}

impl Item for PathBuf {
/// Root prefix to strip.
type Data = PathBuf;
type Config = FilePickerConfig;

fn format(&self, root_path: &Self::Data) -> Row {
self.strip_prefix(root_path)
.unwrap_or(self)
.to_string_lossy()
.into()
}

fn goto_parent(picker: &mut Picker<PathBuf>, cx: &mut Context) {
picker.goto_parent(cx);
}

fn goto_child(picker: &mut Picker<PathBuf>, cx: &mut Context) {
picker.goto_child(cx);
}
}

pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
Expand Down Expand Up @@ -251,7 +264,7 @@ impl<T: Item + PartialEq> Menu<T> {
}
}

use super::PromptEvent as MenuEvent;
use super::{Picker, PromptEvent as MenuEvent};

impl<T: Item + 'static> Component for Menu<T> {
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
Expand Down
Loading

0 comments on commit 2225190

Please sign in to comment.