git.lucas.co / cce-data-editor
structured data editor
git clone https://git.lucas.co/cce-data-editor.git

commite69d2fb2198a6b18843e81fe7d87da4bf90525a5
parent318d44617e
authorLucas Galante <[email protected]>
date2026-09-11 14:05
Span the whole of a joined multi-argument value

`(vec2i)100 200` is one value — kdl_to_json joins the arguments into the
single string "100 200" — but the span stopped at the first entry, so
selecting the key highlighted `(vec2i)100` and left ` 200` outside the
selection. The same narrow answer stood in for a repeated node's element.

Take the span from the first positional argument to the last
(`node_value_span`), falling back to the first entry for a node carrying
only properties and to the node itself for one carrying nothing — both
unchanged. Single-argument nodes are unaffected: first and last are the
same entry.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/main.rs | 64 +++++++++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 52 insertions(+), 12 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 9bd5482..5b300df 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -81,6 +81,31 @@ fn char_to_byte_idx(content: &str, char_idx: usize) -> usize {
         .unwrap_or(content.len())
 }
 
+/// The span of everything a node says after its name: every positional
+/// argument, not just the first. The joined numeric multi-arg form is one
+/// value — `(vec2i)100 200` flattens to the single string "100 200" — so a
+/// span stopping after `100` highlighted half of it. Falls back to the first
+/// entry (a node carrying only properties) and then to the node itself (a
+/// node carrying nothing, e.g. a bare section name).
+fn node_value_span(node: &kdl::KdlNode) -> (usize, usize) {
+    let bounds = |e: &kdl::KdlEntry| {
+        let sp = e.span();
+        (sp.offset(), sp.offset() + sp.len())
+    };
+    let mut positional = node.entries().iter().filter(|e| e.name().is_none());
+    if let Some(first) = positional.next() {
+        let (start, mut end) = bounds(first);
+        for entry in positional {
+            end = bounds(entry).1;
+        }
+        return (start, end);
+    }
+    node.entries().first().map(bounds).unwrap_or_else(|| {
+        let sp = node.span();
+        (sp.offset(), sp.offset() + sp.len())
+    })
+}
+
 /// The spans of the array elements one node contributes to its name's
 /// flattened value, in order. Mirrors `kdl_to_json`: a multi-argument node
 /// whose arguments are all strings (`bevel_apps "a" "b"`) is a LIST, one
@@ -103,11 +128,7 @@ fn node_element_spans(node: &kdl::KdlNode) -> Vec<(usize, usize)> {
     {
         return entries.iter().map(span_of).collect();
     }
-    let span = entries.first().map(span_of).unwrap_or_else(|| {
-        let sp = node.span();
-        (sp.offset(), sp.offset() + sp.len())
-    });
-    vec![span]
+    vec![node_value_span(node)]
 }
 
 fn find_kdl_span(content: &str, tokens: &[PathToken]) -> Option<(usize, usize)> {
@@ -172,13 +193,7 @@ fn find_kdl_span_in_doc(doc: &kdl::KdlDocument, tokens: &[PathToken]) -> Option<
             } else {
                 let node = nodes.first()?;
                 if tokens.len() == 1 {
-                    if let Some(entry) = node.entries().first() {
-                        let span = entry.span();
-                        return Some((span.offset(), span.offset() + span.len()));
-                    } else {
-                        let span = node.span();
-                        return Some((span.offset(), span.offset() + span.len()));
-                    }
+                    return Some(node_value_span(node));
                 } else if tokens.len() == 2 {
                     if let PathToken::Key(prop_key) = &tokens[1] {
                         if let Some(entry) = node.entries().iter().find(|e| e.name().map(|id| id.value()) == Some(prop_key)) {
@@ -3039,6 +3054,31 @@ window_manager {
         assert_eq!(slice("window_manager.gap"), Some("12"));
     }
 
+    /// A joined multi-argument value is one value, so its span is all of it.
+    /// Stopping at the first argument highlighted `100` out of `100 200` —
+    /// and, for a repeated node, told the raw pane the wrong extent for the
+    /// element the tree had selected.
+    #[test]
+    fn span_of_a_joined_multi_arg() {
+        let content = "\
+style {
+    window {
+        position_default (vec2i)100 200
+    }
+}
+anchor (vec2i)1 2
+anchor (vec2i)3 4
+";
+        let slice = |path: &str| -> Option<&str> {
+            let (start, end) = find_kdl_span(content, &parse_path(path))?;
+            Some(content[start..end].trim())
+        };
+        assert_eq!(slice("style.window.position_default"), Some("(vec2i)100 200"));
+        // Repeated: each node is one element, and each element is all of it.
+        assert_eq!(slice("anchor[0]"), Some("(vec2i)1 2"));
+        assert_eq!(slice("anchor[1]"), Some("(vec2i)3 4"));
+    }
+
     /// The other index shape: `key_bindings` children, where the index does
     /// address the n-th node and a trailing key addresses one of its props.
     #[test]