1use crate::engine::ecs::{ComponentId, World};
2use crate::scripting::object::{CeChild, MaterializedCE, Value};
3use slotmap::KeyData;
4
5use super::format_repl_value;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ReplInput {
9 Snippet(String),
10 Ls,
11 Pwd,
12 Cd(String),
13}
14
15pub fn parse_repl_input(source: String) -> ReplInput {
16 let trimmed = source.trim();
17 match trimmed {
18 "ls" => ReplInput::Ls,
19 "pwd" => ReplInput::Pwd,
20 _ => match trimmed.strip_prefix("cd") {
21 Some(rest) if rest.starts_with(char::is_whitespace) && !rest.trim().is_empty() => {
22 ReplInput::Cd(rest.trim().to_string())
23 }
24 _ => ReplInput::Snippet(source),
25 },
26 }
27}
28
29#[derive(Debug, Clone)]
30enum Cursor {
31 World,
32 Value(Value),
33}
34
35#[derive(Debug, Clone)]
36enum Anchor {
37 World,
38 Binding(String),
39 Expression,
40}
41
42#[derive(Debug, Clone)]
43struct Breadcrumb {
44 cursor: Cursor,
45 segment: String,
46}
47
48#[derive(Debug, Clone)]
49pub struct NavigationState {
50 cursor: Cursor,
51 anchor: Anchor,
52 breadcrumbs: Vec<Breadcrumb>,
53}
54
55impl Default for NavigationState {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl NavigationState {
62 pub fn new() -> Self {
63 Self {
64 cursor: Cursor::World,
65 anchor: Anchor::World,
66 breadcrumbs: Vec::new(),
67 }
68 }
69
70 pub fn reset(&mut self) {
71 *self = Self::new();
72 }
73
74 pub fn cwd_value(&self) -> Value {
75 match &self.cursor {
76 Cursor::World => Value::Identifier("__mms_world__".into()),
77 Cursor::Value(value) => value.clone(),
78 }
79 }
80
81 pub fn ensure_valid(&mut self, world: &World) -> Option<String> {
82 let Cursor::Value(Value::ComponentObject { id, .. }) = &self.cursor else {
83 return None;
84 };
85 if world.get_component_node(*id).is_some() {
86 return None;
87 }
88 let message =
89 format!("stale component handle: component {id:?} is not live; returned to /");
90 self.reset();
91 Some(message)
92 }
93
94 pub fn set_evaluated(
95 &mut self,
96 value: Value,
97 binding: Option<String>,
98 world: &World,
99 ) -> Result<(), String> {
100 ensure_navigable(&value, world)?;
101 self.cursor = Cursor::Value(value);
102 self.anchor = binding.map(Anchor::Binding).unwrap_or(Anchor::Expression);
103 self.breadcrumbs.clear();
104 Ok(())
105 }
106
107 pub fn cd_root(&mut self) {
108 self.reset();
109 }
110
111 pub fn cd_parent(&mut self, world: &World) {
112 if let Cursor::Value(Value::ComponentObject { id, .. }) = &self.cursor {
113 if let Some(parent) = world.parent_of(*id) {
114 self.cursor = Cursor::Value(component_value(world, parent));
115 } else {
116 self.reset();
117 }
118 return;
119 }
120 if let Some(parent) = self.breadcrumbs.pop() {
121 self.cursor = parent.cursor;
122 } else if !matches!(self.cursor, Cursor::World) {
123 self.reset();
124 }
125 }
126
127 pub fn cd_child(&mut self, segment: &str, world: &World) -> Result<(), String> {
128 let (value, label) = resolve_child(&self.cursor, segment, world)?;
129 ensure_navigable(&value, world)?;
130 let old = self.cursor.clone();
131 self.breadcrumbs.push(Breadcrumb {
132 cursor: old,
133 segment: label,
134 });
135 self.cursor = Cursor::Value(value);
136 Ok(())
137 }
138
139 pub fn cd_path(&mut self, path: &str, world: &World) -> Result<(), String> {
140 let previous = self.clone();
141 if path.starts_with('/') {
142 self.reset();
143 }
144 for segment in path.split('/').filter(|part| !part.is_empty()) {
145 let result = match segment {
146 "." => Ok(()),
147 ".." => {
148 self.cd_parent(world);
149 Ok(())
150 }
151 child => self.cd_child(child, world),
152 };
153 if let Err(error) = result {
154 *self = previous;
155 return Err(error);
156 }
157 }
158 Ok(())
159 }
160
161 pub fn pwd(&self, world: &World) -> String {
162 match &self.cursor {
163 Cursor::World => "/".into(),
164 Cursor::Value(Value::ComponentObject { id, .. }) => live_pwd(world, *id),
165 Cursor::Value(_) => {
166 let anchor = match &self.anchor {
167 Anchor::World => "/".into(),
168 Anchor::Binding(name) => format!("${name}"),
169 Anchor::Expression => "<expression>".into(),
170 };
171 let suffix = self
172 .breadcrumbs
173 .iter()
174 .map(|crumb| crumb.segment.as_str())
175 .collect::<Vec<_>>()
176 .join("/");
177 if suffix.is_empty() {
178 anchor
179 } else {
180 format!("{anchor}/{suffix}")
181 }
182 }
183 }
184 }
185
186 pub fn listing(&self, world: &World) -> Result<Vec<String>, String> {
187 match &self.cursor {
188 Cursor::World => Ok(world
189 .world_roots()
190 .into_iter()
191 .enumerate()
192 .filter_map(|(index, id)| {
193 crate::engine::repl::util::format_ls_line(world, index, id)
194 })
195 .collect()),
196 Cursor::Value(Value::ComponentObject { id, .. }) => {
197 if world.get_component_node(*id).is_none() {
198 return Err(format!(
199 "stale component handle: component {id:?} is not live"
200 ));
201 }
202 Ok(world
203 .children_of(*id)
204 .iter()
205 .copied()
206 .enumerate()
207 .filter_map(|(index, id)| {
208 crate::engine::repl::util::format_ls_line(world, index, id)
209 })
210 .collect())
211 }
212 Cursor::Value(value) => value_listing(value, world),
213 }
214 }
215}
216
217fn value_listing(value: &Value, world: &World) -> Result<Vec<String>, String> {
218 match value {
219 Value::Object(id) => id
220 .with_map(|map| table_listing(map, world))
221 .ok_or_else(|| String::from("stale evaluated table"))?,
222 Value::Map(map) => table_listing(map, world),
223 Value::Array(values) => values
224 .iter()
225 .enumerate()
226 .map(|(index, value)| Ok(format!("{index}: {}", format_repl_value(value, world)?)))
227 .collect(),
228 Value::ComponentExpr(ce) => ce
229 .children
230 .iter()
231 .enumerate()
232 .map(|(index, child)| match child {
233 CeChild::Spawn(ce) => Ok(format!(
234 "{index}: {}",
235 format_repl_value(&Value::ComponentExpr(Box::new(ce.clone())), world)?
236 )),
237 CeChild::Attach(id) => Ok(format!("{index}: <live component {id:?}>")),
238 })
239 .collect(),
240 other => Err(format!("unsupported navigation value: {other:?}")),
241 }
242}
243
244fn table_listing(
245 map: &std::collections::HashMap<String, Value>,
246 world: &World,
247) -> Result<Vec<String>, String> {
248 let mut keys = map.keys().cloned().collect::<Vec<_>>();
249 keys.sort();
250 keys.into_iter()
251 .enumerate()
252 .map(|(index, key)| {
253 Ok(format!(
254 "{index}: {key} = {}",
255 format_repl_value(&map[&key], world)?
256 ))
257 })
258 .collect()
259}
260
261fn resolve_child(cursor: &Cursor, segment: &str, world: &World) -> Result<(Value, String), String> {
262 match cursor {
263 Cursor::World => resolve_live(world.world_roots(), segment, world),
264 Cursor::Value(Value::ComponentObject { id, .. }) => {
265 if world.get_component_node(*id).is_none() {
266 return Err(format!(
267 "stale component handle: component {id:?} is not live"
268 ));
269 }
270 resolve_live(world.children_of(*id).to_vec(), segment, world)
271 }
272 Cursor::Value(Value::Object(id)) => id
273 .with_map(|map| resolve_table(map, segment))
274 .ok_or_else(|| String::from("stale evaluated table"))?,
275 Cursor::Value(Value::Map(map)) => resolve_table(map, segment),
276 Cursor::Value(Value::Array(values)) => {
277 let index = segment
278 .parse::<usize>()
279 .map_err(|_| format!("array index must be numeric: {segment}"))?;
280 values
281 .get(index)
282 .cloned()
283 .map(|value| (value, index.to_string()))
284 .ok_or_else(|| format!("array index out of range: {index}"))
285 }
286 Cursor::Value(Value::ComponentExpr(ce)) => resolve_ce_child(ce, segment, world),
287 Cursor::Value(other) => Err(format!("unsupported navigation value: {other:?}")),
288 }
289}
290
291fn resolve_table(
292 map: &std::collections::HashMap<String, Value>,
293 segment: &str,
294) -> Result<(Value, String), String> {
295 let mut keys = map.keys().cloned().collect::<Vec<_>>();
296 keys.sort();
297 let key = if let Ok(index) = segment.parse::<usize>() {
298 keys.get(index)
299 .cloned()
300 .ok_or_else(|| format!("table index out of range: {index}"))?
301 } else {
302 segment.to_string()
303 };
304 map.get(&key)
305 .cloned()
306 .map(|value| (value, key.clone()))
307 .ok_or_else(|| format!("table field not found: {key}"))
308}
309
310fn resolve_ce_child(
311 ce: &MaterializedCE,
312 segment: &str,
313 world: &World,
314) -> Result<(Value, String), String> {
315 let children = ce
316 .children
317 .iter()
318 .map(|child| match child {
319 CeChild::Spawn(ce) => Value::ComponentExpr(Box::new(ce.clone())),
320 CeChild::Attach(id) => component_value(world, *id),
321 })
322 .collect::<Vec<_>>();
323 if let Ok(index) = segment.parse::<usize>() {
324 return children
325 .get(index)
326 .cloned()
327 .map(|value| (value, ce_child_label(&children[index], index)))
328 .ok_or_else(|| format!("component-expression index out of range: {index}"));
329 }
330 let authored_matches = children
331 .iter()
332 .enumerate()
333 .filter(|(_, value)| match value {
334 Value::ComponentExpr(ce) => authored_ce_name(ce).as_deref() == Some(segment),
335 _ => false,
336 })
337 .collect::<Vec<_>>();
338 if let [(index, value)] = authored_matches.as_slice() {
339 return Ok(((*value).clone(), ce_child_label(value, *index)));
340 }
341 if authored_matches.len() > 1 {
342 return Err(format!(
343 "ambiguous component-expression child name: {segment}; use its index"
344 ));
345 }
346 let matches = children
347 .iter()
348 .enumerate()
349 .filter(|(_, value)| match value {
350 Value::ComponentExpr(ce) => ce.component_type == segment,
351 Value::ComponentObject { component_type, .. } => component_type == segment,
352 _ => false,
353 })
354 .collect::<Vec<_>>();
355 match matches.as_slice() {
356 [(index, value)] => Ok(((*value).clone(), ce_child_label(value, *index))),
357 [] => Err(format!("component-expression child not found: {segment}")),
358 _ => Err(format!(
359 "ambiguous component-expression child: {segment}; use its index"
360 )),
361 }
362}
363
364fn ce_child_label(value: &Value, index: usize) -> String {
365 match value {
366 Value::ComponentExpr(ce) => authored_ce_name(ce).unwrap_or_else(|| index.to_string()),
367 _ => index.to_string(),
368 }
369}
370
371fn authored_ce_name(ce: &MaterializedCE) -> Option<String> {
372 ce.named.iter().find_map(|(name, value)| {
373 (name == "name").then(|| match value {
374 Value::String(value) => Some(value.clone()),
375 _ => None,
376 })?
377 })
378}
379
380fn resolve_live(
381 ids: Vec<ComponentId>,
382 segment: &str,
383 world: &World,
384) -> Result<(Value, String), String> {
385 if let Ok(index) = segment.parse::<usize>() {
386 let id = ids
387 .get(index)
388 .copied()
389 .ok_or_else(|| format!("component index out of range: {index}"))?;
390 return Ok((component_value(world, id), component_label(world, id)));
391 }
392 let id = if let Ok(guid) = segment.parse::<uuid::Uuid>() {
393 ids.iter().copied().find(|id| {
394 world
395 .get_component_node(*id)
396 .is_some_and(|n| n.guid == guid)
397 })
398 } else if let Some(id) = parse_component_id_short(segment) {
399 ids.contains(&id).then_some(id)
400 } else {
401 let found = ids
402 .iter()
403 .copied()
404 .filter(|id| {
405 world
406 .get_component_node(*id)
407 .is_some_and(|node| node.name == segment)
408 })
409 .collect::<Vec<_>>();
410 match found.as_slice() {
411 [id] => Some(*id),
412 [] => None,
413 _ => {
414 return Err(format!(
415 "ambiguous component name: {segment}; use index, id, or GUID"
416 ));
417 }
418 }
419 };
420 let id = id.ok_or_else(|| format!("component child not found: {segment}"))?;
421 Ok((component_value(world, id), component_label(world, id)))
422}
423
424fn ensure_navigable(value: &Value, world: &World) -> Result<(), String> {
425 match value {
426 Value::Object(id) if id.with_map(|_| ()).is_some() => Ok(()),
427 Value::Map(_) | Value::Array(_) | Value::ComponentExpr(_) => Ok(()),
428 Value::ComponentObject { id, .. } if world.get_component_node(*id).is_some() => Ok(()),
429 Value::ComponentObject { id, .. } => Err(format!(
430 "stale component handle: component {id:?} is not live"
431 )),
432 other => Err(format!(
433 "cd: expected a table, array, live component, or component expression; got {other:?}"
434 )),
435 }
436}
437
438fn component_value(world: &World, id: ComponentId) -> Value {
439 Value::ComponentObject {
440 id,
441 component_type: world.component_name(id).unwrap_or("<deleted>").to_string(),
442 }
443}
444
445fn component_label(world: &World, id: ComponentId) -> String {
446 let name = world.component_label(id).unwrap_or("");
447 if name.is_empty() {
448 format_component_id_short(id)
449 } else {
450 name.to_string()
451 }
452}
453
454fn live_pwd(world: &World, mut id: ComponentId) -> String {
455 let mut parts = Vec::new();
456 loop {
457 let Some(node) = world.get_component_node(id) else {
458 return "/".into();
459 };
460 parts.push(format!("{}:{}", format_component_id_short(id), node.name));
461 match world.parent_of(id) {
462 Some(parent) => id = parent,
463 None => break,
464 }
465 }
466 parts.reverse();
467 format!("/{}", parts.join("/"))
468}
469
470fn format_component_id_short(id: ComponentId) -> String {
471 let debug = format!("{id:?}");
472 debug
473 .split_once('(')
474 .and_then(|(_, rest)| rest.strip_suffix(')'))
475 .unwrap_or(&debug)
476 .to_string()
477}
478
479fn parse_component_id_short(value: &str) -> Option<ComponentId> {
480 let (index, version) = value.split_once('v')?;
481 let index: u32 = index.parse().ok()?;
482 let version: u32 = version.parse().ok()?;
483 Some(KeyData::from_ffi((u64::from(version) << 32) | u64::from(index)).into())
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use std::collections::HashMap;
490
491 #[test]
492 fn commands_are_classified_without_claiming_language_calls() {
493 assert_eq!(parse_repl_input("ls\n".into()), ReplInput::Ls);
494 assert_eq!(parse_repl_input("pwd".into()), ReplInput::Pwd);
495 assert_eq!(
496 parse_repl_input("cd settings.theme".into()),
497 ReplInput::Cd("settings.theme".into())
498 );
499 assert!(matches!(
500 parse_repl_input("ls()".into()),
501 ReplInput::Snippet(_)
502 ));
503 assert!(matches!(
504 parse_repl_input("let cd = 1".into()),
505 ReplInput::Snippet(_)
506 ));
507 }
508
509 #[test]
510 fn tables_and_arrays_use_breadcrumbs_and_reject_scalar_targets() {
511 let world = World::default();
512 let value = Value::Map(HashMap::from([(
513 "items".into(),
514 Value::Array(vec![Value::Map(HashMap::from([(
515 "name".into(),
516 Value::String("cat".into()),
517 )]))]),
518 )]));
519 let mut navigation = NavigationState::new();
520 navigation
521 .set_evaluated(value, Some("state".into()), &world)
522 .unwrap();
523 navigation.cd_child("items", &world).unwrap();
524 navigation.cd_child("0", &world).unwrap();
525 assert_eq!(navigation.pwd(&world), "$state/items/0");
526 assert!(navigation.cd_child("name", &world).is_err());
527 assert_eq!(navigation.pwd(&world), "$state/items/0");
528 navigation.cd_parent(&world);
529 assert_eq!(navigation.pwd(&world), "$state/items");
530 }
531}