use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::helix::{Assoc, Range, RopeSlice, Selection, Tendril, Transaction};
use crate::marks::{MarkAttrs, MarkId};
use crate::msg::{Effect, Msg};
use crate::outline::{BlockInfo, Outline};
use crate::state::{Document, State, View};
use crate::update::{self, Step};
pub type CommandFn = dyn Fn(&Ctx, &Value) -> Result<Edit, String> + Send + Sync;
pub type InputRuleFn = dyn Fn(&Ctx, &Msg) -> Option<Edit> + Send + Sync;
pub type DecoratorFn = dyn Fn(&Ctx, &BlockInfo) -> Decoration + Send + Sync;
#[derive(Clone, Default)]
pub struct Host {
inner: Arc<Inner>,
}
#[derive(Clone, Default)]
struct Inner {
commands: BTreeMap<String, Arc<CommandFn>>,
input_rules: Vec<(String, Arc<InputRuleFn>)>,
decorator: Option<Arc<DecoratorFn>>,
}
impl Host {
pub fn new() -> Host {
Host::default()
}
pub fn command(mut self, name: &str, f: impl Fn(&Ctx, &Value) -> Result<Edit, String> + Send + Sync + 'static) -> Host {
Arc::make_mut(&mut self.inner).commands.insert(name.to_string(), Arc::new(f));
self
}
pub fn input_rule(mut self, name: &str, f: impl Fn(&Ctx, &Msg) -> Option<Edit> + Send + Sync + 'static) -> Host {
Arc::make_mut(&mut self.inner).input_rules.push((name.to_string(), Arc::new(f)));
self
}
pub fn decorator(mut self, f: impl Fn(&Ctx, &BlockInfo) -> Decoration + Send + Sync + 'static) -> Host {
Arc::make_mut(&mut self.inner).decorator = Some(Arc::new(f));
self
}
pub fn command_names(&self) -> Vec<&str> {
self.inner.commands.keys().map(String::as_str).collect()
}
pub fn input_rule_names(&self) -> Vec<&str> {
self.inner.input_rules.iter().map(|(n, _)| n.as_str()).collect()
}
pub fn has_decorator(&self) -> bool {
self.inner.decorator.is_some()
}
pub fn is_empty(&self) -> bool {
self.inner.commands.is_empty() && self.inner.input_rules.is_empty() && self.inner.decorator.is_none()
}
pub fn decorate(&self, ctx: &Ctx, block: &BlockInfo) -> Option<Decoration> {
self.inner.decorator.as_ref().map(|f| f(ctx, block))
}
}
impl PartialEq for Host {
fn eq(&self, _: &Host) -> bool {
true
}
}
impl std::fmt::Debug for Host {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Host")
.field("commands", &self.command_names())
.field("input_rules", &self.input_rule_names())
.field("decorator", &self.has_decorator())
.finish()
}
}
pub struct Ctx<'a> {
pub doc: &'a Document,
pub view: &'a View,
}
impl<'a> Ctx<'a> {
pub fn new(doc: &'a Document, view: &'a View) -> Ctx<'a> {
Ctx { doc, view }
}
pub fn text(&self) -> RopeSlice<'a> {
self.doc.text.slice(..)
}
pub fn blocks(&self) -> Option<Arc<Outline>> {
self.doc.blocks()
}
pub fn selection(&self) -> &'a Selection {
&self.view.selection
}
pub fn caret(&self) -> usize {
self.view.caret()
}
pub fn line_ending(&self) -> &'static str {
self.doc.config.line_ending.as_str()
}
pub fn mapped_selection(&self, changes: &[(usize, usize, String)]) -> Selection {
let txn = Transaction::change(&self.doc.text, changes.iter().map(|(a, b, t)| (*a, *b, (!t.is_empty()).then(|| Tendril::from(t.as_str())))));
let cs = txn.changes();
self.view.selection.clone().transform(|r| Range {
anchor: cs.map_pos(r.anchor, Assoc::After),
head: cs.map_pos(r.head, Assoc::After),
old_visual_position: None,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum MarkOp {
Mint {
pos: usize,
#[serde(default)]
attrs: MarkAttrs,
},
Remove { id: MarkId },
SetGap {
id: MarkId,
#[serde(default)]
gap: Option<bool>,
},
SetData {
id: MarkId,
#[serde(default)]
data: Option<Value>,
},
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Edit {
pub changes: Vec<(usize, usize, String)>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selection: Option<Selection>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub marks: Vec<MarkOp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub effects: Vec<(String, Value)>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub keep_gaps: bool,
}
impl Edit {
pub fn status(text: impl Into<String>) -> Edit {
Edit { status: Some(text.into()), ..Edit::default() }
}
pub fn is_noop(&self) -> bool {
self.changes.is_empty() && self.marks.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deco {
pub text: String,
pub role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Decoration {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hang: Option<Deco>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gutter: Option<Deco>,
}
pub(crate) fn run_command(state: &mut State, name: &str, args: &Value) -> Vec<Effect> {
let host = state.doc.host.clone();
let Some(f) = host.inner.commands.get(name).cloned() else {
state.view.status = Some(format!("no command '{name}'"));
return Vec::new();
};
let result = f(&Ctx::new(&state.doc, &state.view), args);
match result {
Ok(edit) => apply(state, edit),
Err(why) => {
state.view.status = Some(why);
Vec::new()
}
}
}
pub(crate) fn input_rules(state: &mut State, msg: &Msg) -> Option<Vec<Effect>> {
if state.doc.host.inner.input_rules.is_empty() || !takes_input(msg) {
return None;
}
let host = state.doc.host.clone();
let edit = host.inner.input_rules.iter().find_map(|(_, f)| f(&Ctx::new(&state.doc, &state.view), msg))?;
Some(apply(state, edit))
}
fn takes_input(msg: &Msg) -> bool {
msg.edits() && !matches!(msg, Msg::Undo | Msg::Redo | Msg::Command { .. } | Msg::Edit { .. } | Msg::External { .. } | Msg::InsertBlocks { .. })
}
pub(crate) fn apply(state: &mut State, edit: Edit) -> Vec<Effect> {
let len = state.doc.text.len_chars();
let mut at = 0;
for &(from, to, _) in &edit.changes {
if from < at || to < from || to > len {
state.view.status = Some("a command's edit was out of range; nothing changed".into());
return Vec::new();
}
at = to;
}
let pins = if edit.keep_gaps { crate::outline::rules::pins_all(state) } else { None };
if !edit.is_noop() || edit.selection.is_some() {
let selection = match edit.selection {
Some(s) => s,
None => Ctx::new(&state.doc, &state.view).mapped_selection(&edit.changes),
};
let txn = Transaction::change(
&state.doc.text,
edit.changes.iter().map(|(a, b, t)| (*a, *b, (!t.is_empty()).then(|| Tendril::from(t.as_str())))),
)
.with_selection(selection);
let ops = edit.marks;
update::commit_with(state, txn, Step::default(), move |m, new| {
for op in ops {
match op {
MarkOp::Mint { pos, attrs } => {
m.mint_with(crate::marks::line_start_at(new, pos), attrs);
}
MarkOp::Remove { id } => {
m.remove(id);
}
MarkOp::SetGap { id, gap } => {
m.set_gap(id, gap);
}
MarkOp::SetData { id, data } => {
m.set_data(id, data);
}
}
}
});
state.view.fit(&state.doc);
}
if let Some(pins) = pins {
crate::outline::rules::pin(state, pins);
}
if let Some(text) = edit.status {
state.view.status = Some(text);
}
edit.effects.into_iter().map(|(name, data)| Effect::Host { name, data }).collect()
}