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