diff --git a/README.md b/README.md index 3619435..9c2185a 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ - [x] Multiple panes - [x] Pane creation/deletion - [x] Opener +- [x] Find +- [x] Search in buffer switcher +- [x] File saving +- [x] Syntax highlighting ## Todo -- [ ] Find - [ ] Replace - [ ] Project search -- [ ] Search in buffer switcher -- [ ] File saving -- [ ] Syntax highlighting - [ ] Auto-indent (and related features) - [ ] Undo/redo diff --git a/src/action.rs b/src/action.rs index 130b584..baeb613 100644 --- a/src/action.rs +++ b/src/action.rs @@ -14,7 +14,7 @@ pub enum Dir { pub enum Action { Char(char), // Insert a character Indent(bool), // Indent (indent vs deindent) - Move(Dir, bool, bool, bool), // Move the cursor (dir, page, retain_base, word) + Move(Dir, bool, bool, bool), // Move the cursor (dir, page, retain_base, word) PaneMove(Dir), // Move panes PaneOpen(Dir), // Create a new pane PaneClose, // Close the current pane @@ -28,7 +28,7 @@ pub enum Action { Show(Option, String), // Display an optionally titled informational text box to the user OpenSwitcher, // Open the buffer switcher OpenOpener(PathBuf), // Open the file opener - OpenFinder, // Open the finder + OpenFinder(Option), // Open the finder, with the given default query SwitchBuffer(BufferId), // Switch the current pane to the given buffer OpenFile(PathBuf), // Open the file and switch the current pane to it CommandStart(&'static str), // Start a new command @@ -252,7 +252,7 @@ impl RawEvent { } } - pub fn to_open_finder(&self) -> Option { + pub fn to_open_finder(&self, selection: Option) -> Option { if matches!( &self.0, TerminalEvent::Key(KeyEvent { @@ -262,7 +262,7 @@ impl RawEvent { .. }) ) { - Some(Action::OpenFinder) + Some(Action::OpenFinder(selection)) } else { None } diff --git a/src/main.rs b/src/main.rs index 419a20a..0853105 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,6 @@ pub enum Error { fn main() -> Result<(), Error> { let args = Args::parse(); - println!("{args:?}"); let mut state = State::try_from(args)?; let open_buffers = state.buffers.keys().collect::>(); diff --git a/src/state.rs b/src/state.rs index 7b372e1..d1d8393 100644 --- a/src/state.rs +++ b/src/state.rs @@ -251,7 +251,9 @@ impl Buffer { self.insert(pos, (0..n).map(|_| ' ')); } else { // First, find the next non-space character in the line - while self.text.chars().get(pos) == Some(&' ') { pos += 1; } + while self.text.chars().get(pos) == Some(&' ') { + pos += 1; + } // Find the desired column, and hence the number of spaces to remove let coord = self.text.to_coord(pos).map(|e| e.max(0) as usize); @@ -264,11 +266,10 @@ impl Buffer { Some(pos) if self.text.chars().get(pos) == Some(&' ') => { self.remove(pos..pos + 1); pos - }, + } _ => break, }; } - } } @@ -306,7 +307,11 @@ impl Buffer { let class = self.text.chars().get(pos).copied().map(classify); loop { pos = match pos.checked_sub(1) { - Some(pos) if self.text.chars().get(pos).copied().map(classify) == class => pos, + Some(pos) + if self.text.chars().get(pos).copied().map(classify) == class => + { + pos + } _ => break pos, } } @@ -325,7 +330,7 @@ impl Buffer { pos = if self.text.chars().get(pos).copied().map(classify) == class { pos + 1 } else { - break pos + break pos; }; } } else { @@ -461,7 +466,6 @@ fn classify(c: char) -> u8 { } } - pub struct State { pub buffers: HopSlotMap, pub tick: u64, @@ -477,8 +481,12 @@ impl TryFrom for State { theme: theme::Theme::default(), }; - for path in args.paths { - this.buffers.insert(Buffer::from_file(path)?); + if args.paths.is_empty() { + this.buffers.insert(Buffer::default()); + } else { + for path in args.paths { + this.buffers.insert(Buffer::from_file(path)?); + } } Ok(this) diff --git a/src/theme.rs b/src/theme.rs index b2db466..331a797 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -72,7 +72,7 @@ impl Default for Theme { select_bg: Color::AnsiValue(23), line_select_bg: Color::AnsiValue(8), unfocus_select_bg: Color::AnsiValue(240), - search_result_bg: Color::AnsiValue(66), + search_result_bg: Color::AnsiValue(60), margin_bg: Color::Reset, margin_line_num: Color::AnsiValue(245), border: BorderTheme::default(), diff --git a/src/ui/doc.rs b/src/ui/doc.rs index dbd7f26..2806df1 100644 --- a/src/ui/doc.rs +++ b/src/ui/doc.rs @@ -65,19 +65,33 @@ impl Element for Doc { return Err(event); }; - let open_path = buffer.dir.to_owned().unwrap_or(PathBuf::from("/")); + let open_path = buffer + .dir + .to_owned() + .unwrap_or_else(|| std::env::current_dir().expect("no working dir")); + + let selection = buffer.cursors[cursor_id] + .selection() + .map(|range| buffer.text.chars()[range].iter().copied().collect()); match event.to_action(|e| { e.to_open_switcher() .or_else(|| e.to_open_opener(open_path)) - .or_else(|| e.to_open_finder()) + .or_else(|| e.to_open_finder(selection)) .or_else(|| e.to_move()) .or_else(|| e.to_save()) }) { action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))), action @ Some(Action::OpenOpener(_)) => Ok(Resp::handled(action.map(Into::into))), - action @ Some(Action::OpenFinder) => { - self.search = Some(Search::new(buffer.cursors[cursor_id])); + ref action @ Some(Action::OpenFinder(ref query)) => { + self.search = Some(Search::new( + buffer.cursors[cursor_id], + query.clone(), + state, + &mut self.input, + self.buffer, + cursor_id, + )); Ok(Resp::handled(None)) } Some(Action::SwitchBuffer(new_buffer)) => { @@ -124,7 +138,7 @@ impl Visual for Doc { [0, 0], [frame.size()[0], frame.size()[1].saturating_sub(search_h)], ) - .with_focus(true/*self.search.is_none()*/) + .with_focus(true /*self.search.is_none()*/) .with(|f| { self.input.render( state, @@ -162,19 +176,35 @@ pub struct Search { } impl Search { - fn new(old_cursor: Cursor) -> Self { + fn new( + old_cursor: Cursor, + query: Option, + state: &mut State, + input: &mut Input, + buffer_id: BufferId, + cursor_id: CursorId, + ) -> Self { let mut buffer = Buffer::default(); - Self { + let cursor_id = buffer.start_session(); + + // Insert default query + buffer.insert(0, query.iter().flat_map(|s| s.chars())); + + let mut this = Self { old_cursor, - cursor_id: buffer.start_session(), + cursor_id, buffer, input: Input::filter(), selected: 0, needle: Vec::new(), results: Vec::new(), - } + }; + + this.update(state, input, buffer_id, cursor_id); + + this } pub fn contains(&self, pos: usize) -> Option { @@ -189,6 +219,37 @@ impl Search { .map(|_| idx == self.selected) } + fn update( + &mut self, + state: &mut State, + input: &mut Input, + buffer_id: BufferId, + cursor_id: CursorId, + ) { + let buffer = &mut state.buffers[buffer_id]; + + let needle = self.buffer.text.chars(); + if self.needle != needle { + // The needle has changed! + let haystack = buffer.text.chars(); + + self.needle = needle.to_vec(); + self.results = (0..haystack.len().saturating_sub(needle.len())) + .filter(|i| haystack[*i..].starts_with(needle)) + .collect(); + + // Select the first entry that comes after the current cursor position + self.selected = (0..self.results.len()) + .find(|i| self.results[*i] >= self.old_cursor.pos) + .unwrap_or(0); + } + + if let Some(result) = self.results.get(self.selected) { + buffer.cursors[cursor_id].select(*result..*result + self.needle.len()); + input.refocus(buffer, cursor_id); + } + } + fn handle( &mut self, state: &mut State, @@ -225,21 +286,7 @@ impl Search { .map(Resp::into_can_end), }; - let needle = self.buffer.text.chars(); - if self.needle != needle { - let haystack = buffer.text.chars(); - - self.selected = 0; - self.needle = needle.to_vec(); - self.results = (0..haystack.len().saturating_sub(needle.len())) - .filter(|i| haystack[*i..].starts_with(needle)) - .collect(); - } - - if let Some(result) = self.results.get(self.selected) { - buffer.cursors[cursor_id].select(*result..*result + self.needle.len()); - input.refocus(buffer, cursor_id); - } + self.update(state, input, buffer_id, cursor_id); res } diff --git a/src/ui/input.rs b/src/ui/input.rs index 7c2ae4e..46a8025 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -177,7 +177,10 @@ impl Input { } else { None }; - let selected = cursor.selection().zip(pos).map_or(false, |(s, pos)| s.contains(&pos)); + let selected = cursor + .selection() + .zip(pos) + .map_or(false, |(s, pos)| s.contains(&pos)); let (fg, c) = match line.get(coord as usize).copied() { Some('\n') if selected => (state.theme.whitespace, '⮠'), Some(c) => { @@ -197,18 +200,22 @@ impl Input { let bg = match search.map(|s| s.contains(pos?)) { Some(Some(true)) => state.theme.select_bg, Some(Some(false)) => state.theme.search_result_bg, - Some(None) if line_selected && frame.has_focus() => state.theme.line_select_bg, - _ => if selected { - if frame.has_focus() { - state.theme.select_bg - } else { - state.theme.unfocus_select_bg - } - } else if line_selected && frame.has_focus() { + Some(None) if line_selected && frame.has_focus() => { state.theme.line_select_bg - } else { - Color::Reset - }, + } + _ => { + if selected { + if frame.has_focus() { + state.theme.select_bg + } else { + state.theme.unfocus_select_bg + } + } else if line_selected && frame.has_focus() { + state.theme.line_select_bg + } else { + Color::Reset + } + } }; frame .with_bg(bg) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5e20489..5b6d035 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -151,7 +151,10 @@ impl Element for Options { match event.to_action(|e| e.to_go().or_else(|| e.to_move())) { Some(Action::Move(dir, false, false, false)) => { match dir { - Dir::Up => self.selected = (self.selected + self.ranking.len() - 1) % self.ranking.len(), + Dir::Up => { + self.selected = + (self.selected + self.ranking.len() - 1) % self.ranking.len() + } Dir::Down => self.selected = (self.selected + 1) % self.ranking.len(), _ => return Err(event), } diff --git a/src/ui/prompt.rs b/src/ui/prompt.rs index e85d5a9..c0694ca 100644 --- a/src/ui/prompt.rs +++ b/src/ui/prompt.rs @@ -1,6 +1,6 @@ use super::*; use crate::state::{Buffer, BufferId, CursorId}; -use std::{fs, path::PathBuf}; +use std::{cmp::Reverse, fs, path::PathBuf}; pub struct Prompt { buffer: Buffer, @@ -345,15 +345,21 @@ impl Opener { // TODO self.options.set_options(options, |e| { let name = e.path.file_name()?.to_str()?.to_lowercase(); + let modify_time = e + .path + .metadata() + .ok() + .and_then(|m| Some(m.modified().ok()?.elapsed().ok()?.as_secs())) + .unwrap_or(!0); if matches!(e.kind, FileKind::New) { // Special-case: the 'new file' entry always matches last - Some((3, name.chars().count())) + Some((1000, 0, 0)) } else if name == filter { - Some((0, name.chars().count())) + Some((0, modify_time, name.chars().count())) } else if name.starts_with(&filter) { - Some((1, name.chars().count())) + Some((1, modify_time, name.chars().count())) } else if name.contains(&filter) { - Some((2, name.chars().count())) + Some((2, modify_time, name.chars().count())) } else { None }