Skip to content

Flag for the open command to show a single picker for multiple dirs. #13242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 58 additions & 24 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,34 +100,59 @@ fn force_quit(cx: &mut compositor::Context, _args: Args, event: PromptEvent) ->
}

fn open(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
fn parse(arg: Cow<str>) -> (Cow<Path>, Position) {
let (path, pos) = crate::args::parse_file(&arg);
let path = helix_stdx::path::expand_tilde(path);
(path, pos)
}

fn show_picker(cx: &mut compositor::Context, dirs: Vec<PathBuf>) {
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_multiple_roots(editor, dirs);
compositor.push(Box::new(overlaid(picker)));
},
));
Ok(call)
};
cx.jobs.callback(callback);
}
if event != PromptEvent::Validate {
return Ok(());
}

for arg in args {
let (path, pos) = crate::args::parse_file(&arg);
let path = helix_stdx::path::expand_tilde(path);
// 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(editor, path.into_owned());
compositor.push(Box::new(overlaid(picker)));
},
));
Ok(call)
};
cx.jobs.callback(callback);
} else {
// Otherwise, just open the file
let _ = cx.editor.open(&path, Action::Replace)?;
let (view, doc) = current!(cx.editor);
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
doc.set_selection(view.id, pos);
// does not affect opening a buffer without pos
align_view(doc, view, Align::Center);
if args.has_flag("single_picker") {
let dirs: Vec<PathBuf> = args
.into_iter()
.map(|a| {
let path = std::fs::canonicalize(parse(a).0)?;
if !path.is_dir() {
bail!("argument {} is not a directory", path.to_string_lossy());
}
Ok(path)
})
.collect::<anyhow::Result<Vec<_>>>()?;
if !dirs.is_empty() {
show_picker(cx, dirs);
}
} else {
for arg in args {
let (path, pos) = parse(arg);
// 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 dirs = vec![path.into_owned()];
show_picker(cx, dirs);
} else {
// Otherwise, just open the file
let _ = cx.editor.open(&path, Action::Replace)?;
let (view, doc) = current!(cx.editor);
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
doc.set_selection(view.id, pos);
// does not affect opening a buffer without pos
align_view(doc, view, Align::Center);
}
}
}
Ok(())
Expand Down Expand Up @@ -2600,9 +2625,18 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
aliases: &["o", "edit", "e"],
doc: "Open a file from disk into the current view.",
fun: open,
// TODO: Use completers::directory if -s flag is supplied.
completer: CommandCompleter::all(completers::filename),
signature: Signature {
positionals: (1, None),
flags: &[
Flag {
name: "single_picker",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this flag should be kebab-case to be consistent with editor configuration options

alias: Some('s'),
doc: "Show a single picker using multiple root directories.",
..Flag::DEFAULT
},
],
..Signature::DEFAULT
},
},
Expand Down
55 changes: 49 additions & 6 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,21 +193,55 @@ pub struct FilePickerData {
type FilePicker = Picker<PathBuf, FilePickerData>;

pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
let roots = vec![root];
file_picker_multiple_roots(editor, roots)
}

fn longest_common_prefix(paths: &[PathBuf]) -> PathBuf {
if paths.is_empty() {
panic!("Got empty paths list")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error could be handled in other ways, there is no need for panic

}
// Optimize common case.
if paths.len() == 1 {
return paths[0].clone();
}
let mut num_common_components = 0;
let first_path_components = paths[0].components();
// Store path component references in a Vec so we can iterate it multiple times.
let mut all_paths_components: Vec<_> = paths[1..].iter().map(|p| p.components()).collect();
'components: for first_path_component in first_path_components {
for components in &mut all_paths_components {
let component = components.next();
if component.is_none() || component.is_some_and(|c| c != first_path_component) {
break 'components;
}
}
// All paths matched in this component.
num_common_components += 1;
}

paths[0].components().take(num_common_components).collect()
}

pub fn file_picker_multiple_roots(editor: &Editor, roots: Vec<PathBuf>) -> FilePicker {
if roots.is_empty() {
panic!("Expected non-empty argument roots.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be better if this added an error to the status line instead of crashing

}
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant;

let config = editor.config();
let data = FilePickerData {
root: root.clone(),
directory_style: editor.theme.get("ui.text.directory"),
};

let now = Instant::now();

let common_root: PathBuf = longest_common_prefix(&roots);

let mut walk_builder = WalkBuilder::new(&roots[0]);
let dedup_symlinks = config.file_picker.deduplicate_links;
let absolute_root = root.canonicalize().unwrap_or_else(|_| root.clone());
let absolute_root = common_root
.canonicalize()
.unwrap_or_else(|_| roots[0].clone());

let mut walk_builder = WalkBuilder::new(&root);
walk_builder
.hidden(config.file_picker.hidden)
.parents(config.file_picker.parents)
Expand All @@ -220,6 +254,10 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
.max_depth(config.file_picker.max_depth)
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));

for additional_root in &roots[1..] {
walk_builder.add(additional_root);
}

walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
walk_builder.add_custom_ignore_filename(".helix/ignore");

Expand Down Expand Up @@ -264,6 +302,11 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
Spans::from(spans).into()
},
)];

let data = FilePickerData {
root: common_root,
directory_style: editor.theme.get("ui.text.directory"),
};
let picker = Picker::new(columns, 0, [], data, move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
Expand Down