Skip to content

Commit 28022f3

Browse files
committed
Flag for the open command to show a single picker for multiple dirs.
- The picker root is the common prefix path of all directories. - Shows an error if non-directories are supplied. - Flag naming seemed difficult, but I ended up with "single_picker" (-s). Happy to change that if someone has a better idea. Future work: - multiplex between filename and directory completers based on flag presence The main motivation for this are large mono-repos, where each developer cares about a certain subset of directories that they tend to work in, which are not under a common root. This was previously discussed in #11589, but flags seem like an obvious and better alternative to the ideas presented in #11589 (comment).
1 parent db187c4 commit 28022f3

File tree

2 files changed

+104
-30
lines changed

2 files changed

+104
-30
lines changed

helix-term/src/commands/typed.rs

+55-24
Original file line numberDiff line numberDiff line change
@@ -100,34 +100,56 @@ fn force_quit(cx: &mut compositor::Context, _args: Args, event: PromptEvent) ->
100100
}
101101

102102
fn open(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
103+
fn parse(arg:Cow<str>) ->(Cow<Path>, Position) {
104+
let (path, pos) = crate::args::parse_file(&arg);
105+
let path = helix_stdx::path::expand_tilde(path);
106+
(path, pos)
107+
}
108+
109+
fn show_picker(cx: &mut compositor::Context, dirs: Vec<PathBuf>) {
110+
let callback = async move {
111+
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
112+
move |editor: &mut Editor, compositor: &mut Compositor| {
113+
let picker = ui::file_picker_multiple_roots(editor, dirs);
114+
compositor.push(Box::new(overlaid(picker)));
115+
},
116+
));
117+
Ok(call)
118+
};
119+
cx.jobs.callback(callback);
120+
}
103121
if event != PromptEvent::Validate {
104122
return Ok(());
105123
}
106124

107-
for arg in args {
108-
let (path, pos) = crate::args::parse_file(&arg);
109-
let path = helix_stdx::path::expand_tilde(path);
110-
// If the path is a directory, open a file picker on that directory and update the status
111-
// message
112-
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
113-
let callback = async move {
114-
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
115-
move |editor: &mut Editor, compositor: &mut Compositor| {
116-
let picker = ui::file_picker(editor, path.into_owned());
117-
compositor.push(Box::new(overlaid(picker)));
118-
},
119-
));
120-
Ok(call)
121-
};
122-
cx.jobs.callback(callback);
123-
} else {
124-
// Otherwise, just open the file
125-
let _ = cx.editor.open(&path, Action::Replace)?;
126-
let (view, doc) = current!(cx.editor);
127-
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
128-
doc.set_selection(view.id, pos);
129-
// does not affect opening a buffer without pos
130-
align_view(doc, view, Align::Center);
125+
if args.has_flag("single_picker") {
126+
let dirs: Vec<PathBuf> = args.into_iter().map(|a| {
127+
let path = std::fs::canonicalize(&parse(a).0)?;
128+
if !path.is_dir() {
129+
bail!("argument {} is not a directory", path.to_string_lossy());
130+
}
131+
Ok(path)
132+
}).collect::<anyhow::Result<Vec<_>>>()?;
133+
if !dirs.is_empty() {
134+
show_picker(cx, dirs);
135+
}
136+
} else {
137+
for arg in args {
138+
let (path, pos) = parse(arg);
139+
// If the path is a directory, open a file picker on that directory and update the
140+
// status message
141+
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
142+
let dirs = vec![path.into_owned()];
143+
show_picker(cx, dirs);
144+
} else {
145+
// Otherwise, just open the file
146+
let _ = cx.editor.open(&path, Action::Replace)?;
147+
let (view, doc) = current!(cx.editor);
148+
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
149+
doc.set_selection(view.id, pos);
150+
// does not affect opening a buffer without pos
151+
align_view(doc, view, Align::Center);
152+
}
131153
}
132154
}
133155
Ok(())
@@ -2600,9 +2622,18 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
26002622
aliases: &["o", "edit", "e"],
26012623
doc: "Open a file from disk into the current view.",
26022624
fun: open,
2625+
// TODO: Use completers::directory if -s flag is supplied.
26032626
completer: CommandCompleter::all(completers::filename),
26042627
signature: Signature {
26052628
positionals: (1, None),
2629+
flags: &[
2630+
Flag {
2631+
name: "single_picker",
2632+
alias: Some('s'),
2633+
doc: "Show a single picker using multiple root directories.",
2634+
..Flag::DEFAULT
2635+
},
2636+
],
26062637
..Signature::DEFAULT
26072638
},
26082639
},

helix-term/src/ui/mod.rs

+49-6
Original file line numberDiff line numberDiff line change
@@ -193,21 +193,54 @@ pub struct FilePickerData {
193193
type FilePicker = Picker<PathBuf, FilePickerData>;
194194

195195
pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
196+
let roots = vec![root];
197+
file_picker_multiple_roots(editor, roots)
198+
}
199+
200+
fn longest_common_prefix(paths: &Vec<PathBuf>) -> PathBuf {
201+
if paths.is_empty() {
202+
panic!("Got empty paths list")
203+
}
204+
// Optimize common case.
205+
if paths.len() == 1 {
206+
return paths[0].clone();
207+
}
208+
let mut num_common_components = 0;
209+
let mut first_path_components = paths[0].components();
210+
// Store path component references in a Vec so we can iterate it multiple times.
211+
let mut all_paths_components: Vec<_> = paths[1..].into_iter().map(|p|p.components()).collect();
212+
'components: while let Some(first_path_component) = first_path_components.next() {
213+
for components in &mut all_paths_components {
214+
let component = components.next();
215+
if component.is_none() || component.is_some_and(|c| c!= first_path_component) {
216+
break 'components;
217+
}
218+
}
219+
// All paths matched in this component.
220+
num_common_components += 1;
221+
}
222+
223+
paths[0].components().take(num_common_components).collect()
224+
}
225+
226+
pub fn file_picker_multiple_roots(editor: &Editor, roots: Vec<PathBuf>) -> FilePicker {
227+
if roots.is_empty() {
228+
panic!("Expected non-empty argument roots.")
229+
230+
}
196231
use ignore::{types::TypesBuilder, WalkBuilder};
197232
use std::time::Instant;
198233

199234
let config = editor.config();
200-
let data = FilePickerData {
201-
root: root.clone(),
202-
directory_style: editor.theme.get("ui.text.directory"),
203-
};
204235

205236
let now = Instant::now();
206237

238+
let common_root: PathBuf = longest_common_prefix(&roots);
239+
240+
let mut walk_builder = WalkBuilder::new(&roots[0]);
207241
let dedup_symlinks = config.file_picker.deduplicate_links;
208-
let absolute_root = root.canonicalize().unwrap_or_else(|_| root.clone());
242+
let absolute_root = common_root.canonicalize().unwrap_or_else(|_| roots[0].clone());
209243

210-
let mut walk_builder = WalkBuilder::new(&root);
211244
walk_builder
212245
.hidden(config.file_picker.hidden)
213246
.parents(config.file_picker.parents)
@@ -220,6 +253,11 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
220253
.max_depth(config.file_picker.max_depth)
221254
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
222255

256+
for additional_root in &roots[1..] {
257+
walk_builder.add(additional_root);
258+
}
259+
260+
223261
walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
224262
walk_builder.add_custom_ignore_filename(".helix/ignore");
225263

@@ -264,6 +302,11 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
264302
Spans::from(spans).into()
265303
},
266304
)];
305+
306+
let data = FilePickerData {
307+
root: common_root,
308+
directory_style: editor.theme.get("ui.text.directory"),
309+
};
267310
let picker = Picker::new(columns, 0, [], data, move |cx, path: &PathBuf, action| {
268311
if let Err(e) = cx.editor.open(path, action) {
269312
let err = if let Some(err) = e.source() {

0 commit comments

Comments
 (0)