Stupidly reliable opener search, better find functionality
This commit is contained in:
parent
281f75c958
commit
551b2816b3
9 changed files with 130 additions and 60 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>, 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<String>), // 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<Action> {
|
||||
pub fn to_open_finder(&self, selection: Option<String>) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
|
|
@ -262,7 +262,7 @@ impl RawEvent {
|
|||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::OpenFinder)
|
||||
Some(Action::OpenFinder(selection))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>();
|
||||
|
|
|
|||
24
src/state.rs
24
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<BufferId, Buffer>,
|
||||
pub tick: u64,
|
||||
|
|
@ -477,8 +481,12 @@ impl TryFrom<Args> 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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
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<bool> {
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -151,7 +151,10 @@ impl<T: Clone> Element<T> for Options<T> {
|
|||
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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue