use super::*;
impl AppState {
pub(super) fn close_find(&mut self) {
if matches!(self.dialog, Some(Dialog::Prompt(..))) {
self.dialog = None;
}
self.find_shown = false;
self.find_note = None;
}
pub fn open_prompt(&mut self, kind: PromptKind) {
if !self.can_find() {
return;
}
let mut input = TextInput::new();
if kind == PromptKind::Find {
input.set(self.find_term.clone());
}
self.dialog = Some(Dialog::Prompt(kind, input));
}
pub fn can_find(&self) -> bool {
match &self.screen {
Screen::Editor(_) => true,
Screen::Viewer(state) => !state.from_edit && state.mode != ViewMode::Image,
Screen::Panels | Screen::Diff(_) => false,
}
}
pub fn confirm_prompt(&mut self) {
let Some(Dialog::Prompt(kind, input)) = self.dialog.take() else {
return;
};
match kind {
PromptKind::Find => {
if input.text.is_empty() {
self.find_shown = false;
return;
}
self.find_term = input.text;
self.find(true, true);
}
PromptKind::GoToLine => match input.text.trim().parse::<usize>() {
Ok(number) => self.go_to_line(number),
Err(_) if input.text.trim().is_empty() => {}
Err(_) => self.find_note = Some(format!("Not a line number: {}", input.text.trim())),
},
PromptKind::Select | PromptKind::Deselect => {
let Some(glob) = crate::glob::Glob::new(&input.text) else {
return;
};
self.select_pattern = input.text;
self.select_by(&glob, kind == PromptKind::Select);
}
PromptKind::Replace => {
if input.text.is_empty() {
return;
}
self.find_term = input.text;
let mut with = TextInput::new();
with.set(self.replace_with.clone());
self.dialog = Some(Dialog::Prompt(PromptKind::ReplaceWith, with));
}
PromptKind::ReplaceWith => {
self.replace_with = input.text;
self.start_replace();
}
PromptKind::GoTo => self.go_to_path(&input.text),
PromptKind::Filter => {
self.filter_before.clear();
self.set_filter(self.is_left_active, &input.text);
}
}
}
pub fn open_replace(&mut self) {
let Some(state) = self.editor() else {
return;
};
let offered = state.selected_text().filter(|text| !text.contains('\n')).unwrap_or_else(|| self.find_term.clone());
let mut input = TextInput::new();
input.set(offered);
self.dialog = Some(Dialog::Prompt(PromptKind::Replace, input));
}
fn start_replace(&mut self) {
let Some(needle) = crate::find::Needle::new(&self.find_term) else {
return;
};
let Some(state) = self.editor() else {
return;
};
let stop_at = state.selection().map_or((state.cursor_line, state.cursor_col), |(start, _)| start);
self.find_shown = true;
match self.editor_find(&needle, true, true) {
Some(wrapped) => self.dialog = Some(Dialog::Replacing { stop_at, wrapped, replaced: 0 }),
None => self.find_note = Some(format!("Not found: {}", self.find_term)),
}
}
pub fn replace_step(&mut self, replace: bool) -> bool {
let Some(Dialog::Replacing { mut stop_at, mut wrapped, mut replaced }) = self.dialog.take() else {
return false;
};
let Some(needle) = crate::find::Needle::new(&self.find_term) else {
return false;
};
if replace && let Some(((line, start), (_, end))) = self.editor().and_then(EditorState::selection) {
let with = self.replace_with.clone();
self.replace_selection(&with);
replaced += 1;
if wrapped && line == stop_at.0 && start < stop_at.1 {
stop_at.1 = (stop_at.1 + with.chars().count()).saturating_sub(end - start);
}
}
let next = self.editor_find(&needle, true, false);
let at = self.editor().and_then(EditorState::selection).map(|(start, _)| start);
let finished = |went_round: bool, at: (usize, usize)| (wrapped && went_round) || ((wrapped || went_round) && at >= stop_at);
match (next, at) {
(Some(went_round), Some(at)) if !finished(went_round, at) => {
wrapped |= went_round;
self.dialog = Some(Dialog::Replacing { stop_at, wrapped, replaced });
true
}
_ => {
self.finish_replace(replaced);
false
}
}
}
pub fn replace_all(&mut self) {
let Some(state) = self.editor_mut() else {
return;
};
let before = state.lines.clone();
let cursor = (state.cursor_line, state.cursor_col);
let history = std::mem::take(&mut state.undo_stack);
let line_states = std::mem::take(&mut state.line_states);
while self.replace_step(true) {}
if let Some(state) = self.editor_mut() {
state.line_states = line_states;
let changed = state.lines != before;
if changed && !state.line_states.is_empty() {
let differs = |index: &usize| state.lines.get(*index) != before.get(*index);
let (first, last) = if state.lines.len() == before.len() {
let last_index = state.lines.len().saturating_sub(1);
((0..state.lines.len()).find(differs).unwrap_or(0), (0..state.lines.len()).rev().find(differs).unwrap_or(last_index))
} else {
(0, state.lines.len().saturating_sub(1))
};
let extension = state.file_path.extension().and_then(|e| e.to_str()).unwrap_or("").to_string();
crate::viewer::highlight_through(&state.lines, &extension, first, last, &mut state.highlighted_lines, &mut state.line_states);
}
state.undo_stack = history;
if changed {
state.undo_stack.push(EditStep {
first_line: 0,
total_lines_before: before.len(),
before,
cursor,
});
if state.undo_stack.len() > crate::constants::UNDO_LIMIT {
state.undo_stack.remove(0);
}
}
}
}
pub fn finish_replace(&mut self, replaced: usize) {
if let Some(Dialog::Replacing { .. }) = self.dialog {
self.dialog = None;
}
self.find_note = Some(match replaced {
0 => "Nothing replaced".to_string(),
1 => "Replaced 1".to_string(),
many => format!("Replaced {many}"),
});
}
fn replace_selection(&mut self, text: &str) {
if !text.is_empty() {
self.editor_insert_text(text);
return;
}
let line = self.editor().map_or(0, |state| state.cursor_line);
self.push_undo(self.edit_range(line..=line));
if let Some(from) = self.delete_selection() {
self.editor_rehighlight_from(from);
}
self.finish_edit();
}
pub fn find(&mut self, forward: bool, fresh: bool) {
let Some(needle) = crate::find::Needle::new(&self.find_term) else {
self.open_prompt(PromptKind::Find);
return;
};
if !self.can_find() {
return;
}
self.find_shown = true;
let found = if self.is_editing() { self.editor_find(&needle, forward, fresh) } else { self.viewer_find(&needle, forward, fresh) };
self.find_note = match found {
None => Some(format!("Not found: {}", self.find_term)),
Some(true) => Some(if forward { "Wrapped to the top" } else { "Wrapped to the bottom" }.to_string()),
Some(false) => None,
};
}
fn editor_find(&mut self, needle: &crate::find::Needle, forward: bool, fresh: bool) -> Option<bool> {
let height = self.editor_viewport_height.max(1);
let state = self.editor_mut()?;
let cursor = (state.cursor_line, state.cursor_col);
let start = state.selection().map_or(cursor, |(start, _)| start);
let from = if forward && !fresh { cursor } else { start };
let lines = &state.lines;
let found = crate::find::search(needle, lines.len(), |index| lines[index].clone(), from, forward)?;
state.selection_anchor = Some((found.line, found.start));
state.cursor_line = found.line;
state.cursor_col = found.end;
state.auto_scroll = true;
if found.line < state.scroll_offset || found.line >= state.scroll_offset + height {
state.scroll_offset = found.line.saturating_sub(height / 3);
}
Some(found.wrapped)
}
fn viewer_find(&mut self, needle: &crate::find::Needle, forward: bool, fresh: bool) -> Option<bool> {
let (height, width) = (self.viewer_viewport_height.max(1), self.viewer_viewport_width.max(1));
let state = self.viewer_mut()?;
let from = match state.selected_range() {
Some((_, end)) if forward && !fresh => end,
Some((start, _)) => start,
None => {
let (line, from, _) = state.row(state.scroll_offset);
(line, from)
}
};
let found = crate::find::search(needle, state.line_count(), |index| state.line_text(index), from, forward)?;
state.selection = Some(((found.line, found.start), (found.line, found.end)));
let row = state.row_of(found.line, found.start);
if row < state.scroll_offset || row >= state.scroll_offset + height {
let max = state.total_lines.saturating_sub(height);
state.scroll_offset = row.saturating_sub(height / 3).min(max);
}
if !state.wrapping() {
let text = state.line_text(found.line);
let (start, end) = (crate::utils::display_column(&text, found.start), crate::utils::display_column(&text, found.end));
if start < state.horizontal_offset || end > state.horizontal_offset + width {
state.horizontal_offset = start.saturating_sub(width / 4);
}
}
Some(found.wrapped)
}
pub fn go_to_line(&mut self, number: usize) {
if self.is_editing() {
let height = self.editor_viewport_height.max(1);
if let Screen::Editor(state) = &mut self.screen {
let line = number.clamp(1, state.lines.len().max(1)) - 1;
state.selection_anchor = None;
state.cursor_line = line;
state.cursor_col = 0;
state.auto_scroll = true;
if line < state.scroll_offset || line >= state.scroll_offset + height {
state.scroll_offset = line.saturating_sub(height / 3);
}
}
} else if let Screen::Viewer(state) = &mut self.screen {
let line = number.clamp(1, state.line_count().max(1)) - 1;
state.scroll_offset = state.row_of(line, 0).min(state.total_lines.saturating_sub(self.viewer_viewport_height));
}
}
}