about summary refs log tree commit diff
diff options
context:
space:
mode:
authorProfpatsch <mail@profpatsch.de>2021-04-06T00·06+0200
committerProfpatsch <mail@profpatsch.de>2021-04-23T18·30+0000
commit5156da542bac99aa2e625bc4c1033d2017aa5c54 (patch)
tree71b8ae9362003e868fce12c2f18afd8370bfcca3
parent50cf1e1a883f91b1eaed85b7bd694b024761f113 (diff)
feat(users/Profpatsch/struct-edit): per-level position r/2542
The user expects the editor to remember the positions of fields they
navigated from to a new level, so when they return they get put in the
same spot.

We push the index from one field into every level of the value.

Unfortunately this introduces pointers and all the woes they bring.

Change-Id: I889c28b71fd7082b765e1d6874faeb1b36dade60
Reviewed-on: https://cl.tvl.fyi/c/depot/+/2866
Tested-by: BuildkiteCI
Reviewed-by: Profpatsch <mail@profpatsch.de>
-rw-r--r--users/Profpatsch/struct-edit/main.go105
1 files changed, 59 insertions, 46 deletions
diff --git a/users/Profpatsch/struct-edit/main.go b/users/Profpatsch/struct-edit/main.go
index 76e346ed04..7e43074266 100644
--- a/users/Profpatsch/struct-edit/main.go
+++ b/users/Profpatsch/struct-edit/main.go
@@ -15,10 +15,8 @@ import (
 )
 
 // Keeps the full data structure and a path that indexes our current position into it.
-// selectedIndex is the currently selected item. (TODO: save per level)
 type model struct {
 	path          []index
-	selectedIndex index
 	data          val
 }
 
@@ -31,14 +29,17 @@ type index interface{}
 type val struct {
 	// the “type” of value; see tag const belove
 	tag tag
+	// last known position of our cursor
+	last_index index
 	// documentation (TODO)
 	doc string
 	// the actual value;
+	// the actual structure is behind a pointer so we can replace the struct.
 	// determined by the tag
-	// tagString -> string
-	// tagFloat -> float64
-	// tagList -> []val
-	// tagMap -> map[string]val
+	// tagString -> *string
+	// tagFloat -> *float64
+	// tagList -> *[]val
+	// tagMap -> *map[string]val
 	val interface{}
 }
 
@@ -56,9 +57,9 @@ func (v val) Render() string {
 	s := ""
 	switch v.tag {
 	case tagString:
-		s += v.val.(string)
+		s += *v.val.(*string)
 	case tagFloat:
-		s += fmt.Sprint(v.val.(float64))
+		s += fmt.Sprint(*v.val.(*float64))
 	case tagList:
 		s += "[ "
 		vs := []string{}
@@ -103,14 +104,16 @@ func makeVal(i interface{}) val {
 	case string:
 		v = val{
 			tag: tagString,
+			last_index: index(nil),
 			doc: "",
-			val: i,
+			val: &i,
 		}
 	case float64:
 		v = val{
 			tag: tagFloat,
+			last_index: index(nil),
 			doc: "",
-			val: i,
+			val: &i,
 		}
 	case []interface{}:
 		ls := []val{}
@@ -119,8 +122,9 @@ func makeVal(i interface{}) val {
 		}
 		v = val{
 			tag: tagList,
+			last_index: pos1Inner(tagList, &ls),
 			doc: "",
-			val: ls,
+			val: &ls,
 		}
 	case map[string]interface{}:
 		ls := map[string]val{}
@@ -129,8 +133,9 @@ func makeVal(i interface{}) val {
 		}
 		v = val{
 			tag: tagMap,
+			last_index: pos1Inner(tagMap, &ls),
 			doc: "",
-			val: ls,
+			val: &ls,
 		}
 	default:
 		log.Fatalf("makeVal: cannot read json of type %T", i)
@@ -143,6 +148,10 @@ func (v val) pos1() index {
 	return v.enumerate()[0].i
 }
 
+func pos1Inner(tag tag, v interface{}) index {
+	return enumerateInner(tag, v)[0].i
+}
+
 type enumerate struct {
 	i index
 	v val
@@ -170,14 +179,14 @@ func enumerateInner(tag tag, v interface{}) (e []enumerate) {
 	case tagFloat:
 		e = nil
 	case tagList:
-		for i, v := range v.([]val) {
+		for i, v := range *v.(*[]val) {
 			e = append(e, enumerate{i: index(uint(i)), v: v})
 		}
 	case tagMap:
 		// map sorting order is not stable (actually randomized thank jabber)
 		// so let’s sort them
 		keys := []string{}
-		m := v.(map[string]val)
+		m := *v.(*map[string]val)
 		for k, _ := range m {
 			keys = append(keys, k)
 		}
@@ -203,7 +212,9 @@ func (m model) PathString() string {
 
 // walk the given path down in data, to get the value at that point.
 // Assumes that all path indexes are valid indexes into data.
-func walk(data val, path []index) (val, bool, error) {
+// Returns a pointer to the value at point, in order to be able to change it.
+func walk(data *val, path []index) (*val, bool, error) {
+	res := data
 	atPath := func(index int) string {
 		return fmt.Sprintf("at path %v", path[:index+1])
 	}
@@ -211,53 +222,58 @@ func walk(data val, path []index) (val, bool, error) {
 		return fmt.Errorf("walk: can’t walk into %s %v %s", ty, val, atPath(index))
 	}
 	for i, p := range path {
-		switch data.tag {
+		switch res.tag {
 		case tagString:
-			return data, true, nil
+			return nil, true, nil
 		case tagFloat:
-			return data, true, nil
+			return nil, true, nil
 		case tagList:
 			switch p := p.(type) {
 			case uint:
-				list := data.val.([]val)
+				list := *res.val.(*[]val)
 				if int(p) >= len(list) || p < 0 {
-					return data, false, fmt.Errorf("index out of bounds %s", atPath(i))
+					return nil, false, fmt.Errorf("index out of bounds %s", atPath(i))
 				}
-				data = list[p]
+				res = &list[p]
 			default:
-				return data, false, fmt.Errorf("not a list index %s", atPath(i))
+				return nil, false, fmt.Errorf("not a list index %s", atPath(i))
 			}
 		case tagMap:
 			switch p := p.(type) {
 			case string:
-				m := data.val.(map[string]val)
-				var ok bool
-				if data, ok = m[p]; !ok {
-					return data, false, fmt.Errorf("index %s not in map %s", p, atPath(i))
+				m := *res.val.(*map[string]val)
+				if a, ok := m[p]; ok {
+					res = &a
+				} else {
+				  return nil, false, fmt.Errorf("index %s not in map %s", p, atPath(i))
 				}
 			default:
-				return data, false, fmt.Errorf("not a map index %v %s", p, atPath(i))
+				return nil, false, fmt.Errorf("not a map index %v %s", p, atPath(i))
 			}
 
 		default:
-			return data, false, errf(string(data.tag), data.val, i)
+			return nil, false, errf(string(res.tag), res.val, i)
 		}
 	}
-	return data, false, nil
+	return res, false, nil
 }
 
 // descend into the selected index. Assumes that the index is valid.
 // Will not descend into scalars.
 func (m model) descend() (model, error) {
-	newPath := append(m.path, m.selectedIndex)
-	lower, bounce, err := walk(m.data, newPath)
+	// TODO: two walks?!
+	this, _, err := walk(&m.data, m.path)
+	if err != nil {
+		return m, err
+	}
+	newPath := append(m.path, this.last_index)
+	_, bounce, err := walk(&m.data, newPath)
 	if err != nil {
 		return m, err
 	}
 	// only descend if we *can*
 	if !bounce {
 		m.path = newPath
-		m.selectedIndex = lower.pos1()
 	}
 	return m, nil
 }
@@ -266,8 +282,7 @@ func (m model) descend() (model, error) {
 func (m model) ascend() (model, error) {
 	if len(m.path) > 0 {
 		m.path = m.path[:len(m.path)-1]
-		upper, _, err := walk(m.data, m.path)
-		m.selectedIndex = upper.pos1()
+		_, _, err := walk(&m.data, m.path)
 		return m, err
 	}
 	return m, nil
@@ -276,8 +291,7 @@ func (m model) ascend() (model, error) {
 /// go to the next item, or wraparound
 func (min model) next() (m model, err error) {
 	m = min
-	var this val
-	this, _, err = walk(m.data, m.path)
+	this, _, err := walk(&m.data, m.path)
 	if err != nil {
 		return
 	}
@@ -285,17 +299,17 @@ func (min model) next() (m model, err error) {
 	setNext := false
 	for _, enum := range enumL {
 		if setNext {
-			m.selectedIndex = enum.i
+			this.last_index = enum.i
 			setNext = false
 			break
 		}
-		if enum.i == m.selectedIndex {
+		if enum.i == this.last_index {
 			setNext = true
 		}
 	}
 	// wraparound
 	if setNext {
-		m.selectedIndex = enumL[0].i
+		this.last_index = enumL[0].i
 	}
 	return
 }
@@ -303,8 +317,7 @@ func (min model) next() (m model, err error) {
 /// go to the previous item, or wraparound
 func (min model) prev() (m model, err error) {
 	m = min
-	var this val
-	this, _, err = walk(m.data, m.path)
+	this, _, err := walk(&m.data, m.path)
 	if err != nil {
 		return
 	}
@@ -312,8 +325,8 @@ func (min model) prev() (m model, err error) {
 	// last element, wraparound
 	prevIndex := enumL[len(enumL)-1].i
 	for _, enum := range enumL {
-		if enum.i == m.selectedIndex {
-			m.selectedIndex = prevIndex
+		if enum.i == this.last_index {
+			this.last_index = prevIndex
 			break
 		}
 		prevIndex = enum.i
@@ -332,7 +345,6 @@ func initialModel(v interface{}) model {
 	return model{
 		path:          []index{},
 		data:          val,
-		selectedIndex: val.pos1(),
 	}
 }
 
@@ -381,17 +393,18 @@ var selectedColor = lipgloss.NewStyle().
 
 func (m model) View() string {
 	s := pathColor.Render(m.PathString())
-	cur, _, err := walk(m.data, m.path)
+	cur, _, err := walk(&m.data, m.path)
 	if err != nil {
 		log.Fatal(err)
 	}
+	s += cur.doc + "\n"
 	s += "\n"
 	for _, enum := range cur.enumerate() {
 		is := renderIndex(enum.i)
 		if is != "" {
 			s += is + " "
 		}
-		if enum.i == m.selectedIndex {
+		if enum.i == cur.last_index {
 			s += selectedColor.Render(enum.v.Render())
 		} else {
 			s += enum.v.Render()