mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-12 04:58:57 +00:00
5f8f799cf7
quick_replace '\\\\return(s)? ' 'Return$1 ' src/ Filtered to only lines beginning with //
280 lines
8.6 KiB
Rust
280 lines
8.6 KiB
Rust
//! Encapsulation of the reader's history search functionality.
|
|
|
|
use crate::history::{self, History, HistorySearch, SearchDirection, SearchFlags, SearchType};
|
|
use crate::parse_constants::SourceRange;
|
|
use crate::tokenizer::{TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED};
|
|
use crate::wchar::prelude::*;
|
|
use crate::wcstringutil::ifind;
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
|
|
// Make the search case-insensitive unless we have an uppercase character.
|
|
pub fn smartcase_flags(query: &wstr) -> history::SearchFlags {
|
|
if query == query.to_lowercase() {
|
|
history::SearchFlags::IGNORE_CASE
|
|
} else {
|
|
history::SearchFlags::default()
|
|
}
|
|
}
|
|
|
|
struct SearchMatch {
|
|
/// The text of the match.
|
|
pub text: WString,
|
|
/// The offset of the current search string in this match.
|
|
offset: usize,
|
|
}
|
|
|
|
impl SearchMatch {
|
|
fn new(text: WString, offset: usize) -> Self {
|
|
Self { text, offset }
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Eq, Default, PartialEq)]
|
|
pub enum SearchMode {
|
|
#[default]
|
|
/// no search
|
|
Inactive,
|
|
/// searching by line
|
|
Line,
|
|
/// searching by prefix
|
|
Prefix,
|
|
/// searching by token
|
|
Token,
|
|
}
|
|
|
|
/// Encapsulation of the reader's history search functionality.
|
|
#[derive(Default)]
|
|
pub struct ReaderHistorySearch {
|
|
/// The type of search performed.
|
|
mode: SearchMode,
|
|
|
|
/// Our history search itself.
|
|
search: Option<HistorySearch>,
|
|
|
|
/// The ordered list of matches. This may grow long.
|
|
matches: Vec<SearchMatch>,
|
|
|
|
/// A set of new items to skip, corresponding to matches_ and anything added in skip().
|
|
skips: HashSet<WString>,
|
|
|
|
/// Index into our matches list.
|
|
match_index: usize,
|
|
|
|
/// The offset of the current token in the command line. Only non-zero for a token search.
|
|
token_offset: usize,
|
|
}
|
|
|
|
impl ReaderHistorySearch {
|
|
pub fn active(&self) -> bool {
|
|
self.mode != SearchMode::Inactive
|
|
}
|
|
pub fn by_token(&self) -> bool {
|
|
self.mode == SearchMode::Token
|
|
}
|
|
pub fn by_line(&self) -> bool {
|
|
self.mode == SearchMode::Line
|
|
}
|
|
pub fn by_prefix(&self) -> bool {
|
|
self.mode == SearchMode::Prefix
|
|
}
|
|
|
|
/// Move the history search in the given direction `dir`.
|
|
pub fn move_in_direction(&mut self, dir: SearchDirection) -> bool {
|
|
if dir == SearchDirection::Forward {
|
|
self.move_forwards()
|
|
} else {
|
|
self.move_backwards()
|
|
}
|
|
}
|
|
|
|
/// Go to the beginning (earliest) of the search.
|
|
pub fn go_to_beginning(&mut self) {
|
|
if self.matches.is_empty() {
|
|
return;
|
|
}
|
|
self.match_index = self.matches.len() - 1;
|
|
}
|
|
|
|
/// Go to the end (most recent) of the search.
|
|
pub fn go_to_end(&mut self) {
|
|
self.match_index = 0;
|
|
}
|
|
|
|
/// Return the current search result.
|
|
pub fn current_result(&self) -> &wstr {
|
|
&self.matches[self.match_index].text
|
|
}
|
|
|
|
/// Return the string we are searching for.
|
|
pub fn search_string(&self) -> &wstr {
|
|
self.search().original_term()
|
|
}
|
|
|
|
/// Return the range of the original search string in the new command line.
|
|
pub fn search_range_if_active(&self) -> Option<SourceRange> {
|
|
if !self.active() || self.is_at_end() {
|
|
return None;
|
|
}
|
|
Some(SourceRange::new(
|
|
self.token_offset + self.matches[self.match_index].offset,
|
|
self.search_string().len(),
|
|
))
|
|
}
|
|
|
|
/// Return whether we are at the end (most recent) of our search.
|
|
pub fn is_at_end(&self) -> bool {
|
|
self.match_index == 0
|
|
}
|
|
|
|
// Add an item to skip.
|
|
// Return true if it was added, false if already present.
|
|
pub fn add_skip(&mut self, s: WString) -> bool {
|
|
self.skips.insert(s)
|
|
}
|
|
|
|
pub fn handle_deletion(&mut self) {
|
|
assert!(!self.is_at_end());
|
|
self.matches.remove(self.match_index);
|
|
self.match_index -= 1;
|
|
self.search_mut().prepare_to_search_after_deletion();
|
|
self.move_backwards();
|
|
}
|
|
|
|
/// Reset, beginning a new line or token mode search.
|
|
pub fn reset_to_mode(
|
|
&mut self,
|
|
text: WString,
|
|
hist: Arc<History>,
|
|
mode: SearchMode,
|
|
token_offset: usize,
|
|
) {
|
|
assert!(
|
|
mode != SearchMode::Inactive,
|
|
"mode cannot be inactive in this setter"
|
|
);
|
|
self.skips = HashSet::from([text.clone()]);
|
|
self.matches = vec![SearchMatch::new(text.clone(), 0)];
|
|
self.match_index = 0;
|
|
self.mode = mode;
|
|
self.token_offset = token_offset;
|
|
let flags = SearchFlags::NO_DEDUP | smartcase_flags(&text);
|
|
// We can skip dedup in history_search_t because we do it ourselves in skips_.
|
|
self.search = Some(HistorySearch::new_with(
|
|
hist,
|
|
text,
|
|
if self.by_prefix() {
|
|
SearchType::Prefix
|
|
} else {
|
|
SearchType::Contains
|
|
},
|
|
flags,
|
|
0,
|
|
));
|
|
}
|
|
|
|
/// Reset to inactive search.
|
|
pub fn reset(&mut self) {
|
|
self.matches.clear();
|
|
self.skips.clear();
|
|
self.match_index = 0;
|
|
self.mode = SearchMode::Inactive;
|
|
self.token_offset = 0;
|
|
self.search = None;
|
|
}
|
|
|
|
/// Adds the given match if we haven't seen it before.
|
|
fn add_if_new(&mut self, search_match: SearchMatch) {
|
|
if self.add_skip(search_match.text.clone()) {
|
|
self.matches.push(search_match);
|
|
}
|
|
}
|
|
|
|
/// Attempt to append matches from the current history item.
|
|
/// Return true if something was appended.
|
|
fn append_matches_from_search(&mut self) -> bool {
|
|
fn find(zelf: &ReaderHistorySearch, haystack: &wstr, needle: &wstr) -> Option<usize> {
|
|
if zelf.search().ignores_case() {
|
|
return ifind(haystack, needle, false);
|
|
}
|
|
haystack.find(needle)
|
|
}
|
|
let before = self.matches.len();
|
|
let text = self.search().current_string();
|
|
let needle = self.search_string();
|
|
if matches!(self.mode, SearchMode::Line | SearchMode::Prefix) {
|
|
// FIXME: Previous versions asserted out if this wasn't true.
|
|
// This could be hit with a needle of "ö" and haystack of "echo Ö"
|
|
// I'm not sure why - this points to a bug in ifind (probably wrong locale?)
|
|
// However, because the user experience of having it crash is horrible,
|
|
// and the worst thing that can otherwise happen here is that a search is unsuccessful,
|
|
// we just check it instead.
|
|
if let Some(offset) = find(self, text, needle) {
|
|
self.add_if_new(SearchMatch::new(text.to_owned(), offset));
|
|
}
|
|
} else if self.mode == SearchMode::Token {
|
|
let mut tok = Tokenizer::new(text, TOK_ACCEPT_UNFINISHED);
|
|
|
|
let mut local_tokens = vec![];
|
|
while let Some(token) = tok.next() {
|
|
if token.type_ != TokenType::string {
|
|
continue;
|
|
}
|
|
let text = tok.text_of(&token);
|
|
if let Some(offset) = find(self, text, needle) {
|
|
local_tokens.push(SearchMatch::new(text.to_owned(), offset));
|
|
}
|
|
}
|
|
|
|
// Make sure tokens are added in reverse order. See #5150
|
|
for tok in local_tokens.into_iter().rev() {
|
|
self.add_if_new(tok);
|
|
}
|
|
}
|
|
self.matches.len() > before
|
|
}
|
|
|
|
fn move_forwards(&mut self) -> bool {
|
|
// Try to move within our previously discovered matches.
|
|
if self.match_index > 0 {
|
|
self.match_index -= 1;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn move_backwards(&mut self) -> bool {
|
|
// Try to move backwards within our previously discovered matches.
|
|
if self.match_index + 1 < self.matches.len() {
|
|
self.match_index += 1;
|
|
return true;
|
|
}
|
|
|
|
// Add more items from our search.
|
|
while self
|
|
.search_mut()
|
|
.go_to_next_match(SearchDirection::Backward)
|
|
{
|
|
if self.append_matches_from_search() {
|
|
self.match_index += 1;
|
|
assert!(
|
|
self.match_index < self.matches.len(),
|
|
"Should have found more matches"
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Here we failed to go backwards past the last history item.
|
|
false
|
|
}
|
|
|
|
fn search(&self) -> &HistorySearch {
|
|
self.search.as_ref().unwrap()
|
|
}
|
|
|
|
fn search_mut(&mut self) -> &mut HistorySearch {
|
|
self.search.as_mut().unwrap()
|
|
}
|
|
}
|