use super::*;
use crate::symbols::{Arrow, ColDelim, Delim, Radical};
impl Editor {
pub fn op_start(&mut self, kind: BoxKind) {
self.select_anchor = None;
self.op_entry = Some((kind, String::new()));
self.op_cmd = self.executing.clone();
self.op_cursor = 0;
self.op_escape = false;
}
fn op_byte(buf: &str, i: usize) -> usize {
buf.char_indices().nth(i).map_or(buf.len(), |(b, _)| b)
}
pub fn op_type(&mut self, c: char) {
let cur = self.op_cursor;
if let Some((_, buf)) = &mut self.op_entry {
let at = Self::op_byte(buf, cur);
buf.insert(at, c);
self.op_cursor += 1;
}
}
pub fn op_backspace(&mut self) {
let cur = self.op_cursor;
if let Some((_, buf)) = &mut self.op_entry {
if buf.is_empty() {
self.op_entry = None;
} else if cur > 0 {
let at = Self::op_byte(buf, cur - 1);
buf.remove(at);
self.op_cursor -= 1;
}
}
}
pub fn op_delete(&mut self) {
let cur = self.op_cursor;
if let Some((_, buf)) = &mut self.op_entry
&& cur < buf.chars().count()
{
let at = Self::op_byte(buf, cur);
buf.remove(at);
}
}
pub fn op_move(&mut self, delta: isize) -> bool {
let Some((_, buf)) = &self.op_entry else {
return false;
};
let len = buf.chars().count();
match self.op_cursor.checked_add_signed(delta) {
Some(c) if c <= len => {
self.op_cursor = c;
true
}
_ => false,
}
}
pub fn op_commit(&mut self) {
let Some((kind, text)) = self.op_entry.take() else {
return;
};
let upright = |w: &str| match w.chars().count() {
1 => Node::Roman(w.chars().next().unwrap()),
_ => Node::Func(w.to_string()),
};
match kind {
BoxKind::Rm => {
let t = text.trim();
if !t.is_empty() {
let node = upright(t);
let col = self.col;
self.cur_row_mut().insert(col, node);
self.col += 1;
}
}
BoxKind::Text => {
if !text.is_empty() {
let node = Node::Text(text);
let col = self.col;
self.cur_row_mut().insert(col, node);
self.col += 1;
}
}
BoxKind::Tex => {
let mut row = crate::ast::normalize(&crate::from_latex::row_from_latex(&text));
if !self.path.is_empty() {
row.retain(|n| !matches!(n, Node::Break));
}
if row.is_empty() {
return;
}
let col = self.col;
let n = row.len();
self.cur_row_mut().splice(col..col, row);
self.col += n;
}
BoxKind::Op | BoxKind::OpStar => {
let words: Vec<&str> = text.split_whitespace().collect();
if words.is_empty() {
return;
}
if kind == BoxKind::OpStar {
let name = words.concat();
if name.chars().count() < 2 {
let cmd = self.op_cmd.take().unwrap_or_else(|| "op*".into());
self.error(format!("\\{cmd} needs a name of two or more characters"));
return;
}
self.insert_and_enter(Node::BigOp {
name,
lower: vec![],
upper: vec![],
});
} else {
let mut nodes: Vec<Node> = Vec::new();
for (i, w) in words.iter().enumerate() {
if i > 0 {
nodes.push(Node::Sym('␣'));
}
nodes.push(upright(w));
}
let col = self.col;
let n = nodes.len();
self.cur_row_mut().splice(col..col, nodes);
self.col += n;
}
}
}
}
pub fn command_known(&self, cmd: &str) -> bool {
resolve(cmd).is_some()
}
pub fn command_preview_row(&self) -> Option<Row> {
preview_row(self.minibuffer.as_deref()?)
}
pub fn command_preview(&self) -> Option<char> {
match self.command_preview_row()?.as_slice() {
[Node::Sym(c)] => Some(*c),
[Node::BigOpSym { op, .. }] => Some(*op),
_ => None,
}
}
pub fn execute(&mut self, cmd: &str) {
if cmd.is_empty() {
return;
}
match resolve(cmd) {
Some(edit) => {
self.executing = Some(cmd.to_string());
self.apply(edit);
self.executing = None;
}
None if cmd.starts_with("delim") || cmd.starts_with("lr") => {
let p = if cmd.starts_with("delim") {
"delim"
} else {
"lr"
};
self.message = format!(
"\\{p} takes delimiters in visual order: opening, (middle vertical lines,) closing"
);
}
None => self.error(format!("\\{} is not a command", cmd)),
}
}
pub fn apply(&mut self, edit: Edit) {
let wraps = matches!(
edit,
Edit::Insert { wrap: true, .. } | Edit::Accent(_) | Edit::Delim { .. }
);
let replaces = matches!(
edit,
Edit::Sym(_)
| Edit::Insert { wrap: false, .. }
| Edit::Grid { .. }
| Edit::Mid
| Edit::OpenBox(_)
);
if replaces {
if self.selection().is_some() {
self.take_selection();
} else {
self.select_anchor = None;
}
} else if !wraps {
self.select_anchor = None;
}
match edit {
Edit::Insert { node, wrap } => {
let has_empty_slot = {
let fields = node.fields();
fields.iter().any(|&f| node.field(f).is_empty())
};
if wrap && self.selection().is_some() {
let content: crate::ast::Row = self
.take_selection()
.unwrap()
.into_iter()
.filter(|n| *n != Node::Break)
.collect();
let mut node = node;
let fields = node.fields();
*node.field_mut(fields[0]) = content;
let enter = fields[1..]
.iter()
.copied()
.find(|&f| node.field(f).is_empty());
let col = self.col;
self.cur_row_mut().insert(col, node);
match enter {
Some(f) => {
self.path.push((col, f));
self.col = 0;
}
None => self.col += 1,
}
} else if has_empty_slot {
self.insert_and_enter(node);
} else {
let col = self.col;
self.cur_row_mut().insert(col, node);
self.col += 1;
}
}
Edit::Sym(c) => self.insert_sym(c),
Edit::Accent(mark) => self.apply_accent(mark),
Edit::Delim { left, right, mids } => self.apply(Edit::Insert {
node: Node::Delim {
left,
right,
mids,
segs: vec![vec![]; mids + 1],
},
wrap: true,
}),
Edit::Grid { wrap, rows, cols } => match wrap {
GridWrap::Pair(l, r) => self.insert_grid(l, r, rows, cols),
GridWrap::Norm => self.insert_norm_grid(rows, cols),
GridWrap::Bare => self.insert_and_enter(Node::Array {
rows,
cols,
cells: vec![vec![]; rows * cols],
}),
},
Edit::Mid => self.insert_mid(),
Edit::AddRow => self.add_lane(false),
Edit::AddCol => self.add_lane(true),
Edit::DelRow => self.del_lane(false),
Edit::DelCol => self.del_lane(true),
Edit::OpenBox(kind) => self.op_start(kind),
Edit::Negate => self.negate_prev(),
}
}
}
pub fn preview_row(cmd: &str) -> Option<Row> {
if cmd.is_empty() {
return None;
}
match resolve(cmd)? {
Edit::Sym(c) => Some(vec![Node::Sym(c)]),
Edit::Insert { node, .. } => Some(vec![node]),
Edit::Delim { left, right, mids } => Some(vec![Node::Delim {
left,
right,
mids,
segs: vec![vec![]; mids + 1],
}]),
Edit::Grid { wrap, rows, cols } => {
let array = Node::Array {
rows,
cols,
cells: vec![vec![]; rows * cols],
};
Some(vec![match wrap {
GridWrap::Bare => array,
GridWrap::Norm => Node::Norm { arg: vec![array] },
GridWrap::Pair(l, r) => Node::Delim {
left: l,
right: r,
mids: 0,
segs: vec![vec![array]],
},
}])
}
Edit::Accent(mark) => Some(vec![Node::Sym(mark.info().preview)]),
Edit::OpenBox(_) => Some(vec![Node::Sym(crate::glyphs::PLACEHOLDER)]),
Edit::Mid | Edit::Negate | Edit::AddRow | Edit::AddCol | Edit::DelRow | Edit::DelCol => {
None
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Edit {
Insert {
node: Node,
wrap: bool,
},
Sym(char),
Accent(crate::symbols::Accent),
Delim {
left: crate::symbols::Delim,
right: crate::symbols::Delim,
mids: usize,
},
Grid {
wrap: GridWrap,
rows: usize,
cols: usize,
},
Mid,
Negate,
AddRow,
AddCol,
DelRow,
DelCol,
OpenBox(BoxKind),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeCmd {
Free,
BlockSelect,
GridEdit,
CopyAa,
Write,
WriteQuit,
Quit,
}
pub const MODE_COMMANDS: &[(&[&str], ModeCmd, &str, &str)] = &[
(&["f", "F", "free"], ModeCmd::Free, "^F", "free cursor"),
(
&["b", "B", "block", "blockselect"],
ModeCmd::BlockSelect,
"^B",
"block select",
),
(&["g", "G", "grid"], ModeCmd::GridEdit, "^G", "grid edit"),
(&["clipboard"], ModeCmd::CopyAa, "^Y", "AA to clipboard"),
(&["w", "W", "write", "save"], ModeCmd::Write, "^O", "save"),
(
&["wq", "writequit"],
ModeCmd::WriteQuit,
"^W",
"save and quit",
),
(&["quit"], ModeCmd::Quit, "^Q", "quit"),
];
pub fn mode_command(cmd: &str) -> Option<ModeCmd> {
MODE_COMMANDS
.iter()
.find(|(names, ..)| names.contains(&cmd))
.map(|&(_, mode, ..)| mode)
}
pub fn resolve(cmd: &str) -> Option<Edit> {
let ins = |node: Node| Some(Edit::Insert { node, wrap: false });
let wrap = |node: Node| Some(Edit::Insert { node, wrap: true });
let delim = |l: Delim, r: Delim, mids: usize| {
Some(Edit::Delim {
left: l,
right: r,
mids,
})
};
match cmd {
"frac" => wrap(Node::Frac {
num: vec![],
den: vec![],
}),
"norm" => wrap(Node::Norm { arg: vec![] }),
"overbrace" | "underbrace" => wrap(Node::Brace {
over: cmd == "overbrace",
arg: vec![],
label: vec![],
}),
"ceil" => delim(Delim::Col(ColDelim::Ceil), Delim::Col(ColDelim::Ceil), 0),
"floor" => delim(Delim::Col(ColDelim::Floor), Delim::Col(ColDelim::Floor), 0),
"abs" => delim(Delim::Col(ColDelim::Bar), Delim::Col(ColDelim::Bar), 0),
"bra" => delim(Delim::Angle, Delim::Col(ColDelim::Bar), 0),
"ket" => delim(Delim::Col(ColDelim::Bar), Delim::Angle, 0),
"braket" => delim(Delim::Angle, Delim::Angle, 1),
"set" => delim(Delim::Col(ColDelim::Brace), Delim::Col(ColDelim::Brace), 1),
"mid" => Some(Edit::Mid),
"!" | "negate" => Some(Edit::Negate),
"addrow" => Some(Edit::AddRow),
"addcol" => Some(Edit::AddCol),
"delrow" => Some(Edit::DelRow),
"delcol" => Some(Edit::DelCol),
"op" | "operatorname" => Some(Edit::OpenBox(BoxKind::Op)),
"op*" | "operatorname*" | "limits" => Some(Edit::OpenBox(BoxKind::OpStar)),
"rm" => Some(Edit::OpenBox(BoxKind::Rm)),
"text" => Some(Edit::OpenBox(BoxKind::Text)),
"latex" => Some(Edit::OpenBox(BoxKind::Tex)),
_ => {
if let Some(index) = Radical::of_name(cmd) {
wrap(Node::Sqrt { arg: vec![], index })
} else if let Some(op) = Arrow::of_name(cmd) {
ins(Node::Arrow {
op,
over: vec![],
under: vec![],
})
} else if let Some((wrap, rows, cols)) = grid_command(cmd) {
Some(Edit::Grid { wrap, rows, cols })
} else if let Some((l, r, mids)) = lr_spec(cmd) {
delim(l, r, mids)
} else if crate::symbols::func_takes_limits(cmd) {
ins(Node::BigOp {
name: cmd.to_string(),
lower: vec![],
upper: vec![],
})
} else if is_func_name(cmd) {
ins(Node::Func(cmd.to_string()))
} else if let Some(mark) = crate::symbols::Accent::of_name(cmd) {
Some(Edit::Accent(mark))
} else if let Some((sup, arg)) = script_cmd(cmd) {
ins(if sup {
Node::Sup { arg }
} else {
Node::Sub { arg }
})
} else if let Some(c) = symbol_by_name(cmd) {
if is_bigop(c) {
ins(Node::BigOpSym {
op: c,
lower: vec![],
upper: vec![],
})
} else {
Some(Edit::Sym(c))
}
} else if let Some((t, math)) = cmd
.strip_prefix("rm")
.map(|t| (t, true))
.or_else(|| cmd.strip_prefix("text").map(|t| (t, false)))
.filter(|(t, math)| {
!t.is_empty()
&& if *math {
t.chars().all(|c| c.is_ascii_alphanumeric() || c == '.')
} else {
!t.contains(['\'', '(', ')', '[', ']', '{', '}'])
}
})
{
ins(match (math, t.chars().count()) {
(false, _) => Node::Text(t.to_string()),
(true, 1) => Node::Roman(t.chars().next().unwrap()),
(true, _) => Node::Func(t.to_string()),
})
} else {
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mode_spellings_shadow_nothing() {
for (names, mode, ..) in MODE_COMMANDS {
for n in *names {
assert_eq!(mode_command(n), Some(*mode), "\\{} dispatches elsewhere", n);
assert!(resolve(n).is_none(), "\\{} is also an edit", n);
}
}
assert!(mode_command("t").is_none(), "\\t came back");
assert!(mode_command("b").is_some() && resolve("bar").is_some());
assert!(mode_command("ba").is_none());
}
}