Compare commits

...

6 commits

12 changed files with 265 additions and 110 deletions

View file

@ -17,20 +17,20 @@ pub enum Dir {
#[derive(Clone, Debug)]
pub enum Action {
Char(char), // Insert a character
Indent(bool), // Indent (indent vs deindent)
Move(Dir, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word)
Pan(Dir, Dist), // Pan the view window
PaneMove(Dir), // Move panes
PaneOpen(Dir), // Create a new pane
PaneClose, // Close the current pane
Cancel, // Cancels the current action
Continue, // Continue past an info-only element (like a help screen)
Go, // Search, accept, or select the current option
Yes, // A binary confirmation is answered 'yes'
No, // A binary confirmation is answered 'no'
Quit, // Quit the application
OpenPrompt, // Open the command prompt
Char(char), // Insert a character
Indent(bool), // Indent (indent vs deindent)
Move(Dir, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word)
Pan(Dir, Dist), // Pan the view window
PaneMove(Dir), // Move panes
PaneOpen(Dir), // Create a new pane
PaneClose, // Close the current pane
Cancel, // Cancels the current action
Continue, // Continue past an info-only element (like a help screen)
Go, // Search, accept, or select the current option
Yes, // A binary confirmation is answered 'yes'
No, // A binary confirmation is answered 'no'
Quit, // Quit the application
OpenPrompt, // Open the command prompt
Show(Option<String>, String), // Display an optionally titled informational text box to the user
OpenSwitcher, // Open the buffer switcher
OpenOpener(PathBuf), // Open the file opener
@ -45,7 +45,7 @@ pub enum Action {
SelectToken, // Fully select the token under the cursor
SelectAll, // Fully select the entire input
Save, // Save the current buffer
Mouse(MouseAction, [isize; 2], bool), // (action, pos, is_ctrl)
Mouse(MouseAction, [isize; 2], bool, usize), // (action, pos, is_ctrl, drag_id)
Undo,
Redo,
Copy,
@ -495,7 +495,7 @@ impl RawEvent {
}
}
pub fn to_mouse(&self, area: Area) -> Option<Action> {
pub fn to_mouse(&self, area: Area, drag_id_counter: &mut usize) -> Option<Action> {
let TerminalEvent::Mouse(ev) = self.0 else {
return None;
};
@ -504,12 +504,15 @@ impl RawEvent {
let action = match ev.kind {
MouseEventKind::ScrollUp => MouseAction::ScrollUp,
MouseEventKind::ScrollDown => MouseAction::ScrollDown,
MouseEventKind::Down(MouseButton::Left) => MouseAction::Click,
MouseEventKind::Down(MouseButton::Left) => {
*drag_id_counter += 1;
MouseAction::Click
}
MouseEventKind::Drag(MouseButton::Left) => MouseAction::Drag,
_ => return None,
};
let is_ctrl = ev.modifiers == KeyModifiers::CONTROL;
Some(Action::Mouse(action, pos, is_ctrl))
Some(Action::Mouse(action, pos, is_ctrl, *drag_id_counter))
} else {
None
}

View file

@ -1,4 +1,4 @@
use std::{ops::Range, path::Path};
use std::ops::Range;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TokenKind {
@ -58,7 +58,7 @@ impl Highlighter {
Self { entries, matchers }
}
pub fn with(mut self, token: TokenKind, p: impl AsRef<str>) -> Self {
pub fn with(self, token: TokenKind, p: impl AsRef<str>) -> Self {
self.with_many([(token, p)])
}
@ -74,7 +74,7 @@ impl Highlighter {
self
}
fn highlight_str(&self, mut s: &[char]) -> Vec<Token> {
fn highlight_str(&self, s: &[char]) -> Vec<Token> {
let mut tokens = Vec::new();
let mut i = 0;
loop {
@ -325,6 +325,8 @@ impl Regex {
postfix(1, just('*'), |r, _, _| Self::Many(0, !0, Box::new(r))),
postfix(1, just('+'), |r, _, _| Self::Many(1, !0, Box::new(r))),
postfix(1, just('?'), |r, _, _| Self::Many(0, 1, Box::new(r))),
// Non-standard: match the lhs, then rewind the input (i.e: as if it had never been parsed).
// Most useful at the end of tokens for context-sensitivie behaviour. For example, differentiating idents and function calls
postfix(1, just('%'), |r, _, _| Self::Rewind(Box::new(r))),
// Non-standard: `x@y` parses `x` and then `y`. `y` can use `~` to refer to the extra string that was
// parsed by `x`. This supports nesting and is intended for context-sensitive patterns like Rust raw

View file

@ -1,4 +1,3 @@
use super::*;
use crate::highlight::{Highlighter, TokenKind};
use std::path::Path;
@ -22,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

@ -212,13 +212,11 @@ impl Buffer {
}
pub fn save(&mut self) -> Result<(), Error> {
if self.unsaved {
std::fs::write(
self.path.as_ref().expect("buffer must have path to save"),
self.text.to_string(),
)?;
self.unsaved = false;
}
std::fs::write(
self.path.as_ref().expect("buffer must have path to save"),
self.text.to_string(),
)?;
self.unsaved = false;
Ok(())
}
@ -836,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
@ -884,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

@ -80,11 +80,11 @@ impl<'a> Rect<'a> {
}
}
pub fn with<R>(&mut self, f: impl FnOnce(&mut Rect) -> R) -> R {
pub fn with<R>(&mut self, f: impl FnOnce(&mut Rect<'_>) -> R) -> R {
f(self)
}
pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect {
pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect<'_> {
Rect {
area: Area {
origin: [
@ -103,7 +103,7 @@ impl<'a> Rect<'a> {
}
}
pub fn with_border(&mut self, theme: &theme::BorderTheme, title: Option<&str>) -> Rect {
pub fn with_border(&mut self, theme: &theme::BorderTheme, title: Option<&str>) -> Rect<'_> {
let edge = self.size().map(|e| e.saturating_sub(1));
for col in 0..edge[0] {
self.get_mut([col, 0]).map(|c| {
@ -157,7 +157,7 @@ impl<'a> Rect<'a> {
self.rect([1, 1], self.size().map(|e| e.saturating_sub(2)))
}
pub fn with_fg(&mut self, fg: Color) -> Rect {
pub fn with_fg(&mut self, fg: Color) -> Rect<'_> {
Rect {
fg,
bg: self.bg,
@ -167,7 +167,7 @@ impl<'a> Rect<'a> {
}
}
pub fn with_bg(&mut self, bg: Color) -> Rect {
pub fn with_bg(&mut self, bg: Color) -> Rect<'_> {
Rect {
fg: self.fg,
bg,
@ -177,7 +177,7 @@ impl<'a> Rect<'a> {
}
}
pub fn with_focus(&mut self, focus: bool) -> Rect {
pub fn with_focus(&mut self, focus: bool) -> Rect<'_> {
Rect {
fg: self.fg,
bg: self.bg,
@ -199,7 +199,7 @@ impl<'a> Rect<'a> {
self.area.size.map(|e| e as usize)
}
pub fn fill(&mut self, c: char) -> Rect {
pub fn fill(&mut self, c: char) -> Rect<'_> {
for row in 0..self.size()[1] {
for col in 0..self.size()[0] {
let cell = Cell {
@ -215,7 +215,7 @@ impl<'a> Rect<'a> {
self.rect([0, 0], self.size())
}
pub fn text(&mut self, origin: [isize; 2], text: &str) -> Rect {
pub fn text(&mut self, origin: [isize; 2], text: &str) -> Rect<'_> {
for (idx, c) in text.chars().enumerate() {
if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0
{
@ -234,7 +234,7 @@ impl<'a> Rect<'a> {
self.rect([0, 0], self.size())
}
pub fn set_cursor(&mut self, cursor: [isize; 2], style: CursorStyle) -> Rect {
pub fn set_cursor(&mut self, cursor: [isize; 2], style: CursorStyle) -> Rect<'_> {
if self.has_focus
&& (0..=self.size()[0] as isize).contains(&cursor[0])
&& (0..self.size()[1] as isize).contains(&cursor[1])
@ -264,7 +264,7 @@ pub struct Framebuffer {
}
impl Framebuffer {
pub fn rect(&mut self) -> Rect {
pub fn rect(&mut self) -> Rect<'_> {
Rect {
fg: Color::Reset,
bg: Color::Reset,

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

@ -1,9 +1,6 @@
use super::*;
use crate::{
state::{Buffer, BufferId, Cursor, CursorId},
terminal::CursorStyle,
};
use std::{collections::HashMap, path::PathBuf};
use crate::state::{Buffer, BufferId, Cursor, CursorId};
use std::collections::HashMap;
pub struct Doc {
buffer: BufferId,

View file

@ -19,6 +19,8 @@ pub struct Input {
pub focus: [isize; 2],
// Remember the last area for things like scrolling
pub last_area: Area,
pub last_scroll_pos: Option<([isize; 2], usize, usize)>,
pub scroll_grab: Option<(usize, isize)>,
}
impl Input {
@ -59,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)
@ -67,7 +70,6 @@ impl Input {
.or_else(|| e.to_select_token())
.or_else(|| e.to_select_all())
.or_else(|| e.to_indent())
.or_else(|| e.to_mouse(self.last_area))
.or_else(|| e.to_edit())
}) {
Some(Action::Char(c)) => {
@ -83,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)),
@ -94,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)),
@ -130,22 +134,42 @@ impl Input {
buffer.select_all_cursor(cursor_id);
Ok(Resp::handled(None))
}
Some(Action::Mouse(MouseAction::Click, pos, false)) => {
let pos = [self.focus[0] + pos[0], self.focus[1] + pos[1]];
// If we're already in the right place, select the token instead
if let Some(cursor) = buffer.cursors.get(cursor_id)
&& cursor.selection().is_none()
&& buffer.text.to_coord(cursor.pos) == pos
Some(Action::Mouse(MouseAction::Click, pos, false, drag_id)) => {
if let Some((scroll_pos, h, _)) = self.last_scroll_pos
&& scroll_pos[0] == pos[0]
&& (scroll_pos[1]..=scroll_pos[1] + h as isize).contains(&pos[1])
{
buffer.select_token_cursor(cursor_id);
} else {
buffer.goto_cursor(cursor_id, pos, true);
self.scroll_grab = Some((drag_id, pos[1] - scroll_pos[1]));
} else if let Some(pos) = self.last_area.contains(pos) {
let pos = [self.focus[0] + pos[0], self.focus[1] + pos[1]];
// If we're already in the right place, select the token instead
if let Some(cursor) = buffer.cursors.get(cursor_id)
&& cursor.selection().is_none()
&& buffer.text.to_coord(cursor.pos) == pos
{
buffer.select_token_cursor(cursor_id);
} else {
buffer.goto_cursor(cursor_id, pos, true);
}
}
Ok(Resp::handled(None))
}
Some(Action::Mouse(MouseAction::Drag, pos, false, drag_id))
if self.scroll_grab.map_or(false, |(di, _)| di == drag_id) =>
{
if let Some(pos) = self.last_area.contains(pos)
&& let Some((_, offset)) = self.scroll_grab
&& let Some((_, scroll_sz, frame_sz)) = self.last_scroll_pos
{
self.focus[1] = ((pos[1] - offset).max(0) as usize
* buffer.text.lines().count()
/ frame_sz) as isize;
}
Ok(Resp::handled(None))
}
Some(
Action::Mouse(MouseAction::Drag, pos, false)
| Action::Mouse(MouseAction::Click, pos, true),
Action::Mouse(MouseAction::Drag, pos, false, _)
| Action::Mouse(MouseAction::Click, pos, true, _),
) => {
buffer.goto_cursor(
cursor_id,
@ -226,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,
@ -240,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
@ -342,18 +366,19 @@ impl Input {
let line_count = buffer.text.lines().count();
let frame_sz = outer_frame.size()[1].saturating_sub(2).max(1);
let scroll_sz = (frame_sz * frame_sz / line_count).max(1).min(frame_sz);
if scroll_sz != frame_sz {
self.last_scroll_pos = if scroll_sz != frame_sz {
let lines2 = line_count.saturating_sub(frame_sz).max(1);
let offset = frame_sz.saturating_sub(scroll_sz)
* (self.focus[1].max(0) as usize).min(lines2)
/ lines2;
let pos = [outer_frame.size()[0].saturating_sub(1), 1 + offset];
outer_frame
.rect(
[outer_frame.size()[0].saturating_sub(1), 1 + offset],
[1, scroll_sz],
)
.rect(pos, [1, scroll_sz])
.with_bg(Color::White)
.fill(' ');
}
Some((pos.map(|e| e as isize), scroll_sz, frame_sz))
} else {
None
};
}
}

View file

@ -9,7 +9,7 @@ mod status;
pub use self::{
doc::{Doc, Finder},
input::Input,
panes::{Pane, Panes},
panes::Panes,
prompt::{Confirm, Opener, Prompt, Show, Switcher},
root::Root,
search::Searcher,
@ -64,7 +64,7 @@ impl<End> Resp<End> {
pub fn is_end(&self) -> bool {
self.ended.is_some()
}
pub fn into_ended(mut self) -> Option<End> {
pub fn into_ended(self) -> Option<End> {
self.ended
}
}
@ -113,6 +113,7 @@ pub struct Options<T> {
// (score, option)
pub options: Vec<T>,
pub ranking: Vec<usize>,
pub last_height: usize,
}
impl<T> Options<T> {
@ -123,13 +124,18 @@ impl<T> Options<T> {
selected: 0,
options,
ranking,
last_height: 0,
}
}
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>,
mut f: F,
f: F,
) {
self.options = options.into_iter().collect();
self.apply_scoring(f);
@ -155,13 +161,28 @@ 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::Char, false, false)) => {
Some(Action::Move(dir, dist @ (Dist::Char | Dist::Doc), false, false)) => {
let dist = match dist {
Dist::Char => 1,
Dist::Page => unimplemented!(),
Dist::Doc => self.ranking.len(),
};
match dir {
Dir::Up => {
self.selected = (self.selected + self.ranking.len()).saturating_sub(1)
% self.ranking.len().max(1)
if self.selected == 0 {
self.selected = self.ranking.len().saturating_sub(1);
} else {
self.selected = self.selected.saturating_sub(dist);
}
}
Dir::Down => {
if self.selected == self.ranking.len().saturating_sub(1) {
self.selected = 0;
} else {
self.selected =
(self.selected + dist).min(self.ranking.len().saturating_sub(1));
}
}
Dir::Down => self.selected = (self.selected + 1) % self.ranking.len().max(1),
_ => return Err(event),
}
Ok(Resp::handled(None))
@ -192,6 +213,8 @@ impl<T: Visual> Visual for Options<T> {
None,
);
self.last_height = frame.size()[1];
self.focus = self
.focus
.max(

View file

@ -15,6 +15,7 @@ pub struct Panes {
selected: usize,
panes: Vec<Pane>,
last_area: Area,
drag_id_counter: usize,
}
impl Panes {
@ -29,6 +30,7 @@ impl Panes {
})
.collect(),
last_area: Default::default(),
drag_id_counter: 0,
}
}
@ -44,7 +46,7 @@ impl Element for Panes {
.map(Action::PaneMove)
.or_else(|| e.to_pane_open().map(Action::PaneOpen))
.or_else(|| e.to_pane_close())
.or_else(|| e.to_mouse(self.last_area))
.or_else(|| e.to_mouse(self.last_area, &mut self.drag_id_counter))
}) {
Some(Action::PaneMove(Dir::Left)) => {
self.selected = (self.selected + self.panes.len() - 1) % self.panes.len();
@ -86,14 +88,14 @@ impl Element for Panes {
self.selected = new_idx;
Ok(Resp::handled(None))
}
Some(Action::Mouse(action, pos, _)) => {
Some(ref action @ Action::Mouse(ref m_action, pos, _, _)) => {
for (i, pane) in self.panes.iter_mut().enumerate() {
if pane.last_area.contains(pos).is_some() {
if matches!(action, MouseAction::Click) {
if matches!(m_action, MouseAction::Click) {
self.selected = i;
}
match &mut pane.kind {
PaneKind::Doc(doc) => return doc.handle(state, event),
PaneKind::Doc(doc) => return doc.handle(state, action.clone().into()),
PaneKind::Empty => {}
}
}

View file

@ -54,9 +54,9 @@ impl Prompt {
- 1;
Ok(Action::GotoLine(line))
}
Some("search") => {
let needle = args.next().ok_or_else(|| "Expected argument".to_string())?;
Ok(Action::BeginSearch(needle.to_string()))
Some(arg0 @ "search") => {
let needle = cmd.get(arg0.len()..).unwrap().trim().to_string();
Ok(Action::BeginSearch(needle))
}
Some(cmd) => Err(format!("Unknown command `{cmd}`")),
None => Err(format!("No command entered")),
@ -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) {
@ -325,11 +328,12 @@ impl Opener {
let options = entries
.filter_map(|e| e.ok())
.filter_map(|entry| {
let metadata = fs::metadata(entry.path()).ok()?;
Some(FileOption {
path: entry.path(),
kind: if entry.file_type().ok()?.is_dir() {
kind: if metadata.file_type().is_dir() {
FileKind::Dir
} else if entry.file_type().ok()?.is_file() {
} else if metadata.file_type().is_file() {
FileKind::File
} else {
FileKind::Unknown
@ -377,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
@ -408,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
}
}
@ -442,11 +458,12 @@ impl Visual for FileOption {
Some(name) => format!("{name}"),
None => format!("Unknown"),
};
let is_link = if self.is_link { " (symlink)" } else { "" };
let desc = match self.kind {
FileKind::Dir => "Directory",
FileKind::Unknown => "Unknown filesystem item",
FileKind::File => "File",
FileKind::New => "Create new file",
FileKind::Dir => format!("Directory{is_link}"),
FileKind::Unknown => format!("Unknown{is_link}"),
FileKind::File => format!("File{is_link}"),
FileKind::New => format!("Create new file{is_link}"),
};
frame
.with_fg(match self.kind {
@ -463,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 {}/",