Skip to content

Commit 5c7a960

Browse files
committed
FEAT: Add labelled buffer picker
1 parent 0ee5850 commit 5c7a960

File tree

5 files changed

+263
-73
lines changed

5 files changed

+263
-73
lines changed

helix-term/src/commands.rs

+157-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,79 @@ 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> + '_ {
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(labels[0])
3149+
.take(max_len - label_nopad.len())
3150+
.chain(label_nopad.into_iter())
3151+
.collect()
3152+
}
3153+
3154+
#[derive(Clone)]
3155+
struct BufferMeta {
3156+
id: DocumentId,
3157+
label: Vec<char>,
3158+
path: Option<PathBuf>,
3159+
is_modified: bool,
3160+
is_current: bool,
3161+
focused_at: std::time::Instant,
3162+
}
3163+
3164+
fn get_buffers(cx: &mut Context) -> Vec<BufferMeta> {
31173165
let current = view!(cx.editor).doc;
31183166

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-
}
3167+
let labels = &cx.editor.config().buffer_picker.label_alphabet;
31263168

3127-
let new_meta = |doc: &Document| BufferMeta {
3169+
let new_meta = |(i, doc): (usize, &Document)| BufferMeta {
31283170
id: doc.id(),
31293171
path: doc.path().cloned(),
31303172
is_modified: doc.is_modified(),
31313173
is_current: doc.id() == current,
31323174
focused_at: doc.focused_at,
3175+
label: ord_label(i as u32, cx.editor.documents.len() as u32, labels),
31333176
};
31343177

3135-
let mut items = cx
3136-
.editor
3178+
cx.editor
31373179
.documents
31383180
.values()
3181+
.enumerate()
31393182
.map(new_meta)
3140-
.collect::<Vec<BufferMeta>>();
3183+
.collect::<Vec<BufferMeta>>()
3184+
}
31413185

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

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