use super::error::{EvalResult, Flow, signal};
use super::intern::intern;
use super::symbol::Obarray;
use super::value::*;
use crate::buffer::{Buffer, BufferManager};
use crate::emacs_core::value::ValueKind;
fn expect_args(name: &str, args: &[Value], n: usize) -> Result<(), Flow> {
if args.len() != n {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_min_args(name: &str, args: &[Value], min: usize) -> Result<(), Flow> {
if args.len() < min {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_max_args(name: &str, args: &[Value], max: usize) -> Result<(), Flow> {
if args.len() > max {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_fixnump(val: &Value) -> Result<i64, Flow> {
match val.kind() {
ValueKind::Fixnum(n) => Ok(n),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("fixnump"), *val],
)),
}
}
fn expect_wholenump(val: &Value) -> Result<usize, Flow> {
match val.kind() {
ValueKind::Fixnum(n) if n >= 0 => Ok(n as usize),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("wholenump"), *val],
)),
}
}
fn dynamic_buffer_or_global_symbol_value(
obarray: &Obarray,
_dynamic: &[OrderedRuntimeBindingMap],
buf: Option<&Buffer>,
name: &str,
) -> Option<Value> {
if let Some(buf) = buf
&& let Some(value) = buf.get_buffer_local(name)
{
return Some(*value);
}
obarray.symbol_value(name).copied()
}
fn tab_width_in_state(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buf: Option<&Buffer>,
) -> usize {
match dynamic_buffer_or_global_symbol_value(obarray, dynamic, buf, "tab-width") {
Some(v) if v.is_fixnum() && v.as_fixnum().unwrap() > 0 => v.as_fixnum().unwrap() as usize,
Some(v) if v.is_char() && (v.as_char().unwrap() as u32) > 0 => {
v.as_char().unwrap() as usize
}
_ => 8,
}
}
fn indent_tabs_mode_in_state(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buf: Option<&Buffer>,
) -> bool {
dynamic_buffer_or_global_symbol_value(obarray, dynamic, buf, "indent-tabs-mode")
.is_none_or(|value| value.is_truthy())
}
fn buffer_read_only_active_in_state(
obarray: &Obarray,
dynamic: &[OrderedRuntimeBindingMap],
buf: &Buffer,
) -> bool {
if buf.read_only {
return true;
}
dynamic_buffer_or_global_symbol_value(obarray, dynamic, Some(buf), "buffer-read-only")
.is_some_and(|value| value.is_truthy())
}
fn buffer_read_only_active(eval: &super::eval::Context, buf: &Buffer) -> bool {
buffer_read_only_active_in_state(&eval.obarray, &[], buf)
}
fn line_bounds(text: &str, begv: usize, zv: usize, point: usize) -> (usize, usize) {
let bytes = text.as_bytes();
let pt = point.clamp(begv, zv);
let mut bol = pt;
while bol > begv && bytes[bol - 1] != b'\n' {
bol -= 1;
}
let mut eol = pt;
while eol < zv && bytes[eol] != b'\n' {
eol += 1;
}
(bol, eol)
}
fn next_column(column: usize, ch: char, tab_width: usize) -> usize {
if ch == '\t' {
let tab = tab_width.max(1);
column + (tab - (column % tab))
} else {
column + crate::encoding::char_width(ch)
}
}
fn column_for_prefix(prefix: &str, tab_width: usize) -> usize {
let mut column = 0usize;
for ch in prefix.chars() {
column = next_column(column, ch, tab_width);
}
column
}
fn padding_to_column(mut column: usize, target: usize, tab_width: usize) -> String {
let mut out = String::new();
let tab = tab_width.max(1);
while column < target {
let next_tab = column + (tab - (column % tab));
if next_tab <= target && next_tab > column + 1 {
out.push('\t');
column = next_tab;
} else {
out.push(' ');
column += 1;
}
}
out
}
#[inline]
fn is_horizontal_space(ch: char) -> bool {
ch == ' ' || ch == '\t'
}
fn delete_horizontal_space_at_point(
eval: &mut super::eval::Context,
backward_only: bool,
) -> Result<(), Flow> {
let buf = eval
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let pmin = buf.point_min();
let pmax = buf.point_max();
let pt = buf.point();
let mut left = pt;
while left > pmin {
let Some(ch) = buf.char_before(left) else {
break;
};
if !is_horizontal_space(ch) {
break;
}
left -= ch.len_utf8();
}
let mut right = pt;
if !backward_only {
while right < pmax {
let Some(ch) = buf.char_after(right) else {
break;
};
if !is_horizontal_space(ch) {
break;
}
right += ch.len_utf8();
}
}
if left == right {
return Ok(());
}
if buffer_read_only_active(eval, buf) {
return Err(signal(
"buffer-read-only",
vec![Value::string(buf.name.clone())],
));
}
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, left, right);
super::editfns::signal_before_change(eval, left, right)?;
let _ = eval.buffers.delete_buffer_region(current_id, left, right);
super::editfns::signal_after_change(eval, left, left, old_len)?;
Ok(())
}
pub(crate) fn builtin_current_indentation(
ctx: &mut crate::emacs_core::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_args("current-indentation", &args, 0)?;
let Some(buf) = &ctx.buffers.current_buffer() else {
return Ok(Value::fixnum(0));
};
let tabw = tab_width_in_state(&ctx.obarray, &[], Some(buf));
let text = buf.text.to_string();
let (bol, eol) = line_bounds(&text, buf.begv, buf.zv, buf.pt);
let line = &text[bol..eol];
let mut column = 0usize;
for ch in line.chars() {
match ch {
' ' | '\t' => column = next_column(column, ch, tabw),
_ => break,
}
}
Ok(Value::fixnum(column as i64))
}
pub(crate) fn builtin_current_column(
ctx: &mut crate::emacs_core::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_args("current-column", &args, 0)?;
let Some(buf) = &ctx.buffers.current_buffer() else {
return Ok(Value::fixnum(0));
};
let tabw = tab_width_in_state(&ctx.obarray, &[], Some(buf));
let text = buf.text.to_string();
let pt = buf.pt.clamp(buf.begv, buf.zv);
let (bol, _) = line_bounds(&text, buf.begv, buf.zv, pt);
let prefix = &text[bol..pt];
Ok(Value::fixnum(column_for_prefix(prefix, tabw) as i64))
}
pub(crate) fn builtin_move_to_column(
ctx: &mut crate::emacs_core::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_args("move-to-column", &args, 1)?;
expect_max_args("move-to-column", &args, 2)?;
let target = expect_wholenump(&args[0])?;
let force = args.get(1).is_some_and(|v| v.is_truthy());
let Some(current_id) = ctx.buffers.current_buffer_id() else {
return Ok(Value::fixnum(0));
};
let Some(buf) = ctx.buffers.get(current_id) else {
return Ok(Value::fixnum(0));
};
let tabw = tab_width_in_state(&ctx.obarray, &[], Some(buf));
let read_only = buffer_read_only_active_in_state(&ctx.obarray, &[], buf);
let text = buf.text.to_string();
let pt = buf.pt.clamp(buf.begv, buf.zv);
let (bol, eol) = line_bounds(&text, buf.begv, buf.zv, pt);
let line = &text[bol..eol];
let buffer_name = buf.name.clone();
if target == 0 {
let _ = ctx.buffers.goto_buffer_byte(current_id, bol);
return Ok(Value::fixnum(0));
}
let mut column = 0usize;
let mut dest_byte = bol;
let mut reached = 0usize;
let mut found = false;
let mut tab_split: Option<(usize, usize)> = None;
for (rel, ch) in line.char_indices() {
let char_start = bol + rel;
let char_end = char_start + ch.len_utf8();
let next = next_column(column, ch, tabw);
if next >= target {
if force && ch == '\t' && next > target {
tab_split = Some((char_start, column));
} else {
dest_byte = char_end;
reached = next;
}
found = true;
break;
}
dest_byte = char_end;
reached = next;
column = next;
}
if !found {
dest_byte = eol;
reached = column_for_prefix(line, tabw);
}
if let Some((tab_byte, col_before_tab)) = tab_split {
if read_only {
return Err(signal(
"buffer-read-only",
vec![Value::string(buffer_name.clone())],
));
}
let _ = ctx.buffers.goto_buffer_byte(current_id, tab_byte);
let pad = padding_to_column(col_before_tab, target, tabw);
let insert_pos = tab_byte;
let pad_len = pad.len();
super::editfns::signal_before_change(ctx, insert_pos, insert_pos)?;
let _ = ctx.buffers.insert_into_buffer(current_id, &pad);
super::editfns::signal_after_change(ctx, insert_pos, insert_pos + pad_len, 0)?;
return Ok(Value::fixnum(target as i64));
}
let _ = ctx.buffers.goto_buffer_byte(current_id, dest_byte);
if force && reached < target {
if read_only {
return Err(signal("buffer-read-only", vec![Value::string(buffer_name)]));
}
let pad = padding_to_column(reached, target, tabw);
let insert_pos = ctx.buffers.get(current_id).map(|b| b.pt).unwrap_or(0);
let pad_len = pad.len();
super::editfns::signal_before_change(ctx, insert_pos, insert_pos)?;
let _ = ctx.buffers.insert_into_buffer(current_id, &pad);
super::editfns::signal_after_change(ctx, insert_pos, insert_pos + pad_len, 0)?;
reached = target;
}
Ok(Value::fixnum(reached as i64))
}
pub(crate) fn builtin_indent_to(
ctx: &mut crate::emacs_core::eval::Context,
args: Vec<Value>,
) -> EvalResult {
expect_min_args("indent-to", &args, 1)?;
expect_max_args("indent-to", &args, 2)?;
let column = expect_fixnump(&args[0])?.max(0) as usize;
let minimum = if args.len() > 1 && !args[1].is_nil() {
expect_fixnump(&args[1])?.max(0) as usize
} else {
0
};
let current_id = ctx
.buffers
.current_buffer_id()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let buf = ctx
.buffers
.current_buffer()
.ok_or_else(|| signal("error", vec![Value::string("No current buffer")]))?;
let pt = buf.point();
let pmin = buf.point_min();
let text_before = buf.buffer_substring(pmin, pt);
let line_start = text_before.rfind('\n').map(|pos| pos + 1).unwrap_or(0);
let line_prefix = &text_before[line_start..];
let tab_width = tab_width_in_state(&ctx.obarray, &[], Some(buf));
let mut fromcol = 0usize;
for ch in line_prefix.chars() {
fromcol = next_column(fromcol, ch, tab_width);
}
let mincol = column.max(fromcol + minimum);
if fromcol >= mincol {
return Ok(Value::fixnum(mincol as i64));
}
if buffer_read_only_active_in_state(&ctx.obarray, &[], buf) {
return Err(signal(
"buffer-read-only",
vec![Value::string(buf.name.clone())],
));
}
let use_tabs = indent_tabs_mode_in_state(&ctx.obarray, &[], Some(buf));
let mut indent = String::new();
let mut col = fromcol;
if use_tabs {
let tab = tab_width.max(1);
while col < mincol {
let next_tab = col + (tab - (col % tab));
if next_tab <= mincol {
indent.push('\t');
col = next_tab;
} else {
break;
}
}
}
while col < mincol {
indent.push(' ');
col += 1;
}
let insert_pos = ctx.buffers.get(current_id).map(|b| b.pt).unwrap_or(0);
let indent_len = indent.len();
if indent_len > 0 {
super::editfns::signal_before_change(ctx, insert_pos, insert_pos)?;
let _ = ctx.buffers.insert_into_buffer(current_id, &indent);
super::editfns::signal_after_change(ctx, insert_pos, insert_pos + indent_len, 0)?;
}
Ok(Value::fixnum(mincol as i64))
}
pub fn init_indent_vars(obarray: &mut super::symbol::Obarray) {
obarray.set_symbol_value("tab-width", Value::fixnum(8));
obarray.make_special("tab-width");
obarray.set_symbol_value("indent-tabs-mode", Value::T);
obarray.make_special("indent-tabs-mode");
obarray.set_symbol_value("standard-indent", Value::fixnum(4));
obarray.make_special("standard-indent");
obarray.set_symbol_value("tab-stop-list", Value::NIL);
obarray.make_special("tab-stop-list");
}
#[cfg(test)]
#[path = "indent_test.rs"]
mod tests;