Compare commits

...

6 commits

12 changed files with 265 additions and 110 deletions

View file

@ -45,7 +45,7 @@ pub enum Action {
SelectToken, // Fully select the token under the cursor SelectToken, // Fully select the token under the cursor
SelectAll, // Fully select the entire input SelectAll, // Fully select the entire input
Save, // Save the current buffer 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, Undo,
Redo, Redo,
Copy, 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 { let TerminalEvent::Mouse(ev) = self.0 else {
return None; return None;
}; };
@ -504,12 +504,15 @@ impl RawEvent {
let action = match ev.kind { let action = match ev.kind {
MouseEventKind::ScrollUp => MouseAction::ScrollUp, MouseEventKind::ScrollUp => MouseAction::ScrollUp,
MouseEventKind::ScrollDown => MouseAction::ScrollDown, 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, MouseEventKind::Drag(MouseButton::Left) => MouseAction::Drag,
_ => return None, _ => return None,
}; };
let is_ctrl = ev.modifiers == KeyModifiers::CONTROL; 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 { } else {
None None
} }

View file

@ -1,4 +1,4 @@
use std::{ops::Range, path::Path}; use std::ops::Range;
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum TokenKind { pub enum TokenKind {
@ -58,7 +58,7 @@ impl Highlighter {
Self { entries, matchers } 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)]) self.with_many([(token, p)])
} }
@ -74,7 +74,7 @@ impl Highlighter {
self 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 tokens = Vec::new();
let mut i = 0; let mut i = 0;
loop { loop {
@ -325,6 +325,8 @@ impl Regex {
postfix(1, just('*'), |r, _, _| Self::Many(0, !0, Box::new(r))), 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(1, !0, Box::new(r))),
postfix(1, just('?'), |r, _, _| Self::Many(0, 1, 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))), 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 // 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 // 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 crate::highlight::{Highlighter, TokenKind};
use std::path::Path; use std::path::Path;
@ -22,7 +21,7 @@ impl LangPack {
highlighter: Highlighter::default().markdown().git(), highlighter: Highlighter::default().markdown().git(),
comment_syntax: None, comment_syntax: None,
}, },
(_, "toml") => Self { ("Cargo.lock", _) | (_, "toml") => Self {
highlighter: Highlighter::default().toml().git(), highlighter: Highlighter::default().toml().git(),
comment_syntax: Some(vec!['#', ' ']), comment_syntax: Some(vec!['#', ' ']),
}, },

View file

@ -212,13 +212,11 @@ impl Buffer {
} }
pub fn save(&mut self) -> Result<(), Error> { pub fn save(&mut self) -> Result<(), Error> {
if self.unsaved {
std::fs::write( std::fs::write(
self.path.as_ref().expect("buffer must have path to save"), self.path.as_ref().expect("buffer must have path to save"),
self.text.to_string(), self.text.to_string(),
)?; )?;
self.unsaved = false; self.unsaved = false;
}
Ok(()) Ok(())
} }
@ -836,6 +834,16 @@ impl Buffer {
pub fn end_session(&mut self, cursor_id: CursorId) { pub fn end_session(&mut self, cursor_id: CursorId) {
self.cursors.remove(cursor_id); 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 // CLassify the character by property
@ -884,10 +892,7 @@ impl State {
} }
pub fn open_or_get(&mut self, path: PathBuf) -> Result<BufferId, Error> { 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.is_same_path(&path)) {
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)
}) {
Ok(buffer_id) Ok(buffer_id)
} else { } else {
Ok(self.buffers.insert(Buffer::from_file(path)?)) 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) 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 { Rect {
area: Area { area: Area {
origin: [ 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)); let edge = self.size().map(|e| e.saturating_sub(1));
for col in 0..edge[0] { for col in 0..edge[0] {
self.get_mut([col, 0]).map(|c| { 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))) 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 { Rect {
fg, fg,
bg: self.bg, 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 { Rect {
fg: self.fg, fg: self.fg,
bg, 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 { Rect {
fg: self.fg, fg: self.fg,
bg: self.bg, bg: self.bg,
@ -199,7 +199,7 @@ impl<'a> Rect<'a> {
self.area.size.map(|e| e as usize) 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 row in 0..self.size()[1] {
for col in 0..self.size()[0] { for col in 0..self.size()[0] {
let cell = Cell { let cell = Cell {
@ -215,7 +215,7 @@ impl<'a> Rect<'a> {
self.rect([0, 0], self.size()) 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() { for (idx, c) in text.chars().enumerate() {
if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0 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()) 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 if self.has_focus
&& (0..=self.size()[0] as isize).contains(&cursor[0]) && (0..=self.size()[0] as isize).contains(&cursor[0])
&& (0..self.size()[1] as isize).contains(&cursor[1]) && (0..self.size()[1] as isize).contains(&cursor[1])
@ -264,7 +264,7 @@ pub struct Framebuffer {
} }
impl Framebuffer { impl Framebuffer {
pub fn rect(&mut self) -> Rect { pub fn rect(&mut self) -> Rect<'_> {
Rect { Rect {
fg: Color::Reset, fg: Color::Reset,
bg: Color::Reset, bg: Color::Reset,

View file

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

View file

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

View file

@ -19,6 +19,8 @@ pub struct Input {
pub focus: [isize; 2], pub focus: [isize; 2],
// Remember the last area for things like scrolling // Remember the last area for things like scrolling
pub last_area: Area, pub last_area: Area,
pub last_scroll_pos: Option<([isize; 2], usize, usize)>,
pub scroll_grab: Option<(usize, isize)>,
} }
impl Input { impl Input {
@ -59,6 +61,7 @@ impl Input {
event: Event, event: Event,
) -> Result<Resp, Event> { ) -> Result<Resp, Event> {
buffer.begin_action(); buffer.begin_action();
let is_doc = matches!(self.mode, Mode::Doc);
match event.to_action(|e| { match event.to_action(|e| {
e.to_char() e.to_char()
.map(Action::Char) .map(Action::Char)
@ -67,7 +70,6 @@ impl Input {
.or_else(|| e.to_select_token()) .or_else(|| e.to_select_token())
.or_else(|| e.to_select_all()) .or_else(|| e.to_select_all())
.or_else(|| e.to_indent()) .or_else(|| e.to_indent())
.or_else(|| e.to_mouse(self.last_area))
.or_else(|| e.to_edit()) .or_else(|| e.to_edit())
}) { }) {
Some(Action::Char(c)) => { Some(Action::Char(c)) => {
@ -83,7 +85,9 @@ impl Input {
self.refocus(buffer, cursor_id); self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) 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 { let dist = match dist {
Dist::Char => [1, 1], Dist::Char => [1, 1],
Dist::Page => self.last_area.size().map(|s| s.saturating_sub(3).max(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); self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Pan(dir, dist)) => { Some(Action::Pan(dir, dist)) if is_doc => {
let dist = match dist { let dist = match dist {
Dist::Char => [1, 1], Dist::Char => [1, 1],
Dist::Page => self.last_area.size().map(|s| s.saturating_sub(3).max(1)), Dist::Page => self.last_area.size().map(|s| s.saturating_sub(3).max(1)),
@ -130,7 +134,13 @@ impl Input {
buffer.select_all_cursor(cursor_id); buffer.select_all_cursor(cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Mouse(MouseAction::Click, pos, false)) => { 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])
{
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]]; 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 we're already in the right place, select the token instead
if let Some(cursor) = buffer.cursors.get(cursor_id) if let Some(cursor) = buffer.cursors.get(cursor_id)
@ -141,11 +151,25 @@ impl Input {
} else { } else {
buffer.goto_cursor(cursor_id, pos, true); 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)) Ok(Resp::handled(None))
} }
Some( Some(
Action::Mouse(MouseAction::Drag, pos, false) Action::Mouse(MouseAction::Drag, pos, false, _)
| Action::Mouse(MouseAction::Click, pos, true), | Action::Mouse(MouseAction::Click, pos, true, _),
) => { ) => {
buffer.goto_cursor( buffer.goto_cursor(
cursor_id, cursor_id,
@ -226,11 +250,6 @@ impl Input {
title.as_deref(), 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 line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
let margin_w = match self.mode { let margin_w = match self.mode {
Mode::Prompt => 2, Mode::Prompt => 2,
@ -240,6 +259,11 @@ impl Input {
self.last_area = frame.rect([margin_w, 0], [!0, !0]).area(); 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; let mut pos = 0;
for (i, (line_num, (line_pos, line))) in buffer for (i, (line_num, (line_pos, line))) in buffer
.text .text
@ -342,18 +366,19 @@ impl Input {
let line_count = buffer.text.lines().count(); let line_count = buffer.text.lines().count();
let frame_sz = outer_frame.size()[1].saturating_sub(2).max(1); 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); 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 lines2 = line_count.saturating_sub(frame_sz).max(1);
let offset = frame_sz.saturating_sub(scroll_sz) let offset = frame_sz.saturating_sub(scroll_sz)
* (self.focus[1].max(0) as usize).min(lines2) * (self.focus[1].max(0) as usize).min(lines2)
/ lines2; / lines2;
let pos = [outer_frame.size()[0].saturating_sub(1), 1 + offset];
outer_frame outer_frame
.rect( .rect(pos, [1, scroll_sz])
[outer_frame.size()[0].saturating_sub(1), 1 + offset],
[1, scroll_sz],
)
.with_bg(Color::White) .with_bg(Color::White)
.fill(' '); .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::{ pub use self::{
doc::{Doc, Finder}, doc::{Doc, Finder},
input::Input, input::Input,
panes::{Pane, Panes}, panes::Panes,
prompt::{Confirm, Opener, Prompt, Show, Switcher}, prompt::{Confirm, Opener, Prompt, Show, Switcher},
root::Root, root::Root,
search::Searcher, search::Searcher,
@ -64,7 +64,7 @@ impl<End> Resp<End> {
pub fn is_end(&self) -> bool { pub fn is_end(&self) -> bool {
self.ended.is_some() self.ended.is_some()
} }
pub fn into_ended(mut self) -> Option<End> { pub fn into_ended(self) -> Option<End> {
self.ended self.ended
} }
} }
@ -113,6 +113,7 @@ pub struct Options<T> {
// (score, option) // (score, option)
pub options: Vec<T>, pub options: Vec<T>,
pub ranking: Vec<usize>, pub ranking: Vec<usize>,
pub last_height: usize,
} }
impl<T> Options<T> { impl<T> Options<T> {
@ -123,13 +124,18 @@ impl<T> Options<T> {
selected: 0, selected: 0,
options, options,
ranking, 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>( pub fn set_options<F: FnMut(&T) -> Option<S>, S: Ord + Copy>(
&mut self, &mut self,
options: impl IntoIterator<Item = T>, options: impl IntoIterator<Item = T>,
mut f: F, f: F,
) { ) {
self.options = options.into_iter().collect(); self.options = options.into_iter().collect();
self.apply_scoring(f); self.apply_scoring(f);
@ -155,13 +161,28 @@ impl<T> Options<T> {
impl<T: Clone> Element<T> for Options<T> { impl<T: Clone> Element<T> for Options<T> {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> { 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())) { 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 { match dir {
Dir::Up => { Dir::Up => {
self.selected = (self.selected + self.ranking.len()).saturating_sub(1) if self.selected == 0 {
% self.ranking.len().max(1) 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), _ => return Err(event),
} }
Ok(Resp::handled(None)) Ok(Resp::handled(None))
@ -192,6 +213,8 @@ impl<T: Visual> Visual for Options<T> {
None, None,
); );
self.last_height = frame.size()[1];
self.focus = self self.focus = self
.focus .focus
.max( .max(

View file

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

View file

@ -54,9 +54,9 @@ impl Prompt {
- 1; - 1;
Ok(Action::GotoLine(line)) Ok(Action::GotoLine(line))
} }
Some("search") => { Some(arg0 @ "search") => {
let needle = args.next().ok_or_else(|| "Expected argument".to_string())?; let needle = cmd.get(arg0.len()..).unwrap().trim().to_string();
Ok(Action::BeginSearch(needle.to_string())) Ok(Action::BeginSearch(needle))
} }
Some(cmd) => Err(format!("Unknown command `{cmd}`")), Some(cmd) => Err(format!("Unknown command `{cmd}`")),
None => Err(format!("No command entered")), None => Err(format!("No command entered")),
@ -282,6 +282,7 @@ pub struct Opener {
pub buffer: Buffer, pub buffer: Buffer,
pub cursor_id: CursorId, pub cursor_id: CursorId,
pub input: Input, pub input: Input,
preview: Option<(Buffer, CursorId, Input)>,
} }
impl Opener { impl Opener {
@ -297,13 +298,15 @@ impl Opener {
cursor_id, cursor_id,
buffer, buffer,
input: Input::filter(), input: Input::filter(),
preview: None,
}; };
this.update_completions(); this.update_completions();
this this
} }
pub fn requested_height(&self) -> usize { 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) { fn set_string(&mut self, s: &str) {
@ -325,11 +328,12 @@ impl Opener {
let options = entries let options = entries
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter_map(|entry| { .filter_map(|entry| {
let metadata = fs::metadata(entry.path()).ok()?;
Some(FileOption { Some(FileOption {
path: entry.path(), path: entry.path(),
kind: if entry.file_type().ok()?.is_dir() { kind: if metadata.file_type().is_dir() {
FileKind::Dir FileKind::Dir
} else if entry.file_type().ok()?.is_file() { } else if metadata.file_type().is_file() {
FileKind::File FileKind::File
} else { } else {
FileKind::Unknown FileKind::Unknown
@ -377,7 +381,7 @@ impl Opener {
impl Element<()> for Opener { impl Element<()> for Opener {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
let path_str = self.buffer.text.to_string(); 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)), Some(Action::Cancel) => Ok(Resp::end(None)),
// Backspace removes the entire path segment! // Backspace removes the entire path segment!
// Only works if we're at the end of the string // 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)), Ok(None) => Ok(Resp::handled(None)),
Err(event) => { Err(event) => {
let res = self let res = match self
.input .input
.handle(&mut self.buffer, self.cursor_id, event) .handle(&mut self.buffer, self.cursor_id, event)
.map(Resp::into_can_end); .map(Resp::into_can_end)
self.update_completions(); {
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 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}"), Some(name) => format!("{name}"),
None => format!("Unknown"), None => format!("Unknown"),
}; };
let is_link = if self.is_link { " (symlink)" } else { "" };
let desc = match self.kind { let desc = match self.kind {
FileKind::Dir => "Directory", FileKind::Dir => format!("Directory{is_link}"),
FileKind::Unknown => "Unknown filesystem item", FileKind::Unknown => format!("Unknown{is_link}"),
FileKind::File => "File", FileKind::File => format!("File{is_link}"),
FileKind::New => "Create new file", FileKind::New => format!("Create new file{is_link}"),
}; };
frame frame
.with_fg(match self.kind { .with_fg(match self.kind {
@ -463,11 +480,40 @@ impl Visual for FileOption {
impl Visual for Opener { impl Visual for Opener {
fn render(&mut self, state: &State, frame: &mut Rect) { 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 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)); .with(|f| self.options.render(state, f));
frame 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| { .with(|f| {
self.input self.input
.render(state, None, &self.buffer, self.cursor_id, None, f) .render(state, None, &self.buffer, self.cursor_id, None, f)

View file

@ -10,6 +10,7 @@ pub struct Searcher {
buffer: Buffer, buffer: Buffer,
cursor_id: CursorId, cursor_id: CursorId,
input: Input, input: Input,
preview: Option<(Buffer, CursorId, Input, SearchResult)>,
} }
impl Searcher { impl Searcher {
@ -87,11 +88,13 @@ impl Searcher {
cursor_id, cursor_id,
buffer, buffer,
input: Input::filter(), input: Input::filter(),
preview: None,
} }
} }
pub fn requested_height(&self) -> usize { pub fn requested_height(&self) -> usize {
self.options.requested_height() + 3 !0
// self.options.requested_height() + 3
} }
fn update_completions(&mut self) { fn update_completions(&mut self) {
@ -113,7 +116,9 @@ impl Searcher {
impl Element<()> for Searcher { impl Element<()> for Searcher {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> { 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)), Some(Action::Cancel) => Ok(Resp::end(None)),
_ => match self.options.handle(state, event).map(Resp::into_ended) { _ => match self.options.handle(state, event).map(Resp::into_ended) {
// Selecting a directory enters the directory // Selecting a directory enters the directory
@ -123,19 +128,35 @@ impl Element<()> for Searcher {
))))), ))))),
Ok(None) => Ok(Resp::handled(None)), Ok(None) => Ok(Resp::handled(None)),
Err(event) => { Err(event) => {
let res = self let res = match self
.input .input
.handle(&mut self.buffer, self.cursor_id, event) .handle(&mut self.buffer, self.cursor_id, event)
.map(Resp::into_can_end); .map(Resp::into_can_end)
self.update_completions(); {
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 res
} }
}, },
};
if self.buffer.text.to_string() != filter_str {
self.update_completions();
} }
res
} }
} }
#[derive(Clone)] #[derive(Clone, PartialEq)]
pub struct SearchResult { pub struct SearchResult {
pub path: PathBuf, pub path: PathBuf,
pub line_idx: usize, pub line_idx: usize,
@ -162,11 +183,43 @@ impl Visual for SearchResult {
impl Visual for Searcher { impl Visual for Searcher {
fn render(&mut self, state: &State, frame: &mut Rect) { 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 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)); .with(|f| self.options.render(state, f));
frame 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| { .with(|f| {
let title = format!( let title = format!(
"{} of {} results for '{}' in {}/", "{} of {} results for '{}' in {}/",