Skip to content

Commit d2264d8

Browse files
committed
FEAT: add labelled buffer picker
1 parent 0ee5850 commit d2264d8

File tree

4 files changed

+256
-73
lines changed

4 files changed

+256
-73
lines changed

helix-term/src/commands.rs

+156-22
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use helix_view::{
4545
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
4646
editor::Action,
4747
info::Info,
48-
input::KeyEvent,
48+
input::{Event, KeyEvent, KeyModifiers},
4949
keyboard::KeyCode,
5050
theme::Style,
5151
tree,
@@ -58,10 +58,13 @@ use insert::*;
5858
use movement::Movement;
5959

6060
use crate::{
61-
compositor::{self, Component, Compositor},
61+
compositor::{self, Component, Compositor, EventResult},
6262
filter_picker_entry,
6363
job::Callback,
64-
ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent},
64+
ui::{
65+
self, overlay::overlaid, picker::PickerSideEffect, Picker, PickerColumn, Popup, Prompt,
66+
PromptEvent,
67+
},
6568
};
6669

6770
use crate::job::{self, Jobs};
@@ -78,6 +81,7 @@ use std::{
7881

7982
use std::{
8083
borrow::Cow,
84+
iter,
8185
path::{Path, PathBuf},
8286
};
8387

@@ -402,6 +406,7 @@ impl MappableCommand {
402406
file_explorer_in_current_buffer_directory, "Open file explorer at current buffer's directory",
403407
file_explorer_in_current_directory, "Open file explorer at current working directory",
404408
code_action, "Perform code action",
409+
labelled_buffer_picker, "Open labelled buffer picker",
405410
buffer_picker, "Open buffer picker",
406411
jumplist_picker, "Open jumplist picker",
407412
symbol_picker, "Open symbol picker",
@@ -3113,36 +3118,78 @@ fn file_explorer_in_current_directory(cx: &mut Context) {
31133118
}
31143119
}
31153120

3116-
fn buffer_picker(cx: &mut Context) {
3121+
fn iter_newbase(n: u32, base: u32) -> impl Iterator<Item = u32> {
3122+
let mut num = n;
3123+
let mut divisor = 1;
3124+
while divisor * base <= n {
3125+
divisor *= base;
3126+
}
3127+
iter::from_fn(move || {
3128+
if divisor <= 0 {
3129+
return None;
3130+
}
3131+
let digit = num / divisor;
3132+
num %= divisor;
3133+
divisor /= base;
3134+
Some(digit)
3135+
})
3136+
}
3137+
3138+
fn ord_label_nopad(ord: u32, labels: &[char]) -> impl Iterator<Item = char> + use<'_> {
3139+
iter_newbase(ord, labels.len() as u32)
3140+
.map(|i| labels.get(i as usize))
3141+
.filter_map(|o| o)
3142+
.map(|r| *r)
3143+
}
3144+
3145+
fn ord_label(ord: u32, max: u32, labels: &[char]) -> Vec<char> {
3146+
let max_len = ord_label_nopad(max, labels).count();
3147+
let label_nopad: Vec<char> = ord_label_nopad(ord, labels).collect();
3148+
iter::repeat_n(labels[0], max_len - label_nopad.len())
3149+
.chain(label_nopad.into_iter())
3150+
.collect()
3151+
}
3152+
3153+
#[derive(Clone)]
3154+
struct BufferMeta {
3155+
id: DocumentId,
3156+
label: Vec<char>,
3157+
path: Option<PathBuf>,
3158+
is_modified: bool,
3159+
is_current: bool,
3160+
focused_at: std::time::Instant,
3161+
}
3162+
3163+
fn get_buffers(cx: &mut Context) -> Vec<BufferMeta> {
31173164
let current = view!(cx.editor).doc;
31183165

3119-
struct BufferMeta {
3120-
id: DocumentId,
3121-
path: Option<PathBuf>,
3122-
is_modified: bool,
3123-
is_current: bool,
3124-
focused_at: std::time::Instant,
3125-
}
3166+
let labels = &cx.editor.config().buffer_picker.label_alphabet;
31263167

3127-
let new_meta = |doc: &Document| BufferMeta {
3168+
let new_meta = |(i, doc): (usize, &Document)| BufferMeta {
31283169
id: doc.id(),
31293170
path: doc.path().cloned(),
31303171
is_modified: doc.is_modified(),
31313172
is_current: doc.id() == current,
31323173
focused_at: doc.focused_at,
3174+
label: ord_label(i as u32, cx.editor.documents.len() as u32, labels),
31333175
};
31343176

3135-
let mut items = cx
3136-
.editor
3177+
cx.editor
31373178
.documents
31383179
.values()
3180+
.enumerate()
31393181
.map(new_meta)
3140-
.collect::<Vec<BufferMeta>>();
3182+
.collect::<Vec<BufferMeta>>()
3183+
}
31413184

3142-
// mru
3143-
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
3185+
fn get_buffers_mru(cx: &mut Context) -> Vec<BufferMeta> {
3186+
let mut buffers = get_buffers(cx);
3187+
buffers.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
3188+
buffers
3189+
}
31443190

3145-
let columns = [
3191+
fn get_buffer_picker_columns<T>() -> impl IntoIterator<Item = PickerColumn<BufferMeta, T>> {
3192+
[
31463193
PickerColumn::new("id", |meta: &BufferMeta, _| meta.id.to_string().into()),
31473194
PickerColumn::new("flags", |meta: &BufferMeta, _| {
31483195
let mut flags = String::new();
@@ -3165,10 +3212,97 @@ fn buffer_picker(cx: &mut Context) {
31653212
.to_string()
31663213
.into()
31673214
}),
3168-
];
3169-
let picker = Picker::new(columns, 2, items, (), |cx, meta, action| {
3170-
cx.editor.switch(meta.id, action);
3171-
})
3215+
]
3216+
}
3217+
3218+
fn get_labelled_buffer_picker_columns<T>() -> impl IntoIterator<Item = PickerColumn<BufferMeta, T>>
3219+
{
3220+
iter::once(PickerColumn::new("label", |meta: &BufferMeta, _| {
3221+
meta.label.iter().collect::<String>().into()
3222+
}))
3223+
.chain(get_buffer_picker_columns())
3224+
}
3225+
3226+
fn labelled_buffer_picker(cx: &mut Context) {
3227+
let items = get_buffers(cx);
3228+
3229+
let labels = &cx.editor.config().buffer_picker.label_alphabet;
3230+
let max_label = ord_label_nopad(cx.editor.documents.len() as u32, labels).count();
3231+
3232+
let mut chars_read = 0;
3233+
let mut matching: Vec<bool> = iter::repeat_n(true, items.len()).collect();
3234+
3235+
let picker = Picker::new(
3236+
get_labelled_buffer_picker_columns(),
3237+
2,
3238+
items.clone(),
3239+
(),
3240+
|cx, meta, action| {
3241+
cx.editor.switch(meta.id, action);
3242+
},
3243+
)
3244+
.with_text_typing_handler(
3245+
move |event: &Event, cx: &mut compositor::Context| -> (PickerSideEffect, EventResult) {
3246+
if let Event::Key(KeyEvent {
3247+
code: KeyCode::Char(c),
3248+
modifiers: KeyModifiers::NONE,
3249+
}) = event
3250+
{
3251+
chars_read += 1;
3252+
if chars_read > max_label {
3253+
// TODO: raise message that match failed (invalid key sequence)
3254+
chars_read = 0;
3255+
matching.iter_mut().for_each(|v| *v = true);
3256+
return (PickerSideEffect::None, EventResult::Consumed(None));
3257+
}
3258+
let idx = chars_read - 1;
3259+
items.iter().enumerate().for_each(|(i, item)| {
3260+
if *c != item.label[idx] {
3261+
matching[i] = false;
3262+
}
3263+
});
3264+
let nmatches = matching.iter().fold(0, |acc, &c| acc + c as i32);
3265+
if nmatches == 0 {
3266+
// TODO: raise message that match failed (invalid key sequence)
3267+
chars_read = 0;
3268+
matching.iter_mut().for_each(|v| *v = true);
3269+
} else if nmatches == 1 {
3270+
// unique match found
3271+
let match_idx = matching
3272+
.iter()
3273+
.enumerate()
3274+
.find(|(_, c)| **c)
3275+
.map(|(i, _)| i)
3276+
.unwrap();
3277+
cx.editor.switch(items[match_idx].id, Action::Replace);
3278+
return (PickerSideEffect::Close, EventResult::Consumed(None));
3279+
}
3280+
}
3281+
(PickerSideEffect::None, EventResult::Consumed(None))
3282+
},
3283+
)
3284+
.with_preview(|editor, meta| {
3285+
let doc = &editor.documents.get(&meta.id)?;
3286+
let lines = doc.selections().values().next().map(|selection| {
3287+
let cursor_line = selection.primary().cursor_line(doc.text().slice(..));
3288+
(cursor_line, cursor_line)
3289+
});
3290+
Some((meta.id.into(), lines))
3291+
});
3292+
cx.push_layer(Box::new(overlaid(picker)));
3293+
}
3294+
3295+
fn buffer_picker(cx: &mut Context) {
3296+
let items = get_buffers_mru(cx);
3297+
let picker = Picker::new(
3298+
get_buffer_picker_columns(),
3299+
2,
3300+
items,
3301+
(),
3302+
|cx, meta, action| {
3303+
cx.editor.switch(meta.id, action);
3304+
},
3305+
)
31723306
.with_preview(|editor, meta| {
31733307
let doc = &editor.documents.get(&meta.id)?;
31743308
let lines = doc.selections().values().next().map(|selection| {

helix-term/src/keymap/default.rs

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
5757
"p" => goto_previous_buffer,
5858
"k" => move_line_up,
5959
"j" => move_line_down,
60+
"o" => labelled_buffer_picker,
6061
"." => goto_last_modification,
6162
"w" => goto_word,
6263
},

0 commit comments

Comments
 (0)