1use sim_kernel::{Cx, Diagnostic, Error, Expr, Result, Symbol};
15use sim_lib_intent::{field, intent_kind_of};
16use sim_value::path::{Path, PathError, set_at};
17
18use crate::contract::{Draft, Editor, Operation};
19
20pub const EDIT_MODES: &[&str] = &["text", "raw"];
24
25pub struct UniversalEditor {
27 readonly: bool,
28}
29
30impl UniversalEditor {
31 pub fn writable() -> Self {
33 Self { readonly: false }
34 }
35
36 pub fn readonly() -> Self {
38 Self { readonly: true }
39 }
40}
41
42impl Editor for UniversalEditor {
43 fn decode(&self, _cx: &mut Cx, value: &Expr, intent: &Expr) -> Result<Draft> {
44 let Some(kind) = intent_kind_of(intent) else {
45 return Err(Error::HostError("editor input is not an Intent".to_owned()));
46 };
47 match &*kind.name {
48 "edit-field" => self.edit_field(value, intent),
49 "commit" => Ok(Draft::clean(value.clone(), value.clone())),
50 "cancel" => {
51 Ok(Draft::clean(value.clone(), value.clone()))
53 }
54 other => Ok(Draft::rejected(
55 value.clone(),
56 Diagnostic::error(format!("universal editor does not handle intent '{other}'")),
57 )),
58 }
59 }
60
61 fn commit(&self, _cx: &mut Cx, draft: &Draft) -> Result<Operation> {
62 if !draft.committable {
63 return Err(Error::HostError(
64 "draft is not committable; resolve diagnostics first".to_owned(),
65 ));
66 }
67 Ok(Operation {
69 form: Expr::Map(vec![
70 (
71 Expr::Symbol(Symbol::new("op")),
72 Expr::Symbol(Symbol::new("set-value")),
73 ),
74 (Expr::Symbol(Symbol::new("value")), draft.proposed.clone()),
75 ]),
76 })
77 }
78}
79
80impl UniversalEditor {
81 fn edit_field(&self, value: &Expr, intent: &Expr) -> Result<Draft> {
82 if self.readonly {
83 return Ok(Draft::rejected(
84 value.clone(),
85 Diagnostic::error("value is read-only and cannot be edited"),
86 ));
87 }
88 let Some(path_expr @ Expr::List(_)) = field(intent, "path") else {
89 return Ok(Draft::rejected(
90 value.clone(),
91 Diagnostic::error("edit-field is missing a list 'path'"),
92 ));
93 };
94 let Some(new_value) = field(intent, "value") else {
95 return Ok(Draft::rejected(
96 value.clone(),
97 Diagnostic::error("edit-field is missing a 'value'"),
98 ));
99 };
100 let path = match Path::from_expr(path_expr) {
101 Ok(path) => path,
102 Err(error) => {
103 return Ok(Draft::rejected(
104 value.clone(),
105 path_error_diagnostic(path_expr, error),
106 ));
107 }
108 };
109 match set_at(value, &path, new_value.clone()) {
110 Ok(proposed) => Ok(Draft::clean(value.clone(), proposed)),
111 Err(error) => Ok(Draft::rejected(
112 value.clone(),
113 path_error_diagnostic(path_expr, error),
114 )),
115 }
116 }
117}
118
119pub fn render_draft(draft: &Draft, mode: &str) -> Result<Expr> {
124 let proposed = &draft.proposed;
125 let body = match mode {
128 "raw" => sim_codec::encode_portable(sim_kernel::CodecId(0), proposed)
129 .unwrap_or_else(|_| crate::universal_view::render_value(proposed)),
130 _ => crate::universal_view::render_value(proposed),
131 };
132 Ok(sim_lib_scene::node(
133 "box",
134 vec![
135 ("role", Expr::Symbol(Symbol::new("edit"))),
136 ("mode", Expr::Symbol(Symbol::new(mode))),
137 (
138 "children",
139 Expr::List(vec![
140 sim_lib_scene::node(
141 "field",
142 vec![
143 ("kind", Expr::Symbol(Symbol::new("text"))),
144 ("value", Expr::String(body)),
145 ("readonly", Expr::Bool(false)),
146 ],
147 ),
148 committable_badge(draft),
149 ]),
150 ),
151 ],
152 ))
153}
154
155fn committable_badge(draft: &Draft) -> Expr {
156 if draft.committable {
157 sim_lib_scene::node(
158 "badge",
159 vec![
160 ("status", Expr::Symbol(Symbol::new("ok"))),
161 ("label", Expr::String("ready to commit".to_owned())),
162 ],
163 )
164 } else {
165 let message = draft
166 .diagnostics
167 .first()
168 .map(|diagnostic| diagnostic.message.clone())
169 .unwrap_or_else(|| "not committable".to_owned());
170 sim_lib_scene::node(
171 "badge",
172 vec![
173 ("status", Expr::Symbol(Symbol::new("error"))),
174 ("label", Expr::String(message)),
175 ],
176 )
177 }
178}
179
180fn path_error_diagnostic(path: &Expr, error: PathError) -> Diagnostic {
183 Diagnostic::error(format!(
184 "edit rejected at path {}: {error:?}",
185 crate::universal_view::render_value(path)
186 ))
187}