Basic find functionality
This commit is contained in:
parent
8bd6a70968
commit
e9e938ca4a
7 changed files with 266 additions and 44 deletions
|
|
@ -13,7 +13,7 @@ pub enum Dir {
|
|||
#[derive(Clone, Debug)]
|
||||
pub enum Action {
|
||||
Char(char), // Insert a character
|
||||
Backspace, // Backspace a character
|
||||
Indent(bool), // Indent (indent vs deindent)
|
||||
Move(Dir, bool, bool), // Move the cursor (dir, page, retain_base)
|
||||
PaneMove(Dir), // Move panes
|
||||
PaneOpen(Dir), // Create a new pane
|
||||
|
|
@ -28,6 +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
|
||||
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
|
||||
|
|
@ -191,6 +192,20 @@ impl RawEvent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_indent(&self) -> Option<Action> {
|
||||
if let TerminalEvent::Key(KeyEvent {
|
||||
code: c @ (KeyCode::Tab | KeyCode::BackTab),
|
||||
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
}) = &self.0
|
||||
{
|
||||
Some(Action::Indent(*c == KeyCode::Tab))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_open_prompt(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
|
|
@ -239,6 +254,22 @@ impl RawEvent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_open_finder(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Char('f'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::OpenFinder)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_command_start(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
|
|
|
|||
35
src/state.rs
35
src/state.rs
|
|
@ -35,6 +35,18 @@ impl Cursor {
|
|||
Some(self.base.min(self.pos)..self.base.max(self.pos))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place_at(&mut self, pos: usize) {
|
||||
self.base = pos;
|
||||
self.pos = pos;
|
||||
// TODO: Reset desired position
|
||||
}
|
||||
|
||||
pub fn select(&mut self, range: Range<usize>) {
|
||||
self.base = range.start;
|
||||
self.pos = range.end;
|
||||
// TODO: Reset desired position
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -208,13 +220,32 @@ impl Buffer {
|
|||
.or(b)
|
||||
})
|
||||
{
|
||||
cursor.base = tok.range.start;
|
||||
cursor.pos = tok.range.end;
|
||||
cursor.select(tok.range.clone());
|
||||
} else {
|
||||
// TODO: Bell
|
||||
}
|
||||
}
|
||||
|
||||
fn indent_at(&mut self, pos: usize) {
|
||||
const TAB_ALIGN: usize = 4;
|
||||
|
||||
let coord = self.text.to_coord(pos).map(|e| e.max(0) as usize);
|
||||
let next_up = |x: usize, n: usize| (x / n + 1) * n;
|
||||
let n = next_up(coord[0], TAB_ALIGN) - coord[0];
|
||||
self.insert(pos, (0..n).map(|_| ' '));
|
||||
}
|
||||
|
||||
pub fn indent(&mut self, cursor_id: CursorId, forward: bool) {
|
||||
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
|
||||
return;
|
||||
};
|
||||
if let Some(range) = cursor.selection() {
|
||||
} else {
|
||||
let pos = cursor.pos;
|
||||
self.indent_at(pos);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_cursor(
|
||||
&mut self,
|
||||
cursor_id: CursorId,
|
||||
|
|
|
|||
158
src/ui/doc.rs
158
src/ui/doc.rs
|
|
@ -5,12 +5,12 @@ use crate::{
|
|||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Doc {
|
||||
buffer: BufferId,
|
||||
// Remember the cursor we use for each buffer
|
||||
cursors: HashMap<BufferId, CursorId>,
|
||||
input: Input,
|
||||
search: Option<Search>,
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
|
|
@ -22,6 +22,7 @@ impl Doc {
|
|||
.into_iter()
|
||||
.collect(),
|
||||
input: Input::default(),
|
||||
search: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,15 +51,34 @@ impl Doc {
|
|||
|
||||
impl Element for Doc {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
|
||||
if let Some(search) = &mut self.search {
|
||||
let resp = search.handle(state, &mut self.input, self.buffer, cursor_id, event)?;
|
||||
if resp.is_end() {
|
||||
self.search = None;
|
||||
}
|
||||
return Ok(Resp::handled(resp.event));
|
||||
}
|
||||
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
|
||||
let open_path = buffer.dir.to_owned().unwrap_or(PathBuf::from("/"));
|
||||
|
||||
match event.to_action(|e| e.to_open_switcher().or_else(|| e.to_open_opener(open_path))) {
|
||||
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_move())
|
||||
}) {
|
||||
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());
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||
self.switch_buffer(state, new_buffer);
|
||||
Ok(Resp::handled(None))
|
||||
|
|
@ -77,7 +97,6 @@ impl Element for Doc {
|
|||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
self.input.handle(buffer, cursor_id, event)
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +109,137 @@ impl Visual for Doc {
|
|||
return;
|
||||
};
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
self.input.render(state, buffer, cursor_id, frame);
|
||||
|
||||
let search_h = if self.search.is_some() { 3 } else { 0 };
|
||||
|
||||
// Render input
|
||||
frame
|
||||
.rect(
|
||||
[0, 0],
|
||||
[frame.size()[0], frame.size()[1].saturating_sub(search_h)],
|
||||
)
|
||||
.with_focus(self.search.is_none())
|
||||
.with(|f| {
|
||||
self.input.render(
|
||||
state,
|
||||
buffer.name(),
|
||||
buffer,
|
||||
cursor_id,
|
||||
self.search.as_ref(),
|
||||
f,
|
||||
)
|
||||
});
|
||||
|
||||
// Render search
|
||||
if let Some(search) = &mut self.search {
|
||||
frame
|
||||
.rect(
|
||||
[0, frame.size()[1].saturating_sub(search_h)],
|
||||
[frame.size()[0], search_h],
|
||||
)
|
||||
.with_focus(true)
|
||||
.with(|f| search.render(state, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Search {
|
||||
buffer: Buffer,
|
||||
cursor_id: CursorId,
|
||||
input: Input,
|
||||
|
||||
selected: usize,
|
||||
needle: Vec<char>,
|
||||
results: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Search {
|
||||
fn new() -> Self {
|
||||
let mut buffer = Buffer::default();
|
||||
Self {
|
||||
cursor_id: buffer.start_session(),
|
||||
buffer,
|
||||
input: Input::filter(),
|
||||
|
||||
selected: 0,
|
||||
needle: Vec::new(),
|
||||
results: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, pos: usize) -> Option<bool> {
|
||||
let idx = self
|
||||
.results
|
||||
.binary_search(&pos)
|
||||
.unwrap_or_else(|p| p.saturating_sub(1));
|
||||
|
||||
self.results
|
||||
.get(idx)
|
||||
.filter(|start| (**start..**start + self.needle.len()).contains(&pos))
|
||||
.map(|_| idx == self.selected)
|
||||
}
|
||||
|
||||
fn handle(
|
||||
&mut self,
|
||||
state: &mut State,
|
||||
input: &mut Input,
|
||||
buffer_id: BufferId,
|
||||
cursor_id: CursorId,
|
||||
event: Event,
|
||||
) -> Result<Resp<()>, Event> {
|
||||
let buffer = &mut state.buffers[buffer_id];
|
||||
|
||||
let res = match event
|
||||
.to_action(|e| e.to_cancel().or_else(|| e.to_go()).or_else(|| e.to_move()))
|
||||
{
|
||||
Some(Action::Cancel | Action::Go) => return Ok(Resp::end(None)),
|
||||
Some(Action::Move(dir, false, _)) => {
|
||||
match dir {
|
||||
Dir::Up => {
|
||||
self.selected = (self.selected + self.results.len().saturating_sub(1))
|
||||
% self.results.len().max(1)
|
||||
}
|
||||
Dir::Down => self.selected = (self.selected + 1) % self.results.len().max(1),
|
||||
_ => {}
|
||||
}
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
_ => self
|
||||
.input
|
||||
.handle(&mut self.buffer, self.cursor_id, event)
|
||||
.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);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Search {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let title = format!("{} of {} results", self.selected + 1, self.results.len());
|
||||
self.input.render(
|
||||
state,
|
||||
Some(&title),
|
||||
&self.buffer,
|
||||
self.cursor_id,
|
||||
None,
|
||||
frame,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,17 +36,19 @@ impl Input {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn focus(&mut self, coord: [isize; 2]) {
|
||||
for i in 0..2 {
|
||||
self.focus[i] =
|
||||
self.focus[i].clamp(coord[i] - self.last_size[i] as isize + 1, coord[i]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refocus(&mut self, buffer: &mut Buffer, cursor_id: CursorId) {
|
||||
let Some(cursor) = buffer.cursors.get(cursor_id) else {
|
||||
return;
|
||||
};
|
||||
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
||||
for i in 0..2 {
|
||||
self.focus[i] = self.focus[i].clamp(
|
||||
cursor_coord[i] - self.last_size[i] as isize + 1,
|
||||
cursor_coord[i],
|
||||
);
|
||||
}
|
||||
self.focus(cursor_coord);
|
||||
}
|
||||
|
||||
pub fn handle(
|
||||
|
|
@ -60,6 +62,7 @@ impl Input {
|
|||
.map(Action::Char)
|
||||
.or_else(|| e.to_move())
|
||||
.or_else(|| e.to_select_token())
|
||||
.or_else(|| e.to_indent())
|
||||
}) {
|
||||
Some(Action::Char(c)) => {
|
||||
if c == '\x08' {
|
||||
|
|
@ -82,6 +85,10 @@ impl Input {
|
|||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Indent(forward)) => {
|
||||
buffer.indent(cursor_id, forward);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::GotoLine(line)) => {
|
||||
buffer.goto_line_cursor(cursor_id, line);
|
||||
self.refocus(buffer, cursor_id);
|
||||
|
|
@ -98,12 +105,12 @@ impl Input {
|
|||
pub fn render(
|
||||
&mut self,
|
||||
state: &State,
|
||||
title: Option<&str>,
|
||||
buffer: &Buffer,
|
||||
cursor_id: CursorId,
|
||||
search: Option<&Search>,
|
||||
frame: &mut Rect,
|
||||
) {
|
||||
let title = buffer.name();
|
||||
|
||||
// Add frame
|
||||
let mut frame = frame.with_border(
|
||||
if frame.has_focus() {
|
||||
|
|
@ -182,14 +189,21 @@ impl Input {
|
|||
}
|
||||
}
|
||||
};
|
||||
let bg = if let Some(s) = search {
|
||||
match s.contains(pos) {
|
||||
Some(true) => state.theme.select_bg,
|
||||
Some(false) => state.theme.unfocus_select_bg,
|
||||
None => Color::Reset,
|
||||
}
|
||||
} else if !selected {
|
||||
Color::Reset
|
||||
} else if frame.has_focus() {
|
||||
state.theme.select_bg
|
||||
} else {
|
||||
state.theme.unfocus_select_bg
|
||||
};
|
||||
frame
|
||||
.with_bg(if !selected {
|
||||
Color::Reset
|
||||
} else if frame.has_focus() {
|
||||
state.theme.select_bg
|
||||
} else {
|
||||
state.theme.unfocus_select_bg
|
||||
})
|
||||
.with_bg(bg)
|
||||
.with_fg(fg)
|
||||
.text([i as isize, 0], c.encode_utf8(&mut [0; 4]));
|
||||
}
|
||||
|
|
@ -208,19 +222,3 @@ impl Input {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl Visual for Input {
|
||||
// fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
// frame.with(|frame| {
|
||||
// frame.fill(' ');
|
||||
// frame.text([0, 0], self.preamble.chars());
|
||||
|
||||
// frame
|
||||
// .rect([self.preamble.chars().count(), 0], frame.size())
|
||||
// .with(|frame| {
|
||||
// frame.text([0, 0], &self.text);
|
||||
// frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ mod root;
|
|||
mod status;
|
||||
|
||||
pub use self::{
|
||||
doc::Doc,
|
||||
doc::{Doc, Search},
|
||||
input::Input,
|
||||
panes::Panes,
|
||||
panes::{Pane, Panes},
|
||||
prompt::{Confirm, Opener, Prompt, Show, Switcher},
|
||||
root::Root,
|
||||
status::Status,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use super::*;
|
||||
use crate::state::BufferId;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Pane {
|
||||
Empty,
|
||||
Doc(Doc),
|
||||
|
|
@ -22,6 +21,10 @@ impl Panes {
|
|||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_mut(&mut self) -> Option<&mut Pane> {
|
||||
self.panes.get_mut(self.selected)
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Panes {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,10 @@ impl Element<()> for Prompt {
|
|||
|
||||
impl Visual for Prompt {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
frame.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||
frame.with(|f| {
|
||||
self.input
|
||||
.render(state, None, &self.buffer, self.cursor_id, None, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +256,10 @@ impl Visual for Switcher {
|
|||
.with(|f| self.options.render(state, f));
|
||||
frame
|
||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||
.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||
.with(|f| {
|
||||
self.input
|
||||
.render(state, None, &self.buffer, self.cursor_id, None, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +449,9 @@ impl Visual for Opener {
|
|||
.with(|f| self.options.render(state, f));
|
||||
frame
|
||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||
.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||
.with(|f| {
|
||||
self.input
|
||||
.render(state, None, &self.buffer, self.cursor_id, None, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue