Skip to content

Commit

Permalink
feat: search
Browse files Browse the repository at this point in the history
  • Loading branch information
kruserr committed Nov 26, 2024
1 parent 732b58a commit 5f68100
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 26 deletions.
214 changes: 199 additions & 15 deletions cli-text-reader/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crossterm::{
event::{self, Event as CEvent, KeyCode},
execute,
terminal::{self, Clear, ClearType},
style::{SetBackgroundColor, Color},
style::{SetBackgroundColor, Color, SetForegroundColor, ResetColor},
};

use crate::config::load_config;
Expand All @@ -15,18 +15,28 @@ use crate::tutorial::get_tutorial_text;
pub enum EditorMode {
Normal,
Command,
Search,
ReverseSearch,
}

pub struct EditorState {
pub mode: EditorMode,
pub command_buffer: String,
pub search_query: String,
pub search_direction: bool, // true for forward, false for backward
pub last_search_index: Option<usize>,
pub current_match: Option<(usize, usize, usize)>, // (line_index, start, end)
}

impl EditorState {
pub fn new() -> Self {
Self {
mode: EditorMode::Normal,
command_buffer: String::new(),
search_query: String::new(),
search_direction: true,
last_search_index: None,
current_match: None,
}
}
}
Expand Down Expand Up @@ -111,25 +121,70 @@ impl Editor {
let tutorial_lines = get_tutorial_text();

if std::io::stdout().is_terminal() {
execute!(stdout, terminal::EnterAlternateScreen, Hide)?;
terminal::enable_raw_mode()?;

let center_offset = if self.width > self.col { (self.width / 2) - self.col / 2 } else { 0 };

execute!(stdout, MoveTo(0, 0), Clear(ClearType::All))?;
// Save current state
// let was_alternate = terminal::is_alternate_screen_active()?;
let was_raw = terminal::is_raw_mode_enabled()?;

for (i, line) in tutorial_lines.iter().enumerate() {
execute!(stdout, MoveTo(center_offset as u16, (self.height/2 - tutorial_lines.len()/2 + i) as u16))?;
println!("{}", line);
// Setup tutorial display
// if !was_alternate {
// execute!(stdout, terminal::EnterAlternateScreen)?;
// }
if !was_raw {
terminal::enable_raw_mode()?;
}
execute!(stdout, Hide)?;

stdout.flush()?;

while let Ok(event) = event::read() {
if let CEvent::Key(_) = event {
break;
let mut tutorial_offset = 0;
loop {
// Display tutorial with scrolling
execute!(stdout, Clear(ClearType::All))?;
let center_offset = if self.width > self.col { (self.width / 2) - self.col / 2 } else { 0 };

for (i, line) in tutorial_lines.iter()
.skip(tutorial_offset)
.take(self.height)
.enumerate()
{
execute!(stdout, MoveTo(center_offset as u16, i as u16))?;
println!("{}", line);
}

stdout.flush()?;

// Handle scrolling input
match event::read()? {
CEvent::Key(key_event) => match key_event.code {
KeyCode::Char('j') | KeyCode::Down => {
if tutorial_offset + self.height < tutorial_lines.len() {
tutorial_offset += 1;
}
}
KeyCode::Char('k') | KeyCode::Up => {
if tutorial_offset > 0 {
tutorial_offset -= 1;
}
}
KeyCode::PageDown => {
tutorial_offset = (tutorial_offset + self.height)
.min(tutorial_lines.len().saturating_sub(self.height));
}
KeyCode::PageUp => {
tutorial_offset = tutorial_offset.saturating_sub(self.height);
}
_ => break,
},
_ => {}
}
}

// Restore original state
execute!(stdout, Clear(ClearType::All))?;
// if !was_alternate {
// execute!(stdout, terminal::LeaveAlternateScreen)?;
// }
if !was_raw {
terminal::disable_raw_mode()?;
}
}

Ok(())
Expand Down Expand Up @@ -167,6 +222,19 @@ impl Editor {
execute!(stdout, MoveTo(0, i as u16))?;
}

// Handle search highlight
if let Some((line_idx, start, end)) = self.editor_state.current_match {
if line_idx == self.offset + i {
print!("{}", center_offset_string);
print!("{}", &line[..start]);
execute!(stdout, SetBackgroundColor(Color::Yellow), SetForegroundColor(Color::Black))?;
print!("{}", &line[start..end]);
execute!(stdout, ResetColor)?;
println!("{}", &line[end..]);
continue;
}
}

println!("{}{}", center_offset_string, line);

if self.show_highlighter && i == self.height / 2 {
Expand All @@ -177,6 +245,12 @@ impl Editor {
if self.editor_state.mode == EditorMode::Command {
execute!(stdout, MoveTo(0, (self.height - 1) as u16))?;
print!(":{}", self.editor_state.command_buffer);
} else if self.editor_state.mode == EditorMode::Search {
execute!(stdout, MoveTo(0, (self.height - 1) as u16))?;
print!("/{}", self.editor_state.command_buffer);
} else if self.editor_state.mode == EditorMode::ReverseSearch {
execute!(stdout, MoveTo(0, (self.height - 1) as u16))?;
print!("?{}", self.editor_state.command_buffer);
}

// Show progress if enabled
Expand All @@ -199,6 +273,30 @@ impl Editor {
self.editor_state.mode = EditorMode::Command;
self.editor_state.command_buffer.clear();
},
KeyCode::Char('/') => {
self.editor_state.mode = EditorMode::Search;
self.editor_state.command_buffer.clear();
self.editor_state.search_direction = true;
},
KeyCode::Char('?') => {
self.editor_state.mode = EditorMode::ReverseSearch;
self.editor_state.command_buffer.clear();
self.editor_state.search_direction = false;
},
KeyCode::Char('n') => {
if !self.editor_state.search_query.is_empty() {
// Use the original search direction
self.find_next_match(self.editor_state.search_direction);
self.center_on_match();
}
},
KeyCode::Char('N') => {
if !self.editor_state.search_query.is_empty() {
// Use opposite of original search direction
self.find_next_match(!self.editor_state.search_direction);
self.center_on_match();
}
},
KeyCode::Char('j') | KeyCode::Down => {
if self.offset + self.height < self.total_lines {
self.offset += 1;
Expand All @@ -223,6 +321,27 @@ impl Editor {
}
_ => {}
},
EditorMode::Search | EditorMode::ReverseSearch => match key_event.code {
KeyCode::Esc => {
self.editor_state.mode = EditorMode::Normal;
self.editor_state.command_buffer.clear();
},
KeyCode::Enter => {
self.editor_state.search_query = self.editor_state.command_buffer.clone();
// Start from current position
self.find_next_match(self.editor_state.mode == EditorMode::Search);
self.center_on_match();
self.editor_state.mode = EditorMode::Normal;
self.editor_state.command_buffer.clear();
},
KeyCode::Backspace => {
self.editor_state.command_buffer.pop();
},
KeyCode::Char(c) => {
self.editor_state.command_buffer.push(c);
},
_ => {}
},
EditorMode::Command => match key_event.code {
KeyCode::Esc => {
self.editor_state.mode = EditorMode::Normal;
Expand Down Expand Up @@ -277,6 +396,71 @@ impl Editor {
cmd => Ok(handle_command(cmd, &mut self.show_highlighter))
}
}

fn find_next_match(&mut self, forward: bool) {
if self.editor_state.search_query.is_empty() {
return;
}

let query = self.editor_state.search_query.to_lowercase();
let start_idx = if let Some((idx, _, _)) = self.editor_state.current_match {
idx
} else {
self.offset
};

let find_in_line = |line: &str, query: &str| -> Option<(usize, usize)> {
line.to_lowercase()
.find(&query)
.map(|start| (start, start + query.len()))
};

if forward {
// Forward search
for i in start_idx + 1..self.lines.len() {
if let Some((start, end)) = find_in_line(&self.lines[i], &query) {
self.editor_state.current_match = Some((i, start, end));
return;
}
}
// Wrap around to beginning
for i in 0..=start_idx {
if let Some((start, end)) = find_in_line(&self.lines[i], &query) {
self.editor_state.current_match = Some((i, start, end));
return;
}
}
} else {
// Backward search
for i in (0..start_idx).rev() {
if let Some((start, end)) = find_in_line(&self.lines[i], &query) {
self.editor_state.current_match = Some((i, start, end));
return;
}
}
// Wrap around to end
for i in (start_idx..self.lines.len()).rev() {
if let Some((start, end)) = find_in_line(&self.lines[i], &query) {
self.editor_state.current_match = Some((i, start, end));
return;
}
}
}
}

fn center_on_match(&mut self) {
if let Some((line_idx, _, _)) = self.editor_state.current_match {
let half_height = (self.height / 2) as i32;
let new_offset = line_idx as i32 - half_height;
self.offset = if new_offset < 0 {
0
} else if new_offset + self.height as i32 > self.total_lines as i32 {
self.total_lines - self.height
} else {
new_offset as usize
};
}
}
}

pub fn handle_command(command: &str, show_highlighter: &mut bool) -> bool {
Expand Down
30 changes: 19 additions & 11 deletions cli-text-reader/src/tutorial.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@

pub fn get_tutorial_text() -> Vec<String> {
vec![
"Welcome to the Text Reader!".to_string(),
"Welcome to cli-text-reader!".to_string(),
"".to_string(),
"Basic Controls:".to_string(),
" j or ↓ : Move down one line".to_string(),
" k or ↑ : Move up one line".to_string(),
" PageDown : Move down one page".to_string(),
" PageUp : Move up one page".to_string(),
" :z : Toggle line highlighter".to_string(),
" :q : Quit".to_string(),
" :help : Open this tutorial".to_string(),
" :tutorial : Open this tutorial".to_string(),
"Navigation:".to_string(),
"j or ↓ = scroll down".to_string(),
"k or ↑ = scroll up".to_string(),
"PageDown = scroll down one page".to_string(),
"PageUp = scroll up one page".to_string(),
"".to_string(),
"Search:".to_string(),
"/ = search forward".to_string(),
"? = search backward".to_string(),
"n = next match".to_string(),
"N = previous match".to_string(),
"".to_string(),
"Commands:".to_string(),
": then type command:".to_string(),
"q = quit".to_string(),
"z = toggle line highlighter".to_string(),
"p = toggle progress".to_string(),
"help or tutorial = show this tutorial".to_string(),
"".to_string(),
"Press any key to continue...".to_string(),
]
Expand Down

0 comments on commit 5f68100

Please sign in to comment.