use std::collections::VecDeque;
use super::error::{EvalResult, Flow, signal};
use super::intern::intern;
use super::value::{Value, ValueKind};
use crate::buffer::Buffer;
fn expect_min_max_args(name: &str, args: &[Value], min: usize, max: usize) -> Result<(), Flow> {
if args.len() < min || args.len() > max {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_string(val: &Value) -> Result<String, Flow> {
match val.kind() {
ValueKind::String => Ok(val.as_str().unwrap().to_owned()),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *val],
)),
}
}
fn expect_integer_or_marker(val: &Value) -> Result<i64, Flow> {
match val.kind() {
ValueKind::Fixnum(n) => Ok(n),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("integer-or-marker-p"), *val],
)),
}
}
fn expect_sequence_string(val: &Value) -> Result<String, Flow> {
match val.kind() {
ValueKind::String => Ok(val.as_str().unwrap().to_owned()),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("sequencep"), *val],
)),
}
}
fn lisp_pos_to_byte(buf: &crate::buffer::Buffer, raw: i64) -> usize {
buf.lisp_pos_to_accessible_byte(raw)
}
fn replacement_region_bounds(
buf: &crate::buffer::Buffer,
start_arg: Option<&Value>,
end_arg: Option<&Value>,
backward: bool,
region_noncontiguous: bool,
) -> Result<(usize, usize), Flow> {
if region_noncontiguous {
let mark = buf.mark().ok_or_else(|| {
signal(
"error",
vec![Value::string(
"The mark is not set now, so there is no region",
)],
)
})?;
let pt = buf.point();
return Ok((pt.min(mark), pt.max(mark)));
}
let start = match start_arg {
Some(v) if !v.is_nil() => lisp_pos_to_byte(buf, expect_integer_or_marker(v)?),
_ if backward => buf.point_min(),
_ => buf.point(),
};
let end = match end_arg {
Some(v) if !v.is_nil() => lisp_pos_to_byte(buf, expect_integer_or_marker(v)?),
_ if backward => buf.point(),
_ => buf.point_max(),
};
if start <= end {
Ok((start, end))
} else {
Ok((end, start))
}
}
fn line_operation_region_bounds(
buf: &crate::buffer::Buffer,
start_arg: Option<&Value>,
end_arg: Option<&Value>,
) -> Result<(usize, usize), Flow> {
let start = match start_arg {
Some(v) if !v.is_nil() => lisp_pos_to_byte(buf, expect_integer_or_marker(v)?),
_ => buf.point(),
};
let end = match end_arg {
Some(v) if !v.is_nil() => lisp_pos_to_byte(buf, expect_integer_or_marker(v)?),
_ => buf.point_max(),
};
if start <= end {
Ok((start, end))
} else {
Ok((end, start))
}
}
fn line_start_at_or_before(source: &str, at: usize) -> usize {
let pos = at.min(source.len());
match source[..pos].rfind('\n') {
Some(idx) => idx + 1,
None => 0,
}
}
fn dynamic_or_global_symbol_value(eval: &super::eval::Context, name: &str) -> Option<Value> {
eval.obarray.symbol_value(name).cloned()
}
fn buffer_read_only_active(eval: &super::eval::Context, buf: &Buffer) -> bool {
if buf.read_only {
return true;
}
if let Some(value) = buf.get_buffer_local("buffer-read-only") {
return value.is_truthy();
}
eval.obarray
.symbol_value("buffer-read-only")
.is_some_and(|value| value.is_truthy())
}
fn case_fold_for_pattern(eval: &super::eval::Context, pattern: &str) -> bool {
let case_fold_search_enabled = dynamic_or_global_symbol_value(eval, "case-fold-search")
.map(|value| !value.is_nil())
.unwrap_or(true);
if !case_fold_search_enabled {
return false;
}
let smart_case_enabled = dynamic_or_global_symbol_value(eval, "search-upper-case")
.map(|value| !value.is_nil())
.unwrap_or(true);
if !smart_case_enabled {
return true;
}
resolve_case_fold(None, pattern)
}
fn case_replace_enabled(eval: &super::eval::Context) -> bool {
dynamic_or_global_symbol_value(eval, "case-replace")
.map(|value| !value.is_nil())
.unwrap_or(true)
}
fn replace_lax_whitespace_enabled(eval: &super::eval::Context) -> bool {
dynamic_or_global_symbol_value(eval, "replace-lax-whitespace")
.map(|value| !value.is_nil())
.unwrap_or(false)
}
fn resolve_search_whitespace_regexp(eval: &super::eval::Context) -> Option<String> {
let raw = match dynamic_or_global_symbol_value(eval, "search-whitespace-regexp") {
Some(v) if v.is_string() => v.as_str().unwrap().to_owned(),
Some(v) if v.is_nil() => "[ \t\n\r]+".to_string(),
None => "[ \t\n\r]+".to_string(),
Some(_) => return None,
};
Some(raw)
}
fn quote_emacs_regexp_literal(literal: &str) -> String {
let mut result = String::with_capacity(literal.len() + 8);
for ch in literal.chars() {
match ch {
'.' | '*' | '+' | '?' | '[' | '^' | '$' | '\\' => {
result.push('\\');
result.push(ch);
}
_ => result.push(ch),
}
}
result
}
fn build_lax_whitespace_pattern(pattern: &str, whitespace_regex: &str) -> String {
let mut raw = String::new();
let mut literal = String::new();
let mut in_space_run = false;
for ch in pattern.chars() {
if ch == ' ' {
if !literal.is_empty() {
raw.push_str("e_emacs_regexp_literal(&literal));
literal.clear();
}
if !in_space_run {
raw.push_str("\\(");
raw.push_str(whitespace_regex);
raw.push_str("\\)");
in_space_run = true;
}
} else {
in_space_run = false;
literal.push(ch);
}
}
if !literal.is_empty() {
raw.push_str("e_emacs_regexp_literal(&literal));
}
raw
}
fn string_matches_regexp(text: &str, pattern: &str, case_fold: bool) -> Result<bool, Flow> {
let mut match_data = None;
super::regex::string_match_full_with_case_fold(pattern, text, 0, case_fold, &mut match_data)
.map(|matched| matched.is_some())
.map_err(|e| {
signal(
"invalid-regexp",
vec![Value::string(format!("Invalid regexp: {e}"))],
)
})
}
fn count_string_regexp_matches(text: &str, pattern: &str, case_fold: bool) -> Result<i64, Flow> {
let iterated = super::regex::iterate_string_matches_with_case_fold(pattern, text, 0, case_fold)
.map_err(|e| {
signal(
"invalid-regexp",
vec![Value::string(format!("Invalid regexp: {e}"))],
)
})?;
Ok(iterated
.matches
.into_iter()
.filter_map(|groups| groups.first().and_then(|group| *group))
.filter(|(match_start, match_end)| {
!(*match_start == *match_end && *match_start >= text.len())
})
.count() as i64)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SearchDirection {
Forward,
Backward,
}
pub struct IsearchState {
pub active: bool,
pub direction: SearchDirection,
pub search_string: String,
pub regexp: bool,
pub case_fold: Option<bool>,
pub wrapped: bool,
pub success: bool,
pub match_start: Option<usize>,
pub match_end: Option<usize>,
pub origin: usize,
pub barrier: usize,
pub history_index: Option<usize>,
pub lazy_matches: Vec<(usize, usize)>,
}
pub struct SearchHistory {
strings: VecDeque<String>,
regexp_strings: VecDeque<String>,
max_length: usize,
}
impl SearchHistory {
pub fn new() -> Self {
Self {
strings: VecDeque::new(),
regexp_strings: VecDeque::new(),
max_length: 100,
}
}
pub fn push(&mut self, string: String, regexp: bool) {
let ring = if regexp {
&mut self.regexp_strings
} else {
&mut self.strings
};
if let Some(pos) = ring.iter().position(|s| *s == string) {
ring.remove(pos);
}
ring.push_front(string);
if ring.len() > self.max_length {
ring.pop_back();
}
}
pub fn get(&self, index: usize, regexp: bool) -> Option<&str> {
let ring = if regexp {
&self.regexp_strings
} else {
&self.strings
};
ring.get(index).map(|s| s.as_str())
}
pub fn len(&self, regexp: bool) -> usize {
if regexp {
self.regexp_strings.len()
} else {
self.strings.len()
}
}
pub fn strings(&self, regexp: bool) -> &VecDeque<String> {
if regexp {
&self.regexp_strings
} else {
&self.strings
}
}
}
impl Default for SearchHistory {
fn default() -> Self {
Self::new()
}
}
pub struct IsearchManager {
state: Option<IsearchState>,
history: SearchHistory,
last_search_string: Option<String>,
last_search_regexp: bool,
}
impl IsearchManager {
pub fn new() -> Self {
Self {
state: None,
history: SearchHistory::new(),
last_search_string: None,
last_search_regexp: false,
}
}
pub fn begin_search(&mut self, direction: SearchDirection, regexp: bool, origin: usize) {
self.state = Some(IsearchState {
active: true,
direction,
search_string: String::new(),
regexp,
case_fold: None, wrapped: false,
success: true,
match_start: None,
match_end: None,
origin,
barrier: origin,
history_index: None,
lazy_matches: Vec::new(),
});
}
pub fn end_search(&mut self, save_to_history: bool) {
if let Some(state) = self.state.take() {
if save_to_history && !state.search_string.is_empty() {
self.history.push(state.search_string.clone(), state.regexp);
}
if !state.search_string.is_empty() {
self.last_search_regexp = state.regexp;
self.last_search_string = Some(state.search_string);
}
}
}
pub fn abort_search(&mut self) -> usize {
let origin = self.state.as_ref().map(|s| s.origin).unwrap_or(0);
self.state = None;
origin
}
pub fn add_char(&mut self, ch: char) {
if let Some(state) = self.state.as_mut() {
state.search_string.push(ch);
state.history_index = None;
}
}
pub fn delete_char(&mut self) {
if let Some(state) = self.state.as_mut() {
state.search_string.pop();
state.history_index = None;
}
}
pub fn set_string(&mut self, s: String) {
if let Some(state) = self.state.as_mut() {
state.search_string = s;
state.history_index = None;
}
}
pub fn toggle_regexp(&mut self) {
if let Some(state) = self.state.as_mut() {
state.regexp = !state.regexp;
}
}
pub fn toggle_case_fold(&mut self) {
if let Some(state) = self.state.as_mut() {
state.case_fold = match state.case_fold {
None => Some(true),
Some(true) => Some(false),
Some(false) => None,
};
}
}
pub fn reverse_direction(&mut self) {
if let Some(state) = self.state.as_mut() {
state.direction = match state.direction {
SearchDirection::Forward => SearchDirection::Backward,
SearchDirection::Backward => SearchDirection::Forward,
};
}
}
pub fn search_next(&mut self, text: &str) -> Option<(usize, usize)> {
let state = self.state.as_mut()?;
if state.search_string.is_empty() {
state.success = true;
state.match_start = None;
state.match_end = None;
return None;
}
let case_fold = resolve_case_fold(state.case_fold, &state.search_string);
let from = match state.direction {
SearchDirection::Forward => state.match_end.unwrap_or(state.barrier),
SearchDirection::Backward => state.match_start.unwrap_or(state.barrier),
};
let forward = state.direction == SearchDirection::Forward;
if let Some((start, end)) = find_match(
text,
&state.search_string,
from,
forward,
state.regexp,
case_fold,
) {
state.success = true;
state.match_start = Some(start);
state.match_end = Some(end);
return Some((start, end));
}
if !state.wrapped {
state.wrapped = true;
let wrap_from = if forward { 0 } else { text.len() };
if let Some((start, end)) = find_match(
text,
&state.search_string,
wrap_from,
forward,
state.regexp,
case_fold,
) {
state.success = true;
state.match_start = Some(start);
state.match_end = Some(end);
return Some((start, end));
}
}
state.success = false;
None
}
pub fn search_update(&mut self, text: &str) -> Option<(usize, usize)> {
let state = self.state.as_mut()?;
if state.search_string.is_empty() {
state.success = true;
state.match_start = None;
state.match_end = None;
state.wrapped = false;
return None;
}
let case_fold = resolve_case_fold(state.case_fold, &state.search_string);
let forward = state.direction == SearchDirection::Forward;
if let Some((start, end)) = find_match(
text,
&state.search_string,
state.origin,
forward,
state.regexp,
case_fold,
) {
state.success = true;
state.wrapped = false;
state.match_start = Some(start);
state.match_end = Some(end);
return Some((start, end));
}
let wrap_from = if forward { 0 } else { text.len() };
if let Some((start, end)) = find_match(
text,
&state.search_string,
wrap_from,
forward,
state.regexp,
case_fold,
) {
state.success = true;
state.wrapped = true;
state.match_start = Some(start);
state.match_end = Some(end);
return Some((start, end));
}
state.success = false;
state.wrapped = false;
state.match_start = None;
state.match_end = None;
None
}
pub fn compute_lazy_matches(&mut self, text: &str, visible_start: usize, visible_end: usize) {
let state = match self.state.as_mut() {
Some(s) => s,
None => return,
};
state.lazy_matches.clear();
if state.search_string.is_empty() {
return;
}
let case_fold = resolve_case_fold(state.case_fold, &state.search_string);
let start = visible_start.min(text.len());
let end = visible_end.min(text.len());
if start >= end {
return;
}
let region = &text[start..end];
if state.regexp {
if let Ok(iterated) = super::regex::iterate_string_matches_with_case_fold(
&state.search_string,
region,
0,
case_fold,
) {
for groups in iterated.matches {
let Some((match_start, match_end)) = groups.first().and_then(|group| *group)
else {
continue;
};
if match_start == match_end {
continue;
}
state
.lazy_matches
.push((start + match_start, start + match_end));
}
}
} else {
let haystack = if case_fold {
region.to_lowercase()
} else {
region.to_string()
};
let needle = if case_fold {
state.search_string.to_lowercase()
} else {
state.search_string.clone()
};
let mut search_from = 0;
while let Some(pos) = haystack[search_from..].find(&needle) {
let ms = start + search_from + pos;
let me = ms + needle.len();
state.lazy_matches.push((ms, me));
search_from += pos + needle.len();
}
}
}
pub fn history_previous(&mut self) {
let state = match self.state.as_mut() {
Some(s) => s,
None => return,
};
let ring_len = self.history.len(state.regexp);
if ring_len == 0 {
return;
}
let new_index = match state.history_index {
None => 0,
Some(i) => {
if i + 1 < ring_len {
i + 1
} else {
return;
}
}
};
if let Some(s) = self.history.get(new_index, state.regexp) {
state.search_string = s.to_string();
state.history_index = Some(new_index);
}
}
pub fn history_next(&mut self) {
let state = match self.state.as_mut() {
Some(s) => s,
None => return,
};
match state.history_index {
None => {}
Some(0) => {
state.search_string.clear();
state.history_index = None;
}
Some(i) => {
let new_index = i - 1;
if let Some(s) = self.history.get(new_index, state.regexp) {
state.search_string = s.to_string();
state.history_index = Some(new_index);
}
}
}
}
pub fn yank_word_or_char(&mut self, text: &str, point: usize) {
let state = match self.state.as_mut() {
Some(s) => s,
None => return,
};
if point >= text.len() {
return;
}
let rest = &text[point..];
let mut end = 0;
let mut chars = rest.chars();
if let Some(ch) = chars.next() {
end += ch.len_utf8();
if ch.is_alphanumeric() || ch == '_' {
for ch2 in chars {
if ch2.is_alphanumeric() || ch2 == '_' {
end += ch2.len_utf8();
} else {
break;
}
}
}
}
state.search_string.push_str(&rest[..end]);
state.history_index = None;
}
pub fn is_active(&self) -> bool {
self.state.as_ref().is_some_and(|s| s.active)
}
pub fn state(&self) -> Option<&IsearchState> {
self.state.as_ref()
}
pub fn prompt(&self) -> String {
let state = match self.state.as_ref() {
Some(s) => s,
None => return String::new(),
};
let mut parts = Vec::new();
if !state.success {
parts.push("Failing");
}
if state.wrapped {
parts.push("Wrapped");
}
if state.regexp {
parts.push("Regexp");
}
let dir = match state.direction {
SearchDirection::Forward => "I-search",
SearchDirection::Backward => "I-search backward",
};
parts.push(dir);
let prompt = parts.join(" ");
format!("{}: {}", prompt, state.search_string)
}
}
impl Default for IsearchManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryReplaceResponse {
Yes,
No,
ReplaceAll,
Quit,
Edit,
Delete,
Undo,
Help,
}
#[derive(Clone, Debug)]
pub struct QueryReplaceUndo {
pub position: usize,
pub original: String,
pub replacement: String,
}
pub struct QueryReplaceState {
pub from_string: String,
pub to_string: String,
pub regexp: bool,
pub delimited: bool,
pub case_fold: Option<bool>,
pub preserve_case: bool,
pub region_start: Option<usize>,
pub region_end: Option<usize>,
pub current_match: Option<(usize, usize)>,
pub replaced_count: usize,
pub skipped_count: usize,
pub undo_stack: Vec<QueryReplaceUndo>,
}
#[derive(Clone, Debug)]
pub enum QueryReplaceAction {
Replace(usize, usize, String),
Skip,
Done(QueryReplaceSummary),
ShowHelp(String),
NeedInput,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QueryReplaceSummary {
pub replaced: usize,
pub skipped: usize,
}
pub struct QueryReplaceManager {
state: Option<QueryReplaceState>,
}
impl QueryReplaceManager {
pub fn new() -> Self {
Self { state: None }
}
pub fn begin(&mut self, from: String, to: String, regexp: bool) {
self.state = Some(QueryReplaceState {
from_string: from,
to_string: to,
regexp,
delimited: false,
case_fold: None,
preserve_case: true,
region_start: None,
region_end: None,
current_match: None,
replaced_count: 0,
skipped_count: 0,
undo_stack: Vec::new(),
});
}
pub fn begin_in_region(
&mut self,
from: String,
to: String,
regexp: bool,
start: usize,
end: usize,
) {
self.begin(from, to, regexp);
if let Some(state) = self.state.as_mut() {
state.region_start = Some(start);
state.region_end = Some(end);
}
}
pub fn find_next(&mut self, text: &str, from_pos: usize) -> Option<(usize, usize)> {
let state = self.state.as_mut()?;
let limit = state.region_end.unwrap_or(text.len()).min(text.len());
let start = from_pos.max(state.region_start.unwrap_or(0));
if start > limit {
state.current_match = None;
return None;
}
let case_fold = resolve_case_fold(state.case_fold, &state.from_string);
let result = find_match(
text,
&state.from_string,
start,
true,
state.regexp,
case_fold,
);
if let Some((ms, me)) = result {
if me <= limit {
state.current_match = Some((ms, me));
return Some((ms, me));
}
}
state.current_match = None;
None
}
pub fn respond(&mut self, response: QueryReplaceResponse) -> QueryReplaceAction {
let state = match self.state.as_mut() {
Some(s) => s,
None => {
return QueryReplaceAction::Done(QueryReplaceSummary {
replaced: 0,
skipped: 0,
});
}
};
match response {
QueryReplaceResponse::Yes => {
if let Some((start, end)) = state.current_match {
let matched_text = String::new(); let replacement = state.to_string.clone();
let replacement = if state.preserve_case {
replacement
} else {
replacement
};
state.replaced_count += 1;
state.undo_stack.push(QueryReplaceUndo {
position: start,
original: matched_text,
replacement: replacement.clone(),
});
state.current_match = None;
QueryReplaceAction::Replace(start, end, replacement)
} else {
QueryReplaceAction::Skip
}
}
QueryReplaceResponse::No => {
state.skipped_count += 1;
state.current_match = None;
QueryReplaceAction::Skip
}
QueryReplaceResponse::ReplaceAll => {
if let Some((start, end)) = state.current_match {
let replacement = state.to_string.clone();
state.replaced_count += 1;
state.undo_stack.push(QueryReplaceUndo {
position: start,
original: String::new(),
replacement: replacement.clone(),
});
state.current_match = None;
QueryReplaceAction::Replace(start, end, replacement)
} else {
QueryReplaceAction::Skip
}
}
QueryReplaceResponse::Quit => {
let summary = QueryReplaceSummary {
replaced: state.replaced_count,
skipped: state.skipped_count,
};
self.state = None;
QueryReplaceAction::Done(summary)
}
QueryReplaceResponse::Edit => QueryReplaceAction::NeedInput,
QueryReplaceResponse::Delete => {
if let Some((start, end)) = state.current_match {
state.replaced_count += 1;
state.undo_stack.push(QueryReplaceUndo {
position: start,
original: String::new(),
replacement: String::new(),
});
state.current_match = None;
QueryReplaceAction::Replace(start, end, String::new())
} else {
QueryReplaceAction::Skip
}
}
QueryReplaceResponse::Undo => {
QueryReplaceAction::Skip
}
QueryReplaceResponse::Help => {
let help = concat!(
"y/SPC - replace this match\n",
"n/DEL - skip this match\n",
"! - replace all remaining matches\n",
"q/RET - quit\n",
"e - edit replacement\n",
"d - delete match (no replacement)\n",
"u - undo last replacement\n",
"? - show this help",
);
QueryReplaceAction::ShowHelp(help.to_string())
}
}
}
pub fn compute_replacement(&self, matched: &str) -> String {
let state = match self.state.as_ref() {
Some(s) => s,
None => return String::new(),
};
if state.preserve_case {
preserve_case(&state.to_string, matched)
} else {
state.to_string.clone()
}
}
pub fn undo_last(&mut self) -> Option<QueryReplaceUndo> {
let state = self.state.as_mut()?;
let entry = state.undo_stack.pop();
if entry.is_some() {
state.replaced_count = state.replaced_count.saturating_sub(1);
}
entry
}
pub fn finish(&mut self) -> QueryReplaceSummary {
let state = match self.state.take() {
Some(s) => s,
None => {
return QueryReplaceSummary {
replaced: 0,
skipped: 0,
};
}
};
QueryReplaceSummary {
replaced: state.replaced_count,
skipped: state.skipped_count,
}
}
pub fn is_active(&self) -> bool {
self.state.is_some()
}
pub fn state(&self) -> Option<&QueryReplaceState> {
self.state.as_ref()
}
pub fn prompt(&self) -> String {
let state = match self.state.as_ref() {
Some(s) => s,
None => return String::new(),
};
let kind = if state.regexp {
"Query replacing regexp"
} else {
"Query replacing"
};
format!(
"{} {} with {}: (y/n/!/q/?)",
kind, state.from_string, state.to_string
)
}
}
impl Default for QueryReplaceManager {
fn default() -> Self {
Self::new()
}
}
fn resolve_case_fold(override_val: Option<bool>, search_string: &str) -> bool {
match override_val {
Some(v) => v,
None => {
!search_string.chars().any(|c| c.is_uppercase())
}
}
}
fn build_regex_pattern(pattern: &str, case_fold: bool) -> String {
let translated = super::regex::translate_emacs_regex(pattern);
if case_fold {
format!("(?i){}", translated)
} else {
translated
}
}
fn find_match(
text: &str,
pattern: &str,
from: usize,
forward: bool,
regexp: bool,
case_fold: bool,
) -> Option<(usize, usize)> {
if pattern.is_empty() {
return None;
}
let text_len = text.len();
if regexp {
if forward {
let start = from.min(text_len);
let iterated = super::regex::iterate_string_matches_with_case_fold(
pattern, text, start, case_fold,
)
.ok()?;
iterated
.matches
.into_iter()
.find_map(|groups| groups.first().and_then(|group| *group))
} else {
let end = from.min(text_len);
let iterated = super::regex::iterate_string_matches_with_case_fold(
pattern,
&text[..end],
0,
case_fold,
)
.ok()?;
iterated
.matches
.into_iter()
.filter_map(|groups| groups.first().and_then(|group| *group))
.last()
}
} else {
if forward {
let start = from.min(text_len);
let region = &text[start..];
if case_fold {
let hay = region.to_lowercase();
let needle = pattern.to_lowercase();
let pos = hay.find(&needle)?;
Some((start + pos, start + pos + needle.len()))
} else {
let pos = region.find(pattern)?;
Some((start + pos, start + pos + pattern.len()))
}
} else {
let end = from.min(text_len);
let region = &text[..end];
if case_fold {
let hay = region.to_lowercase();
let needle = pattern.to_lowercase();
let pos = hay.rfind(&needle)?;
Some((pos, pos + needle.len()))
} else {
let pos = region.rfind(pattern)?;
Some((pos, pos + pattern.len()))
}
}
}
}
fn is_delimited_word_char(ch: char) -> bool {
ch.is_alphanumeric()
}
fn is_delimited_match(text: &str, start: usize, end: usize) -> bool {
let left = text.get(..start).and_then(|s| s.chars().next_back());
let right = text.get(end..).and_then(|s| s.chars().next());
let left_ok = match left {
Some(ch) => !is_delimited_word_char(ch),
None => true,
};
let right_ok = match right {
Some(ch) => !is_delimited_word_char(ch),
None => true,
};
left_ok && right_ok
}
fn preserve_case(replacement: &str, matched: &str) -> String {
super::casefiddle::apply_replace_match_case(replacement, matched)
}
fn expand_emacs_replacement(rep: &str, groups: &[Option<(usize, usize)>], source: &str) -> String {
let mut out = String::with_capacity(rep.len());
let mut chars = rep.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '\\' {
out.push(ch);
continue;
}
let Some(next) = chars.next() else {
out.push('\\');
break;
};
match next {
'&' => {
if let Some(Some((start, end))) = groups.first()
&& let Some(text) = source.get(*start..*end)
{
out.push_str(text);
}
}
'1'..='9' => {
let idx = next.to_digit(10).unwrap() as usize;
if let Some(Some((start, end))) = groups.get(idx)
&& let Some(text) = source.get(*start..*end)
{
out.push_str(text);
}
}
'\\' => out.push('\\'),
other => out.push(other),
}
}
out
}
fn replace_string_eval_impl(
eval: &mut super::eval::Context,
args: Vec<Value>,
query_style_point: bool,
) -> EvalResult {
expect_min_max_args("replace-string", &args, 2, 7)?;
let from = expect_sequence_string(&args[0])?;
let to = expect_string(&args[1])?;
let delimited = args.get(2).is_some_and(|v| !v.is_nil());
let backward = args.get(5).is_some_and(|v| !v.is_nil());
let region_noncontiguous = args.get(6).is_some_and(|v| !v.is_nil());
if region_noncontiguous && !backward {
let point_max = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if buf.mark().is_none() {
return Err(signal(
"error",
vec![Value::string(
"The mark is not set now, so there is no region",
)],
));
}
buf.point_max()
};
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = eval.buffers.goto_buffer_byte(current_id, point_max);
return Ok(Value::NIL);
}
let (start, end, source, read_only, buffer_name) = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let (start, end) = replacement_region_bounds(
buf,
args.get(3),
args.get(4),
backward,
region_noncontiguous,
)?;
(
start,
end,
buf.buffer_substring(start, end),
buffer_read_only_active(eval, buf),
buf.name.clone(),
)
};
if from.is_empty() {
if source.is_empty() {
return Ok(Value::NIL);
}
if read_only {
return Err(signal("buffer-read-only", vec![Value::string(buffer_name)]));
}
let mut out = String::with_capacity(source.len() + to.len() * source.chars().count());
if backward {
for ch in source.chars() {
out.push(ch);
out.push_str(&to);
}
} else {
for ch in source.chars() {
out.push_str(&to);
out.push(ch);
}
}
if out == source {
return Ok(Value::NIL);
}
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let old_len = super::editfns::current_buffer_byte_span_char_len(eval, start, end);
let new_len = out.len();
super::editfns::signal_before_change(eval, start, end)?;
let _ = eval.buffers.delete_buffer_region(current_id, start, end);
let _ = eval.buffers.goto_buffer_byte(current_id, start);
let _ = eval.buffers.insert_into_buffer(current_id, &out);
super::editfns::signal_after_change(eval, start, start + new_len, old_len)?;
if backward {
if let Some(first) = source.chars().next() {
let _ = eval
.buffers
.goto_buffer_byte(current_id, start + first.len_utf8());
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start);
}
} else if query_style_point {
if let Some(last) = source.chars().last() {
let _ = eval.buffers.goto_buffer_byte(
current_id,
start + out.len().saturating_sub(last.len_utf8()),
);
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start);
}
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start + out.len());
}
return Ok(Value::NIL);
}
let case_fold = case_fold_for_pattern(eval, &from);
let preserve_match_case = case_fold && case_replace_enabled(eval);
let lax_whitespace_regex = if replace_lax_whitespace_enabled(eval) && from.contains(' ') {
resolve_search_whitespace_regexp(eval)
} else {
None
};
let mut out = String::with_capacity(source.len());
let mut replaced = 0usize;
let mut backward_point = None;
let mut query_forward_point = None;
if let Some(whitespace_regex) = lax_whitespace_regex {
let pattern = build_lax_whitespace_pattern(&from, &whitespace_regex);
let iterated =
super::regex::iterate_string_matches_with_case_fold(&pattern, &source, 0, case_fold)
.map_err(|e| {
signal(
"invalid-regexp",
vec![Value::string(format!("Invalid regexp: {e}"))],
)
})?;
let mut last = 0usize;
for groups in iterated.matches {
let Some((m_start, m_end)) = groups.first().and_then(|group| *group) else {
continue;
};
if delimited && !is_delimited_match(&source, m_start, m_end) {
continue;
}
out.push_str(&source[last..m_start]);
let matched = &source[m_start..m_end];
if preserve_match_case {
out.push_str(&preserve_case(&to, matched));
} else {
out.push_str(&to);
}
query_forward_point = Some(out.len());
if backward && backward_point.is_none() {
backward_point = Some(m_start);
}
replaced += 1;
last = m_end;
}
out.push_str(&source[last..]);
} else {
let mut cursor = 0usize;
while let Some((m_start, m_end)) =
find_match(&source, &from, cursor, true, false, case_fold)
{
if delimited && !is_delimited_match(&source, m_start, m_end) {
out.push_str(&source[cursor..m_end]);
cursor = m_end;
continue;
}
out.push_str(&source[cursor..m_start]);
let matched = &source[m_start..m_end];
if preserve_match_case {
out.push_str(&preserve_case(&to, matched));
} else {
out.push_str(&to);
}
query_forward_point = Some(out.len());
if backward && backward_point.is_none() {
backward_point = Some(m_start);
}
replaced += 1;
cursor = m_end;
}
out.push_str(&source[cursor..]);
}
if replaced == 0 {
return Ok(Value::NIL);
}
if read_only {
return Err(signal("buffer-read-only", vec![Value::string(buffer_name)]));
}
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let old_len = super::editfns::current_buffer_byte_span_char_len(eval, start, end);
let new_len = out.len();
super::editfns::signal_before_change(eval, start, end)?;
let _ = eval.buffers.delete_buffer_region(current_id, start, end);
let _ = eval.buffers.goto_buffer_byte(current_id, start);
let _ = eval.buffers.insert_into_buffer(current_id, &out);
super::editfns::signal_after_change(eval, start, start + new_len, old_len)?;
if backward {
if let Some(pos) = backward_point {
let _ = eval.buffers.goto_buffer_byte(current_id, start + pos);
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start);
}
} else if query_style_point {
if let Some(pos) = query_forward_point {
let _ = eval.buffers.goto_buffer_byte(current_id, start + pos);
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start);
}
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start + out.len());
}
Ok(Value::NIL)
}
pub(crate) fn builtin_replace_string(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
replace_string_eval_impl(eval, args, false)
}
fn replace_regexp_eval_impl(
eval: &mut super::eval::Context,
args: Vec<Value>,
query_style_point: bool,
) -> EvalResult {
expect_min_max_args("replace-regexp", &args, 2, 7)?;
let from = expect_sequence_string(&args[0])?;
let to = expect_string(&args[1])?;
let delimited = args.get(2).is_some_and(|v| !v.is_nil());
let backward = args.get(5).is_some_and(|v| !v.is_nil());
let region_noncontiguous = args.get(6).is_some_and(|v| !v.is_nil());
if region_noncontiguous && !backward {
let point_max = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if buf.mark().is_none() {
return Err(signal(
"error",
vec![Value::string(
"The mark is not set now, so there is no region",
)],
));
}
buf.point_max()
};
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let _ = eval.buffers.goto_buffer_byte(current_id, point_max);
return Ok(Value::NIL);
}
let (start, end, source, read_only, buffer_name) = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let (start, end) = replacement_region_bounds(
buf,
args.get(3),
args.get(4),
backward,
region_noncontiguous,
)?;
(
start,
end,
buf.buffer_substring(start, end),
buffer_read_only_active(eval, buf),
buf.name.clone(),
)
};
let case_fold = case_fold_for_pattern(eval, &from);
let preserve_match_case = case_fold && case_replace_enabled(eval);
let iterated = super::regex::iterate_string_matches_with_case_fold(
&from, &source, 0, case_fold,
)
.map_err(|e| {
signal(
"invalid-regexp",
vec![Value::string(format!("Invalid regexp: {e}"))],
)
})?;
let mut out = String::with_capacity(source.len());
let mut last = 0usize;
let mut replaced = 0usize;
let mut backward_point = None;
let mut query_forward_point = None;
for groups in iterated.matches {
let Some((match_start, match_end)) = groups.first().and_then(|group| *group) else {
continue;
};
if delimited && !is_delimited_match(&source, match_start, match_end) {
continue;
}
if match_start == match_end {
if backward {
if match_start == 0 {
continue;
}
} else {
if match_start >= source.len() {
continue;
}
}
out.push_str(&source[last..match_start]);
let expanded = expand_emacs_replacement(&to, &groups, &source);
if preserve_match_case {
out.push_str(&preserve_case(&expanded, &source[match_start..match_end]));
} else {
out.push_str(&expanded);
}
query_forward_point = Some(out.len());
last = match_start;
if backward && backward_point.is_none() {
backward_point = Some(match_start);
}
replaced += 1;
continue;
}
out.push_str(&source[last..match_start]);
let expanded = expand_emacs_replacement(&to, &groups, &source);
if preserve_match_case {
out.push_str(&preserve_case(&expanded, &source[match_start..match_end]));
} else {
out.push_str(&expanded);
}
query_forward_point = Some(out.len());
last = match_end;
if backward && backward_point.is_none() {
backward_point = Some(match_start);
}
replaced += 1;
}
out.push_str(&source[last..]);
if replaced == 0 {
return Ok(Value::NIL);
}
if read_only {
return Err(signal("buffer-read-only", vec![Value::string(buffer_name)]));
}
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let old_len = super::editfns::current_buffer_byte_span_char_len(eval, start, end);
let new_len = out.len();
super::editfns::signal_before_change(eval, start, end)?;
let _ = eval.buffers.delete_buffer_region(current_id, start, end);
let _ = eval.buffers.goto_buffer_byte(current_id, start);
let _ = eval.buffers.insert_into_buffer(current_id, &out);
super::editfns::signal_after_change(eval, start, start + new_len, old_len)?;
if backward {
if let Some(pos) = backward_point {
let _ = eval.buffers.goto_buffer_byte(current_id, start + pos);
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start);
}
} else if query_style_point {
if let Some(pos) = query_forward_point {
let _ = eval.buffers.goto_buffer_byte(current_id, start + pos);
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start);
}
} else {
let _ = eval.buffers.goto_buffer_byte(current_id, start + out.len());
}
Ok(Value::NIL)
}
pub(crate) fn builtin_replace_regexp(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
replace_regexp_eval_impl(eval, args, false)
}
pub(crate) fn builtin_query_replace(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_max_args("query-replace", &args, 2, 7)?;
replace_string_eval_impl(eval, args, true)
}
pub(crate) fn builtin_query_replace_regexp(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_max_args("query-replace-regexp", &args, 2, 7)?;
match replace_regexp_eval_impl(eval, args, true) {
Err(Flow::Signal(sig)) if sig.symbol_name() == "invalid-regexp" => Ok(Value::NIL),
other => other,
}
}
pub(crate) fn builtin_keep_lines(eval: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
expect_min_max_args("keep-lines", &args, 1, 4)?;
let regexp = expect_sequence_string(&args[0])?;
let (point_min, start, end, source) = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let (start, end) = line_operation_region_bounds(buf, args.get(1), args.get(2))?;
(
buf.point_min(),
start,
end,
buf.buffer_substring(buf.point_min(), buf.point_max()),
)
};
let case_fold = case_fold_for_pattern(eval, ®exp);
let rel_start = start.saturating_sub(point_min).min(source.len());
let rel_end = end.saturating_sub(point_min).min(source.len());
let mut rel_cursor = line_start_at_or_before(&source, rel_start);
let mut delete_ranges: Vec<(usize, usize)> = Vec::new();
while rel_cursor < source.len() {
let abs_line_start = point_min + rel_cursor;
if abs_line_start >= point_min + rel_end {
break;
}
let line_tail = &source[rel_cursor..];
let line_len = match line_tail.find('\n') {
Some(idx) => idx + 1,
None => line_tail.len(),
};
let rel_line_end = rel_cursor + line_len;
let line = if source.as_bytes().get(rel_line_end.wrapping_sub(1)) == Some(&b'\n') {
&source[rel_cursor..rel_line_end - 1]
} else {
&source[rel_cursor..rel_line_end]
};
let keep_line = match string_matches_regexp(line, ®exp, case_fold) {
Ok(matched) => matched,
Err(Flow::Signal(sig)) if sig.symbol_name() == "invalid-regexp" => {
return Ok(Value::NIL);
}
Err(err) => return Err(err),
};
if !keep_line {
delete_ranges.push((point_min + rel_cursor, point_min + rel_line_end));
}
rel_cursor = rel_line_end;
}
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if !delete_ranges.is_empty() {
let region_start = delete_ranges.last().map(|(s, _)| *s).unwrap_or(start);
let region_end = delete_ranges.first().map(|(_, e)| *e).unwrap_or(start);
let total_deleted: usize = delete_ranges.iter().map(|(s, e)| e - s).sum();
let old_len =
super::editfns::current_buffer_byte_span_char_len(eval, region_start, region_end);
super::editfns::signal_before_change(eval, region_start, region_end)?;
for (del_start, del_end) in delete_ranges.into_iter().rev() {
let _ = eval
.buffers
.delete_buffer_region(current_id, del_start, del_end);
}
super::editfns::signal_after_change(
eval,
region_start,
region_end - total_deleted,
old_len,
)?;
}
let _ = eval.buffers.goto_buffer_byte(current_id, start);
Ok(Value::NIL)
}
pub(crate) fn builtin_flush_lines(eval: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
expect_min_max_args("flush-lines", &args, 1, 4)?;
let regexp = expect_sequence_string(&args[0])?;
let (point_min, start, end, source) = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let (start, end) = line_operation_region_bounds(buf, args.get(1), args.get(2))?;
(
buf.point_min(),
start,
end,
buf.buffer_substring(buf.point_min(), buf.point_max()),
)
};
let case_fold = case_fold_for_pattern(eval, ®exp);
let rel_start = start.saturating_sub(point_min).min(source.len());
let rel_end = end.saturating_sub(point_min).min(source.len());
let mut rel_cursor = line_start_at_or_before(&source, rel_start);
let mut delete_ranges: Vec<(usize, usize)> = Vec::new();
while rel_cursor < source.len() {
let abs_line_start = point_min + rel_cursor;
if abs_line_start >= point_min + rel_end {
break;
}
let line_tail = &source[rel_cursor..];
let line_len = match line_tail.find('\n') {
Some(idx) => idx + 1,
None => line_tail.len(),
};
let rel_line_end = rel_cursor + line_len;
let line = if source.as_bytes().get(rel_line_end.wrapping_sub(1)) == Some(&b'\n') {
&source[rel_cursor..rel_line_end - 1]
} else {
&source[rel_cursor..rel_line_end]
};
if string_matches_regexp(line, ®exp, case_fold)? {
delete_ranges.push((point_min + rel_cursor, point_min + rel_line_end));
}
rel_cursor = rel_line_end;
}
let current_id = eval
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
if !delete_ranges.is_empty() {
let region_start = delete_ranges.first().map(|(s, _)| *s).unwrap_or(start);
let region_end = delete_ranges.last().map(|(_, e)| *e).unwrap_or(start);
let total_deleted: usize = delete_ranges.iter().map(|(s, e)| e - s).sum();
let old_len =
super::editfns::current_buffer_byte_span_char_len(eval, region_start, region_end);
super::editfns::signal_before_change(eval, region_start, region_end)?;
for (del_start, del_end) in delete_ranges.into_iter().rev() {
let _ = eval
.buffers
.delete_buffer_region(current_id, del_start, del_end);
}
super::editfns::signal_after_change(
eval,
region_start,
region_end - total_deleted,
old_len,
)?;
}
let _ = eval.buffers.goto_buffer_byte(current_id, start);
Ok(Value::fixnum(0))
}
pub(crate) fn builtin_how_many(eval: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
expect_min_max_args("how-many", &args, 1, 4)?;
let regexp = expect_sequence_string(&args[0])?;
let source = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let (start, end) = line_operation_region_bounds(buf, args.get(1), args.get(2))?;
buf.buffer_substring(start, end)
};
if regexp.is_empty() {
return Ok(Value::fixnum(source.chars().count() as i64));
}
let case_fold = case_fold_for_pattern(eval, ®exp);
Ok(Value::fixnum(count_string_regexp_matches(
&source, ®exp, case_fold,
)?))
}
pub(crate) fn builtin_count_matches(
eval: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_max_args("count-matches", &args, 1, 4)?;
let regexp = expect_sequence_string(&args[0])?;
let source = {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let (start, end) = line_operation_region_bounds(buf, args.get(1), args.get(2))?;
buf.buffer_substring(start, end)
};
if regexp.is_empty() {
return Ok(Value::fixnum(source.chars().count() as i64));
}
let case_fold = case_fold_for_pattern(eval, ®exp);
Ok(Value::fixnum(count_string_regexp_matches(
&source, ®exp, case_fold,
)?))
}
pub(crate) fn builtin_isearch_forward(args: Vec<Value>) -> EvalResult {
expect_min_max_args("isearch-forward", &args, 0, 2)?;
Err(signal(
"error",
vec![Value::string(
"move-to-window-line called from unrelated buffer",
)],
))
}
pub(crate) fn builtin_isearch_backward(args: Vec<Value>) -> EvalResult {
expect_min_max_args("isearch-backward", &args, 0, 2)?;
Err(signal(
"error",
vec![Value::string(
"move-to-window-line called from unrelated buffer",
)],
))
}
#[cfg(test)]
#[path = "isearch_test.rs"]
mod tests;