Added previews to opener and searcher

This commit is contained in:
Joshua Barretto 2025-09-29 17:57:07 +01:00
parent 3e8dcdfb11
commit 0337006986
7 changed files with 141 additions and 30 deletions

View file

@ -21,7 +21,7 @@ impl LangPack {
highlighter: Highlighter::default().markdown().git(),
comment_syntax: None,
},
(_, "toml") => Self {
("Cargo.lock", _) | (_, "toml") => Self {
highlighter: Highlighter::default().toml().git(),
comment_syntax: Some(vec!['#', ' ']),
},

View file

@ -834,6 +834,16 @@ impl Buffer {
pub fn end_session(&mut self, cursor_id: CursorId) {
self.cursors.remove(cursor_id);
}
pub fn is_same_path(&self, path: &Path) -> bool {
self.path
.as_ref()
.and_then(|p| p.canonicalize().ok())
.as_ref()
.map_or(false, |p| {
path.canonicalize().ok().map_or(false, |path| *p == path)
})
}
}
// CLassify the character by property
@ -882,10 +892,7 @@ impl State {
}
pub fn open_or_get(&mut self, path: PathBuf) -> Result<BufferId, Error> {
let true_path = path.canonicalize()?;
if let Some((buffer_id, _)) = self.buffers.iter().find(|(_, b)| {
b.path.as_ref().and_then(|p| p.canonicalize().ok()).as_ref() == Some(&true_path)
}) {
if let Some((buffer_id, _)) = self.buffers.iter().find(|(_, b)| b.is_same_path(&path)) {
Ok(buffer_id)
} else {
Ok(self.buffers.insert(Buffer::from_file(path)?))

View file

@ -72,7 +72,7 @@ impl Default for Theme {
fn default() -> Self {
Self {
ui_bg: Color::AnsiValue(235),
select_bg: Color::AnsiValue(23),
select_bg: Color::AnsiValue(8),
line_select_bg: Color::AnsiValue(238),
unfocus_select_bg: Color::AnsiValue(240),
search_result_bg: Color::AnsiValue(60),

View file

@ -61,6 +61,7 @@ impl Input {
event: Event,
) -> Result<Resp, Event> {
buffer.begin_action();
let is_doc = matches!(self.mode, Mode::Doc);
match event.to_action(|e| {
e.to_char()
.map(Action::Char)
@ -84,7 +85,9 @@ impl Input {
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
Some(Action::Move(dir, dist, retain_base, word)) => {
Some(Action::Move(dir, dist, retain_base, word))
if matches!(dir, Dir::Left | Dir::Right) || is_doc =>
{
let dist = match dist {
Dist::Char => [1, 1],
Dist::Page => self.last_area.size().map(|s| s.saturating_sub(3).max(1)),
@ -95,7 +98,7 @@ impl Input {
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
Some(Action::Pan(dir, dist)) => {
Some(Action::Pan(dir, dist)) if is_doc => {
let dist = match dist {
Dist::Char => [1, 1],
Dist::Page => self.last_area.size().map(|s| s.saturating_sub(3).max(1)),
@ -247,11 +250,6 @@ impl Input {
title.as_deref(),
);
let Some(cursor) = buffer.cursors.get(cursor_id) else {
return;
};
let cursor_coord = buffer.text.to_coord(cursor.pos);
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
let margin_w = match self.mode {
Mode::Prompt => 2,
@ -261,6 +259,11 @@ impl Input {
self.last_area = frame.rect([margin_w, 0], [!0, !0]).area();
let Some(cursor) = buffer.cursors.get(cursor_id) else {
return;
};
let cursor_coord = buffer.text.to_coord(cursor.pos);
let mut pos = 0;
for (i, (line_num, (line_pos, line))) in buffer
.text

View file

@ -128,6 +128,10 @@ impl<T> Options<T> {
}
}
pub fn selected(&self) -> Option<&T> {
self.options.get(*self.ranking.get(self.selected)?)
}
pub fn set_options<F: FnMut(&T) -> Option<S>, S: Ord + Copy>(
&mut self,
options: impl IntoIterator<Item = T>,
@ -157,10 +161,10 @@ impl<T> Options<T> {
impl<T: Clone> Element<T> for Options<T> {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> {
match event.to_action(|e| e.to_go().or_else(|| e.to_move())) {
Some(Action::Move(dir, dist, false, false)) => {
Some(Action::Move(dir, dist @ (Dist::Char | Dist::Doc), false, false)) => {
let dist = match dist {
Dist::Char => 1,
Dist::Page => self.last_height.saturating_sub(1).min(self.ranking.len()),
Dist::Page => unimplemented!(),
Dist::Doc => self.ranking.len(),
};
match dir {

View file

@ -282,6 +282,7 @@ pub struct Opener {
pub buffer: Buffer,
pub cursor_id: CursorId,
pub input: Input,
preview: Option<(Buffer, CursorId, Input)>,
}
impl Opener {
@ -297,13 +298,15 @@ impl Opener {
cursor_id,
buffer,
input: Input::filter(),
preview: None,
};
this.update_completions();
this
}
pub fn requested_height(&self) -> usize {
self.options.requested_height() + 3
!0
// self.options.requested_height() * 2 + 3
}
fn set_string(&mut self, s: &str) {
@ -378,7 +381,7 @@ impl Opener {
impl Element<()> for Opener {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
let path_str = self.buffer.text.to_string();
match event.to_action(|e| e.to_cancel().or_else(|| e.to_char().map(Action::Char))) {
let res = match event.to_action(|e| e.to_cancel().or_else(|| e.to_char().map(Action::Char))) {
Some(Action::Cancel) => Ok(Resp::end(None)),
// Backspace removes the entire path segment!
// Only works if we're at the end of the string
@ -409,15 +412,27 @@ impl Element<()> for Opener {
}
Ok(None) => Ok(Resp::handled(None)),
Err(event) => {
let res = self
let res = match self
.input
.handle(&mut self.buffer, self.cursor_id, event)
.map(Resp::into_can_end);
self.update_completions();
.map(Resp::into_can_end)
{
Ok(x) => Ok(x),
Err(event) => if let Some((buffer, cursor_id, input)) = &mut self.preview {
input.handle(buffer, *cursor_id, event).map(Resp::into_can_end)
} else {
Err(event)
},
};
res
}
},
};
if self.buffer.text.to_string() != path_str {
self.update_completions();
}
res
}
}
@ -465,11 +480,40 @@ impl Visual for FileOption {
impl Visual for Opener {
fn render(&mut self, state: &State, frame: &mut Rect) {
self.preview = self.options.selected().and_then(|f| {
self.preview
.take()
.filter(|(b, _, _)| b.is_same_path(&f.path))
.or_else(|| {
let mut buffer = Buffer::from_file(f.path.clone()).ok()?;
let cursor_id = buffer.start_session();
Some((buffer, cursor_id, Input::default()))
})
});
let path_input_sz = 3;
let remaining_sz = frame.size()[1].saturating_sub(path_input_sz);
let (preview_sz, options_sz) = if remaining_sz > 12 {
let preview_sz = remaining_sz / 2;
(preview_sz, remaining_sz - preview_sz)
} else {
(0, remaining_sz)
};
if let Some((buffer, cursor_id, input)) = &mut self.preview {
frame.rect([0, 0], [frame.size()[0], preview_sz]).with(|f| {
input.render(state, buffer.name().as_deref(), buffer, *cursor_id, None, f)
});
}
frame
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
.rect([0, preview_sz], [frame.size()[0], options_sz])
.with(|f| self.options.render(state, f));
frame
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
.rect(
[0, preview_sz + options_sz],
[frame.size()[0], path_input_sz],
)
.with(|f| {
self.input
.render(state, None, &self.buffer, self.cursor_id, None, f)

View file

@ -10,6 +10,7 @@ pub struct Searcher {
buffer: Buffer,
cursor_id: CursorId,
input: Input,
preview: Option<(Buffer, CursorId, Input, SearchResult)>,
}
impl Searcher {
@ -87,11 +88,13 @@ impl Searcher {
cursor_id,
buffer,
input: Input::filter(),
preview: None,
}
}
pub fn requested_height(&self) -> usize {
self.options.requested_height() + 3
!0
// self.options.requested_height() + 3
}
fn update_completions(&mut self) {
@ -113,7 +116,9 @@ impl Searcher {
impl Element<()> for Searcher {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
match event.to_action(|e| e.to_cancel().or_else(|| e.to_char().map(Action::Char))) {
let filter_str = self.buffer.text.to_string();
let res = match event.to_action(|e| e.to_cancel().or_else(|| e.to_char().map(Action::Char)))
{
Some(Action::Cancel) => Ok(Resp::end(None)),
_ => match self.options.handle(state, event).map(Resp::into_ended) {
// Selecting a directory enters the directory
@ -123,19 +128,35 @@ impl Element<()> for Searcher {
))))),
Ok(None) => Ok(Resp::handled(None)),
Err(event) => {
let res = self
let res = match self
.input
.handle(&mut self.buffer, self.cursor_id, event)
.map(Resp::into_can_end);
self.update_completions();
.map(Resp::into_can_end)
{
Ok(x) => Ok(x),
Err(event) => {
if let Some((buffer, cursor_id, input, _)) = &mut self.preview {
input
.handle(buffer, *cursor_id, event)
.map(Resp::into_can_end)
} else {
Err(event)
}
}
};
res
}
},
};
if self.buffer.text.to_string() != filter_str {
self.update_completions();
}
res
}
}
#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct SearchResult {
pub path: PathBuf,
pub line_idx: usize,
@ -162,11 +183,43 @@ impl Visual for SearchResult {
impl Visual for Searcher {
fn render(&mut self, state: &State, frame: &mut Rect) {
let path_input_sz = 3;
let remaining_sz = frame.size()[1].saturating_sub(path_input_sz);
let (preview_sz, options_sz) = if remaining_sz > 12 {
let preview_sz = remaining_sz / 2;
(preview_sz, remaining_sz - preview_sz)
} else {
(0, remaining_sz)
};
self.preview = self.options.selected().and_then(|result| {
self.preview
.take()
.filter(|(_, _, _, r)| r == result)
.or_else(|| {
let mut buffer = Buffer::from_file(result.path.clone()).ok()?;
let cursor_id = buffer.start_session();
let mut input = Input::default();
buffer.goto_cursor(cursor_id, [0, result.line_idx as isize], true);
input.focus([0, result.line_idx as isize - preview_sz as isize / 2]);
Some((buffer, cursor_id, input, result.clone()))
})
});
if let Some((buffer, cursor_id, input, result)) = &mut self.preview {
frame.rect([0, 0], [frame.size()[0], preview_sz]).with(|f| {
input.render(state, buffer.name().as_deref(), buffer, *cursor_id, None, f)
});
}
frame
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
.rect([0, preview_sz], [frame.size()[0], options_sz])
.with(|f| self.options.render(state, f));
frame
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
.rect(
[0, preview_sz + options_sz],
[frame.size()[0], path_input_sz],
)
.with(|f| {
let title = format!(
"{} of {} results for '{}' in {}/",