Added project search
This commit is contained in:
parent
b0b4de2f64
commit
d13982cc05
7 changed files with 263 additions and 39 deletions
|
|
@ -36,12 +36,14 @@ pub enum Action {
|
|||
OpenOpener(PathBuf), // Open the file opener
|
||||
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
|
||||
GotoLine(isize), // Go to the specified file line
|
||||
SelectToken, // Fully select the token under the cursor
|
||||
SelectAll, // Fully select the entire input
|
||||
Save, // Save the current buffer
|
||||
OpenFile(PathBuf, usize), // Open the file (on the given line) and switch the current pane to it
|
||||
CommandStart(&'static str), // Start a new command
|
||||
GotoLine(isize), // Go to the specified file line
|
||||
BeginSearch(String), // Request to begin a search with the given needle
|
||||
OpenSearcher(PathBuf, String), // Start a project-wide search with the given location and needle
|
||||
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)
|
||||
Undo,
|
||||
Redo,
|
||||
|
|
@ -306,7 +308,7 @@ impl RawEvent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_open_opener(&self, path: PathBuf) -> Option<Action> {
|
||||
pub fn to_open_opener(&self, path: &PathBuf) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
|
|
@ -316,7 +318,7 @@ impl RawEvent {
|
|||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::OpenOpener(path))
|
||||
Some(Action::OpenOpener(path.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -349,6 +351,16 @@ impl RawEvent {
|
|||
})
|
||||
) {
|
||||
Some(Action::CommandStart("goto_line"))
|
||||
} else if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Char('f'),
|
||||
modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
}) if *modifiers == KeyModifiers::CONTROL | KeyModifiers::SHIFT
|
||||
) {
|
||||
Some(Action::CommandStart("search"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub struct Doc {
|
|||
// Remember the cursor we use for each buffer
|
||||
cursors: HashMap<BufferId, CursorId>,
|
||||
input: Input,
|
||||
search: Option<Search>,
|
||||
finder: Option<Finder>,
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
|
|
@ -22,7 +22,7 @@ impl Doc {
|
|||
.into_iter()
|
||||
.collect(),
|
||||
input: Input::default(),
|
||||
search: None,
|
||||
finder: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,10 +53,10 @@ 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 let Some(finder) = &mut self.finder {
|
||||
let resp = finder.handle(state, &mut self.input, self.buffer, cursor_id, event)?;
|
||||
if resp.is_end() {
|
||||
self.search = None;
|
||||
self.finder = None;
|
||||
}
|
||||
return Ok(Resp::handled(resp.event));
|
||||
}
|
||||
|
|
@ -72,15 +72,16 @@ impl Element for Doc {
|
|||
|
||||
match event.to_action(|e| {
|
||||
e.to_open_switcher()
|
||||
.or_else(|| e.to_open_opener(open_path))
|
||||
.or_else(|| e.to_open_opener(&open_path))
|
||||
.or_else(|| e.to_open_finder(None))
|
||||
.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::OpenSwitcher) | action @ Some(Action::OpenOpener(_)) => {
|
||||
Ok(Resp::handled(action.map(Into::into)))
|
||||
}
|
||||
ref action @ Some(Action::OpenFinder(ref query)) => {
|
||||
self.search = Some(Search::new(
|
||||
self.finder = Some(Finder::new(
|
||||
buffer.cursors[cursor_id],
|
||||
query.clone(),
|
||||
state,
|
||||
|
|
@ -90,13 +91,27 @@ impl Element for Doc {
|
|||
));
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::BeginSearch(needle)) => {
|
||||
let path = buffer
|
||||
.path
|
||||
.clone()
|
||||
.unwrap_or_else(|| std::env::current_dir().expect("no cwd"));
|
||||
Ok(Resp::handled(Some(
|
||||
Action::OpenSearcher(path, needle).into(),
|
||||
)))
|
||||
}
|
||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||
self.switch_buffer(state, new_buffer);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::OpenFile(path)) => match state.open_or_get(path) {
|
||||
Some(Action::OpenFile(path, line_idx)) => match state.open_or_get(path) {
|
||||
Ok(buffer_id) => {
|
||||
self.switch_buffer(state, buffer_id);
|
||||
if let Some(buffer) = state.buffers.get_mut(self.buffer) {
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
buffer.goto_cursor(cursor_id, [0, line_idx as isize], true);
|
||||
self.input.refocus(buffer, cursor_id);
|
||||
}
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Err(err) => Ok(Resp::handled(Some(
|
||||
|
|
@ -126,40 +141,40 @@ impl Visual for Doc {
|
|||
};
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
|
||||
let search_h = if self.search.is_some() { 3 } else { 0 };
|
||||
let finder_h = if self.finder.is_some() { 3 } else { 0 };
|
||||
|
||||
// Render input
|
||||
frame
|
||||
.rect(
|
||||
[0, 0],
|
||||
[frame.size()[0], frame.size()[1].saturating_sub(search_h)],
|
||||
[frame.size()[0], frame.size()[1].saturating_sub(finder_h)],
|
||||
)
|
||||
.with_focus(true /*self.search.is_none()*/)
|
||||
.with_focus(true /*self.finder.is_none()*/)
|
||||
.with(|f| {
|
||||
self.input.render(
|
||||
state,
|
||||
buffer.name().as_deref(),
|
||||
buffer,
|
||||
cursor_id,
|
||||
self.search.as_ref(),
|
||||
self.finder.as_ref(),
|
||||
f,
|
||||
)
|
||||
});
|
||||
|
||||
// Render search
|
||||
if let Some(search) = &mut self.search {
|
||||
// Render finder
|
||||
if let Some(finder) = &mut self.finder {
|
||||
frame
|
||||
.rect(
|
||||
[0, frame.size()[1].saturating_sub(search_h)],
|
||||
[frame.size()[0], search_h],
|
||||
[0, frame.size()[1].saturating_sub(finder_h)],
|
||||
[frame.size()[0], finder_h],
|
||||
)
|
||||
.with_focus(true)
|
||||
.with(|f| search.render(state, f));
|
||||
.with(|f| finder.render(state, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Search {
|
||||
pub struct Finder {
|
||||
old_cursor: Cursor,
|
||||
|
||||
buffer: Buffer,
|
||||
|
|
@ -171,7 +186,7 @@ pub struct Search {
|
|||
results: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Search {
|
||||
impl Finder {
|
||||
fn new(
|
||||
old_cursor: Cursor,
|
||||
query: Option<String>,
|
||||
|
|
@ -288,7 +303,7 @@ impl Search {
|
|||
}
|
||||
}
|
||||
|
||||
impl Visual for Search {
|
||||
impl Visual for Finder {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let title = format!("{} of {} results", self.selected + 1, self.results.len());
|
||||
self.input.render(
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ 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_area.size()[i] as isize + 1, coord[i]);
|
||||
self.focus[i] = self.focus[i]
|
||||
.max(coord[i] - self.last_area.size()[i] as isize + 1)
|
||||
.min(coord[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +208,7 @@ impl Input {
|
|||
title: Option<&str>,
|
||||
buffer: &Buffer,
|
||||
cursor_id: CursorId,
|
||||
search: Option<&Search>,
|
||||
finder: Option<&Finder>,
|
||||
frame: &mut Rect,
|
||||
) {
|
||||
// Add frame
|
||||
|
|
@ -296,7 +297,7 @@ impl Input {
|
|||
}
|
||||
None => (Color::Reset, ' '),
|
||||
};
|
||||
let bg = match search.map(|s| s.contains(pos?)) {
|
||||
let bg = match finder.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() => {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ mod input;
|
|||
mod panes;
|
||||
mod prompt;
|
||||
mod root;
|
||||
mod search;
|
||||
mod status;
|
||||
|
||||
pub use self::{
|
||||
doc::{Doc, Search},
|
||||
doc::{Doc, Finder},
|
||||
input::Input,
|
||||
panes::{Pane, Panes},
|
||||
prompt::{Confirm, Opener, Prompt, Show, Switcher},
|
||||
root::Root,
|
||||
search::Searcher,
|
||||
status::Status,
|
||||
};
|
||||
|
||||
|
|
@ -156,10 +158,10 @@ impl<T: Clone> Element<T> for Options<T> {
|
|||
Some(Action::Move(dir, Dist::Char, false, false)) => {
|
||||
match dir {
|
||||
Dir::Up => {
|
||||
self.selected =
|
||||
(self.selected + self.ranking.len() - 1) % self.ranking.len()
|
||||
self.selected = (self.selected + self.ranking.len()).saturating_sub(1)
|
||||
% self.ranking.len().max(1)
|
||||
}
|
||||
Dir::Down => self.selected = (self.selected + 1) % self.ranking.len(),
|
||||
Dir::Down => self.selected = (self.selected + 1) % self.ranking.len().max(1),
|
||||
_ => return Err(event),
|
||||
}
|
||||
Ok(Resp::handled(None))
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ 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(cmd) => Err(format!("Unknown command `{cmd}`")),
|
||||
None => Err(format!("No command entered")),
|
||||
}
|
||||
|
|
@ -397,7 +401,7 @@ impl Element<()> for Opener {
|
|||
self.set_string(&format!("{}/", file.path.display()));
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Ok(Some(file)) => Ok(Resp::end(Some(Action::OpenFile(file.path).into()))),
|
||||
Ok(Some(file)) => Ok(Resp::end(Some(Action::OpenFile(file.path, 0).into()))),
|
||||
Ok(None) => Ok(Resp::handled(None)),
|
||||
Err(event) => {
|
||||
let res = self
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub enum Task {
|
|||
Confirm(Confirm),
|
||||
Switcher(Switcher),
|
||||
Opener(Opener),
|
||||
Searcher(Searcher),
|
||||
}
|
||||
|
||||
impl Task {
|
||||
|
|
@ -23,6 +24,7 @@ impl Task {
|
|||
Self::Confirm(c) => c.requested_height(),
|
||||
Self::Switcher(s) => s.requested_height(),
|
||||
Self::Opener(o) => o.requested_height(),
|
||||
Self::Searcher(s) => s.requested_height(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +60,7 @@ impl Element<()> for Root {
|
|||
Task::Confirm(c) => c.handle(state, event),
|
||||
Task::Switcher(s) => s.handle(state, event),
|
||||
Task::Opener(o) => o.handle(state, event),
|
||||
Task::Searcher(s) => s.handle(state, event),
|
||||
};
|
||||
|
||||
match res {
|
||||
|
|
@ -98,6 +101,10 @@ impl Element<()> for Root {
|
|||
self.tasks.clear(); // Overrides all
|
||||
self.tasks.push(Task::Opener(Opener::new(path)));
|
||||
}
|
||||
Action::OpenSearcher(path, needle) => {
|
||||
self.tasks.clear(); // Overrides all
|
||||
self.tasks.push(Task::Searcher(Searcher::new(path, needle)));
|
||||
}
|
||||
Action::CommandStart(cmd) => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks
|
||||
|
|
@ -158,6 +165,7 @@ impl Visual for Root {
|
|||
Task::Confirm(c) => c.render(state, frame),
|
||||
Task::Switcher(s) => s.render(state, frame),
|
||||
Task::Opener(o) => o.render(state, frame),
|
||||
Task::Searcher(s) => s.render(state, frame),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
182
src/ui/search.rs
Normal file
182
src/ui/search.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
use super::*;
|
||||
use crate::state::{Buffer, CursorId};
|
||||
use std::fs;
|
||||
|
||||
pub struct Searcher {
|
||||
options: Options<SearchResult>,
|
||||
path: PathBuf,
|
||||
needle: String,
|
||||
// Filter
|
||||
buffer: Buffer,
|
||||
cursor_id: CursorId,
|
||||
input: Input,
|
||||
}
|
||||
|
||||
impl Searcher {
|
||||
pub fn new(mut path: PathBuf, needle: String) -> Self {
|
||||
let path = loop {
|
||||
if let Ok(mut entries) = fs::read_dir(&path)
|
||||
&& entries.any(|e| {
|
||||
e.map_or(false, |e| {
|
||||
e.file_name() == ".git" && e.file_type().map_or(false, |t| t.is_dir())
|
||||
})
|
||||
})
|
||||
{
|
||||
break path;
|
||||
} else if !path.pop() {
|
||||
break std::env::current_dir().expect("No cwd");
|
||||
}
|
||||
};
|
||||
|
||||
fn search_in(path: &PathBuf, needle: &str, results: &mut Vec<SearchResult>) {
|
||||
// Cap reached!
|
||||
if results.len() < 500 {
|
||||
// Skip hidden files
|
||||
if path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("")
|
||||
.starts_with(".")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(file) = fs::File::open(path)
|
||||
&& let Ok(md) = file.metadata()
|
||||
// Maximum 1 MB
|
||||
&& md.len() < 1 << 20
|
||||
&& let Ok(s) = fs::read_to_string(path)
|
||||
{
|
||||
for (line_idx, line_text) in
|
||||
s.lines().enumerate().filter(|(_, l)| l.contains(needle))
|
||||
{
|
||||
results.push(SearchResult {
|
||||
path: path.clone(),
|
||||
line_idx,
|
||||
line_text: line_text.trim().to_string(),
|
||||
});
|
||||
}
|
||||
} else if let Ok(entries) = fs::read_dir(path) {
|
||||
// Special case, ignore Rust target dir to prevent searching too many places
|
||||
{
|
||||
let mut path = path.clone();
|
||||
path.push("CACHEDIR.TAG");
|
||||
if path.exists() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
let Ok(entry) = entry else { continue };
|
||||
search_in(&entry.path(), needle, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
search_in(&path, &needle, &mut results);
|
||||
|
||||
let mut buffer = Buffer::default();
|
||||
let cursor_id = buffer.start_session();
|
||||
|
||||
Self {
|
||||
options: Options::new(results),
|
||||
path,
|
||||
needle,
|
||||
cursor_id,
|
||||
buffer,
|
||||
input: Input::filter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn requested_height(&self) -> usize {
|
||||
self.options.requested_height() + 3
|
||||
}
|
||||
|
||||
fn update_completions(&mut self) {
|
||||
let filter = self.buffer.text.to_string().to_lowercase();
|
||||
self.options.apply_scoring(|e| {
|
||||
let name = format!("{}", e.path.display()).to_lowercase();
|
||||
if name == filter {
|
||||
Some((0, name.chars().count()))
|
||||
} else if name.starts_with(&filter) {
|
||||
Some((1, name.chars().count()))
|
||||
} else if name.contains(&filter) {
|
||||
Some((2, name.chars().count()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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))) {
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
_ => match self.options.handle(state, event).map(Resp::into_ended) {
|
||||
// Selecting a directory enters the directory
|
||||
Ok(Some(result)) => Ok(Resp::end(Some(Event::Action(Action::OpenFile(
|
||||
result.path,
|
||||
result.line_idx,
|
||||
))))),
|
||||
Ok(None) => Ok(Resp::handled(None)),
|
||||
Err(event) => {
|
||||
let res = self
|
||||
.input
|
||||
.handle(&mut self.buffer, self.cursor_id, event)
|
||||
.map(Resp::into_can_end);
|
||||
self.update_completions();
|
||||
res
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SearchResult {
|
||||
pub path: PathBuf,
|
||||
pub line_idx: usize,
|
||||
pub line_text: String,
|
||||
}
|
||||
|
||||
impl Visual for SearchResult {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let name = match self.path.file_name().and_then(|n| n.to_str()) {
|
||||
Some(name) => format!("{name}"),
|
||||
None => format!("Unknown"),
|
||||
};
|
||||
frame
|
||||
.with_fg(state.theme.option_file)
|
||||
.text([0, 0], &format!("{name}:{}", self.line_idx + 1));
|
||||
frame.with_fg(state.theme.margin_line_num).with(|f| {
|
||||
f.text(
|
||||
[f.size()[0] as isize / 3, 0],
|
||||
&format!("{}", self.line_text),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Searcher {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
frame
|
||||
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
|
||||
.with(|f| self.options.render(state, f));
|
||||
frame
|
||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||
.with(|f| {
|
||||
let title = format!(
|
||||
"{} of {} results for '{}' in {}/",
|
||||
self.options.selected + 1,
|
||||
self.options.ranking.len(),
|
||||
self.needle,
|
||||
self.path.display()
|
||||
);
|
||||
self.input
|
||||
.render(state, Some(&title), &self.buffer, self.cursor_id, None, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue