Compare commits
6 commits
3862c66538
...
0337006986
| Author | SHA1 | Date | |
|---|---|---|---|
| 0337006986 | |||
| 3e8dcdfb11 | |||
| 6a6bcec4ce | |||
| 07837f7761 | |||
| df378b46de | |||
| 4d50159122 |
12 changed files with 265 additions and 110 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!['#', ' ']),
|
||||
},
|
||||
|
|
|
|||
27
src/state.rs
27
src/state.rs
|
|
@ -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)?))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {}/",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue