diff --git a/fern/README.md b/fern/README.md new file mode 100644 index 000000000..ea4f81616 --- /dev/null +++ b/fern/README.md @@ -0,0 +1,39 @@ +``` +docker run -it -w /app -v ./fern:/app golang:1.20.4 bash +``` + +render text +then scroll overflow + + +Add before as a function in all blocks + +let foo = a b -> +delete all of a label leaves red mark + +delete arg + +let foo = _ y -> { + let fdd = 2 + let y = 11 +} + +" -> string +p -> perform +handle -> handle +{ -> record +l -> let +Enter Shift line above below +[ -> list +, -> list item +m -> match { + Red missing + +} + +No special key for Tag yet +letters for things means no typing for variables +automatically assume typing for variables can have selection along bottom by number but then number cant strt aa number +https://earthly.dev/blog/tui-app-with-go/ + +delete on let removes line highlight first diff --git a/fern/cmd/main.go b/fern/cmd/main.go new file mode 100644 index 000000000..c9c4089f0 --- /dev/null +++ b/fern/cmd/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/gdamore/tcell/v2" + "github.com/midas-framework/project_wisdom/fern" +) + +func main() { + tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) + screen, err := tcell.NewScreen() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + if err = screen.Init(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + dir, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + path := filepath.Join(dir, "saved.json") + // path := filepath.Join(dir, "../eyg/saved/saved.json") + store := &fileStore{path} + + // fern.New(screen, store) + err = fern.Run(screen, store) + screen.Fini() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +type fileStore struct { + path string +} + +var _ fern.Store = (*fileStore)(nil) + +func (store *fileStore) Load() ([]byte, error) { + return os.ReadFile(store.path) +} + +func (store *fileStore) Save(data []byte) error { + return os.WriteFile(store.path, data, 0644) +} diff --git a/fern/coordinate.go b/fern/coordinate.go new file mode 100644 index 000000000..0b747378e --- /dev/null +++ b/fern/coordinate.go @@ -0,0 +1,8 @@ +package fern + +// public fields because .X & .Y are useful interfactes to a point +// Technically Coordinate is 1D but we use singular here to save plural for lists +type Coordinate struct { + X int + Y int +} diff --git a/fern/cursor.go b/fern/cursor.go new file mode 100644 index 000000000..e1bfee2e9 --- /dev/null +++ b/fern/cursor.go @@ -0,0 +1,62 @@ +package fern + +func zipper(source Node, path []int) (Node, func(Node) Node, error) { + tree := source + var bs []func(Node) Node + for _, p := range path { + t, b, err := tree.child(p) + tree = t + if err != nil { + return Var{}, nil, err + } + bs = append(bs, b) + } + return tree, func(n Node) Node { + for i := len(bs) - 1; i >= 0; i-- { + n = bs[i](n) + } + return n + }, nil +} + +// (call, (call, cons, x), tail) +// ((cons x) tail) +// (reverse (uppercase "bob")) +// ((map users) uppercase) + +// x -> pre on list +// default is 5 -> [5] but also [1, 2] -> [1, [2]] +// always add to list + +// making my own gc +// fast list of parents possible +// func z(source []string, remaining int) { +// root := source[0] +// } + +// func unwrap(source []string, at int) { +// var parents = []string{} +// parent = parents[at] +// i := 0 +// new := []string{} +// for i := 0; i < parent; i++ { +// new = append(new, source[i]) +// } +// for i := at; i < len(source); i++ { +// new = append(new, source[i]) +// } +// return new[12] +// } + +// func callWith(source []string, at int) [2]int { +// // can have arrays for performance +// new := []string{} +// for i := 0; i < at; i++ { +// new = append(new, source[i]) +// } +// new = append(new, "call", "vacant") +// for i := at; i < len(source); i++ { +// new = append(new, source[i]) +// } + +// } diff --git a/fern/decode.go b/fern/decode.go new file mode 100644 index 000000000..d4f21348d --- /dev/null +++ b/fern/decode.go @@ -0,0 +1,98 @@ +package fern + +import ( + "encoding/json" + "fmt" +) + +type encoded struct { + Key string `json:"0"` + Label string `json:"l"` + Body json.RawMessage `json:"b"` + Fn json.RawMessage `json:"f"` + Arg json.RawMessage `json:"a"` + // Let, Integer, String all have value + Value json.RawMessage `json:"v"` + Then json.RawMessage `json:"t"` + Note string `json:"c"` +} + +// recursive, is continuation for looping a good style in go or is it just a hammer +func decode(data []byte) (Node, error) { + var e encoded + err := json.Unmarshal(data, &e) + if err != nil { + return nil, err + } + switch e.Key { + case "f": + body, err := decode(e.Body) + if err != nil { + return nil, err + } + return Fn{e.Label, body}, nil + case "a": + fn, err := decode(e.Fn) + if err != nil { + return nil, err + } + arg, err := decode(e.Arg) + if err != nil { + return nil, err + } + return Call{fn, arg}, nil + case "v": + return Var{e.Label}, nil + case "l": + value, err := decode(e.Value) + if err != nil { + return nil, err + } + then, err := decode(e.Then) + if err != nil { + return nil, err + } + return Let{e.Label, value, then}, nil + case "z": + return Vacant{e.Note}, nil + case "i": + var value int + err := json.Unmarshal(e.Value, &value) + if err != nil { + return nil, err + } + return Integer{value}, nil + case "s": + var value string + err := json.Unmarshal(e.Value, &value) + if err != nil { + return nil, err + } + return String{value}, nil + case "ta": + return Tail{}, nil + case "c": + return Cons{}, nil + case "u": + return Empty{}, nil + case "e": + return Extend{e.Label}, nil + case "g": + return Select{e.Label}, nil + case "o": + return Overwrite{e.Label}, nil + case "t": + return Tag{e.Label}, nil + case "m": + return Case{e.Label}, nil + case "n": + return NoCases{}, nil + case "p": + return Perform{e.Label}, nil + case "h": + return Handle{e.Label}, nil + case "b": + return Builtin{e.Label}, nil + } + return nil, fmt.Errorf("unknown node type %s", e.Key) +} diff --git a/fern/e2.go b/fern/e2.go new file mode 100644 index 000000000..b976f7201 --- /dev/null +++ b/fern/e2.go @@ -0,0 +1,64 @@ +package fern + +// context i.e. context free grammer +// situation, surroundings +// location +// site +// position as in tail position + +// ident path mode all needed, (block list)(sugar) also +// linear iteration a real pain for call + +// linear walk through level on a line depth = new line and reduce ident +// pass in *path if there render return cursor +// dont pass in cursor if we delete from offset position that indicates selecting the node move to selected and path +// if we delete use path for new cursor +// arrow keys or escape exit + +type situ struct { + indent int + nested bool + block bool + path []int +} + +// state with count, output, and mapping to info +// len(info = count) +// separate focus from highlight +// func (string_ String) render(situ situ, focus []int, output []rendered) ([]rendered, []string) { +// // make a new pusher with offset counter inside +// zero := 0 +// output = append(output, rendered{charachter: '"', offset: &zero}) +// // start := len(output) +// // if targeted(focus) { +// // } +// for i, ch := range string_.value { +// offset := i + 1 +// output = append(output, rendered{charachter: ch, offset: &offset}) +// } +// output = append(output, rendered{charachter: '"'}) +// if !situ.nested { +// output = append(output, rendered{charachter: '\n'}) +// } +// // start := len(output) + +// return nil, nil +// } + +// y = count newlines +// x = count back to newline + +func inner(focus []int, child int) []int { + // catches focus nil I believe + if len(focus) == 0 || focus[0] != child { + return nil + } + return focus[1:] +} + +func targeted(focus []int) bool { + if focus != nil || len(focus) == 0 { + return true + } + return false +} diff --git a/fern/editor.go b/fern/editor.go new file mode 100644 index 000000000..be1e2f6ac --- /dev/null +++ b/fern/editor.go @@ -0,0 +1,387 @@ +package fern + +import ( + "encoding/json" + "fmt" + + "github.com/gdamore/tcell/v2" +) + +// How do we handle typing or actions when the cursor is not on any element, +// This is because full grid allows motion of the tree that is not possible with tree navigation +// Make every point a no op by default + +// After edits how do we put cursor in the right place, Option 1 we don't + +// If we allow letters to be typed at the end of something how do we reference the square after particularly when it is blank + +// How do we do highlight functions if for example you backspace at the beginning of a line + +// Option 1 style of interaction +// In place text editing +// - needs rerender or panel on each charachter press +// Single Box in the middle + +// Option +// Explicit insert mode -> But you should end up in insert mode after inserting fn/binary etc +// typing ! for macros I don't want typo's i.e. lat! what does that do. + +// Option implementation +// Tree lookup with offset +// Reverse lookup where grid takes us to element + +// Panel of infinite charachter arrays or Wrapping + +// TODO lookup all text box features +// move cursor to beginning of element - maybe have a state that is at path +// TODO undo number match +// TODO page overflow +// Undo BG color +// Don't edit outside of the correct box +// Python tui thing https://github.com/Textualize/textual#about +// Don't wrap just go over the edge if content exists +// Have a map of path to positions on the grid + +// Draw needs to update cursor or return a grid of places +// How does one store the grid of places? +// Editing in place doesn't allow auto complete options etc +// Pulling up new window allows lensed edits +// i.e markdown syntax or something +// return grid position from render for cursor +// multi cursor returns lis +// view fragment with hash and pop upwards when expanding + +// {r: 12, g: 10, b: 10} calling i on this has a lense +// How do we plug this together +// Deploy > Develop +// Can all edit's including of binary be in that view + +// All +// arrows are a pain for collapsing + +// Panic comes from the writing to grid/g2 which is out of range and set content just fails quietly +// Rendering code to a panel that is bigger than the screen doesn't allow us to pre allocate the array +// But if we use hashes it's not a problem + +// CHOICE ========= +// edit in place OR does "i" bring up an edit +// screen allows things like color pickers, which are not so useful in TUI +// in place is less jumping + +// full typing idea was to use ! as a trigger for macro to do something. but this looks like VS Code style auto completes +// Shift enter for line above works in insert and command mode + +// What does small reactivity a.la Solid look like. -> This is valuable for lots of performant UI's + +// With my current style insert mode never start a new line unless multiline string +// can we edit just one line + +// call update fn on element +// Should we assume one letter to the right when typing or not +// type handle! + +// Pressing i on an element can create an in place box with a limit. But pressing i on a "(" after a var is going to be confusing + +// React style diff by caching call to render with offset, there is no matrix transformations to efficiently move one square +// cache can be list for each element we have a single point or x/y + +// Insert on Call can return correct child for each bracket + +// How does language server automcomplete + +// Right now anything calls a draw because text updates in place + +type mode string + +const ( + command mode = "command" + insert mode = "insert" +) + +func Draw(screen tcell.Screen, source Node, focus []int, mode mode) ([][][]int, [][]ref) { + w, h := screen.Size() + grid := make([][][]int, w) + for x := range grid { + grid[x] = make([][]int, h) + + } + g2 := make([][]ref, w) + for x := range g2 { + g2[x] = make([]ref, h) + + } + index := 0 + source.draw(screen, &Coordinate{}, focus, mode, &grid, []int{}, &g2, &index, 0, false, false) + return grid, g2 +} + +func New(s tcell.Screen, store Store) { + source := Source() + w, h := s.Size() + + if w == 0 || h == 0 { + return + } + + s.SetStyle(tcell.StyleDefault) + s.Clear() + + cursor := &Coordinate{} + + s.SetCursorStyle(tcell.CursorStyleDefault) + s.SetCursorStyle(tcell.CursorStyleSteadyBar) + s.ShowCursor(cursor.X, cursor.Y) + + raw, err := store.Load() + if err != nil { + fmt.Println(err.Error()) + return + } + + source, err = decode(raw) + if err != nil { + fmt.Println(err.Error()) + return + } + + var focus []int + mode := command + grid, g2 := Draw(s, source, focus, mode) + var yanked Node = Vacant{} + + quit := make(chan struct{}) + go func() { + for { + ev := s.PollEvent() + switch ev := ev.(type) { + case *tcell.EventKey: + switch ev.Key() { + + case tcell.KeyLeft: + cursor.X = Max(cursor.X-1, 0) + render(s, *cursor, w, h, grid, g2) + case tcell.KeyRight: + cursor.X = Min(cursor.X+1, w-1) + render(s, *cursor, w, h, grid, g2) + case tcell.KeyUp: + cursor.Y = Max(cursor.Y-1, 0) + render(s, *cursor, w, h, grid, g2) + case tcell.KeyDown: + cursor.Y = Min(cursor.Y+1, h-1) + render(s, *cursor, w, h, grid, g2) + case tcell.KeyRune: + if mode == command { + path := grid[cursor.X][cursor.Y] + target, c, err := zipper(source, path) + if err != nil { + fmt.Println(err.Error()) + } + changed := false + // path to focus on + // var focus []int + switch ev.Rune() { + case 'q': + data, err := json.Marshal(source) + if err != nil { + fmt.Println(err.Error()) + } + err = store.Save(data) + if err != nil { + fmt.Println(err.Error()) + } + case 'w': + source = c(Call{Vacant{""}, target}) + // focus = append(path, 0) + changed = true + case 'e': + source = c(Let{"", Vacant{""}, target}) + changed = true + case 'r': + if _, ok := target.(Vacant); ok { + source = c(Empty{}) + } else { + source = c(Call{Call{Extend{""}, Vacant{""}}, target}) + } + changed = true + case 't': + if _, ok := target.(Vacant); ok { + source = c(Tag{}) + } else { + source = c(Call{Tag{""}, target}) + } + changed = true + case 'y': + yanked = target + case 'Y': + source = c(yanked) + changed = true + // u -> don't really use and delete in labels might make more sense + case 'i': + // return update fn from path/node use in the saving state + // always needs to be string even for number + focus = grid[cursor.X][cursor.Y] + mode = insert + changed = true + case 'o': + // always have a target because the target should be a variable never a tail + source = c(Call{Call{Overwrite{""}, Vacant{""}}, target}) + // unchanged false can be part of default maybe as i keys don't change source + changed = true + case 'p': + // could always call if value given + source = c(Perform{}) + changed = true + case 'd': + source = c(Vacant{}) + changed = true + case 'f': + source = c(Fn{"", target}) + changed = true + case 'g': + if _, ok := target.(Vacant); ok { + source = c(Select{}) + } else { + source = c(Call{Select{""}, target}) + } + changed = true + case 'h': + source = c(Handle{}) + changed = true + case 'c': + source = c(Call{target, Vacant{}}) + changed = true + case 'x': + if _, ok := target.(Vacant); ok { + source = c(Empty{}) + } else { + source = c(Call{Call{Cons{}, Vacant{""}}, target}) + } + changed = true + case 'v': + source = c(Var{""}) + changed = true + case 'b': + source = c(String{" "}) + changed = true + case 'n': + source = c(Integer{0}) + changed = true + case 'm': + source = c(Call{Call{Case{}, Vacant{""}}, target}) + changed = true + case 'M': + source = c(NoCases{}) + changed = true + + } + if changed { + s.Clear() + grid, g2 = Draw(s, source, focus, mode) + render(s, *cursor, w, h, grid, g2) + } + } else { + + path := grid[cursor.X][cursor.Y] + offset := g2[cursor.X][cursor.Y].offset + if offset < 0 { + break + } + target, c, err := zipper(source, path) + if err != nil { + fmt.Println(err.Error()) + } + var new Node + switch t := target.(type) { + case Fn: + param := t.param[:offset] + string(ev.Rune()) + t.param[offset:] + new = Fn{param, t.body} + case Var: + label := t.label[:offset] + string(ev.Rune()) + t.label[offset:] + new = Var{label} + case Let: + label := t.label[:offset] + string(ev.Rune()) + t.label[offset:] + new = Let{label, t.value, t.then} + case Vacant: + new = Vacant{t.note + string(ev.Rune())} + case String: + value := t.value[:offset] + string(ev.Rune()) + t.value[offset:] + new = String{value} + case Extend: + label := t.label[:offset] + string(ev.Rune()) + t.label[offset:] + new = Extend{label} + case Select: + label := t.label[:offset] + string(ev.Rune()) + t.label[offset:] + new = Select{label} + case Overwrite: + label := t.label[:offset] + string(ev.Rune()) + t.label[offset:] + new = Overwrite{label} + case Perform: + label := t.label[:offset] + string(ev.Rune()) + t.label[offset:] + new = Perform{label} + case Handle: + label := t.label[:offset] + string(ev.Rune()) + t.label[offset:] + new = Handle{label} + default: + panic("not a node I expected") + } + source = c(new) + s.Clear() + cursor.X += 1 + grid, g2 = Draw(s, source, focus, mode) + render(s, *cursor, w, h, grid, g2) + } + case tcell.KeyEscape, tcell.KeyEnter, tcell.KeyCtrlC: + if mode == command { + close(quit) + return + } + mode = command + s.Clear() + grid, g2 = Draw(s, source, focus, mode) + render(s, *cursor, w, h, grid, g2) + case tcell.KeyCtrlL: + s.Sync() + default: + fmt.Printf("%#v\n", ev.Key()) + } + case *tcell.EventResize: + s.Sync() + } + } + }() + <-quit + s.Fini() +} + +func render(s tcell.Screen, cursor Coordinate, w, h int, grid [][][]int, g2 [][]ref) { + s.ShowCursor(cursor.X, cursor.Y) + for i := 0; i < w; i++ { + s.SetContent(i, h-1, ' ', nil, tcell.StyleDefault) + } + for i, ch := range fmt.Sprintf("%#v", grid[cursor.X][cursor.Y]) { + s.SetContent(i, h-1, ch, nil, tcell.StyleDefault) + } + for i := 0; i < w; i++ { + s.SetContent(i, h-2, ' ', nil, tcell.StyleDefault) + } + for i, ch := range fmt.Sprintf("%d", g2[cursor.X][cursor.Y]) { + s.SetContent(i, h-2, ch, nil, tcell.StyleDefault) + } + s.Show() +} + +// Max returns the larger of x or y. +func Max(x, y int) int { + if x < y { + return y + } + return x +} + +// Min returns the smaller of x or y. +func Min(x, y int) int { + if x > y { + return y + } + return x +} diff --git a/fern/editor2.go b/fern/editor2.go new file mode 100644 index 000000000..bf77e2176 --- /dev/null +++ b/fern/editor2.go @@ -0,0 +1,346 @@ +package fern + +import ( + "encoding/json" + "fmt" + + "github.com/gdamore/tcell/v2" +) + +// There should separate terms for all the following +// The fully rendered page of code +// A view into that page, possibly the editor +// The CLI application that controls the terminal - app + +func Run(s tcell.Screen, store Store) error { + w, h := s.Size() + + if w == 0 || h == 0 { + return fmt.Errorf("zero sized screen") + } + + s.EnableMouse() + s.SetStyle(tcell.StyleDefault) + + raw, err := store.Load() + if err != nil { + return err + } + + source, err := decode(raw) + if err != nil { + return err + } + + // editor will not always have code, app might be in loading state + editor := NewEditor(w, h-1, source) + message := "" + focus := editor.page.lookup[editor.position.X][editor.position.Y] + if focus != nil { + message = fmt.Sprintf("%s@%d", pathToString(focus.path), focus.offset) + } + clicking := false + for { + // Don't check error as zero value is fine + for i := 0; i < editor.size.X; i++ { + x := editor.shift.X + i + for j := 0; j < editor.size.Y; j++ { + y := editor.shift.Y + j + if x >= editor.page.size.X || y >= editor.page.size.Y { + s.SetContent(x, y, ' ', []rune{}, tcell.StyleDefault) + continue + } + r := editor.page.lookup[x][y] + if r == nil { + s.SetContent(i, j, ' ', []rune{}, tcell.StyleDefault) + continue + } + s.SetContent(i, j, r.character, []rune{}, r.style) + } + + } + + for i, ch := range message { + s.SetContent(i, editor.size.Y, ch, []rune{}, tcell.StyleDefault) + } + for i := len(message); i < editor.size.X; i++ { + s.SetContent(i, editor.size.Y, ' ', []rune{}, tcell.StyleDefault) + } + + s.ShowCursor( + editor.position.X-editor.shift.X, + editor.position.Y-editor.shift.Y, + ) + s.Show() + message = "" + ev := s.PollEvent() + switch ev := ev.(type) { + case *tcell.EventKey: + switch ev.Key() { + case tcell.KeyLeft: + editor.moveCursor(Coordinate{-1, 0}) + case tcell.KeyRight: + editor.moveCursor(Coordinate{1, 0}) + case tcell.KeyUp: + editor.moveCursor(Coordinate{0, -1}) + case tcell.KeyDown: + editor.moveCursor(Coordinate{0, 1}) + case tcell.KeyHome: + editor.moveCursor(Coordinate{-editor.size.X, 0}) + case tcell.KeyEnd: + editor.moveCursor(Coordinate{editor.size.X, 0}) + case tcell.KeyPgUp: + editor.moveCursor(Coordinate{0, -editor.size.Y}) + case tcell.KeyPgDn: + editor.moveCursor(Coordinate{0, editor.size.Y}) + case tcell.KeyRune: + editor.keyPress(ev.Rune()) + case tcell.KeyEnter: + editor.lineBelow() + // What is Backspace (not 2) + case tcell.KeyBackspace2: + editor.deleteCharachter() + case tcell.KeyCtrlW: + // Is there a transform primitive + editor.callWith() + case tcell.KeyCtrlE: + // Is there a transform primitive + editor.assignTo() + case tcell.KeyCtrlS: + data, err := json.Marshal(editor.source) + if err != nil { + panic(err.Error()) + } + err = store.Save(data) + if err != nil { + panic(err.Error()) + } + case tcell.KeyCtrlD: + editor.deleteTarget() + case tcell.KeyCtrlF: + editor.function() + case tcell.KeyCtrlL: + s.Sync() + case tcell.KeyEscape, tcell.KeyCtrlC: + return nil + default: + message = "sddsd" + } + if message == "" { + focus := editor.page.lookup[editor.position.X][editor.position.Y] + if focus != nil { + message = fmt.Sprintf("%s@%d", pathToString(focus.path), focus.offset) + } + } + case *tcell.EventMouse: + // Because I have to track the delta this is quite different to how a browser + // interaction model would look + // building custom but with shared Eyg components on occasion + current := ev.Buttons() == tcell.Button1 + if !clicking && current { + x, y := ev.Position() + editor.setCursor(x, y) + } + clicking = current + } + } +} + +type editor struct { + size Coordinate + shift Coordinate + // position in page of code + position Coordinate + source Node + // cache + page page + info map[string]int +} + +// TODO block actions by going to start of line + +func NewEditor(w, h int, source Node) editor { + size := Coordinate{w, h} + + e := editor{size: size} + e.updateSource(source, []int{}, 0) + return e +} + +func (e *editor) updateSource(source Node, focus []int, offset int) { + rendered, info := Print(source) + page := NewPage(rendered) + start := info[pathToString(focus)] + cursor := page.coordinates[start+offset] + e.position = cursor + e.source = source + e.page = page + e.info = info + e.updateShift() +} + +func (e *editor) moveCursor(step Coordinate) { + x := e.position.X + step.X + y := e.position.Y + step.Y + e.setCursor(x, y) +} + +func (e *editor) setCursor(x, y int) { + e.position.X = Min(Max(0, x), e.page.size.X-1) + e.position.Y = Min(Max(0, y), e.page.size.Y-1) + + e.updateShift() +} + +func (e *editor) updateShift() { + if overflow := e.position.X - (e.size.X + e.shift.X) + 1; overflow > 0 { + e.shift.X += overflow + } + if overflow := e.position.X - e.shift.X; overflow < 0 { + e.shift.X += overflow + } + if overflow := e.position.Y - (e.size.Y + e.shift.Y) + 1; overflow > 0 { + e.shift.Y += overflow + } + if overflow := e.position.Y - e.shift.Y; overflow < 0 { + e.shift.Y += overflow + } +} + +func (e *editor) keyPress(ch rune) { + r := e.page.lookup[e.position.X][e.position.Y] + if r == nil { + return + } + target, build, err := zipper(e.source, r.path) + if err != nil { + panic("erorr making the zipper") + } + + new, subPath, offset := target.keyPress(ch, r.offset) + s := build(new) + e.updateSource(s, append(r.path, subPath...), offset) + // Should be safe as movement limited to bounding box +} + +func (e *editor) lineBelow() { + var found rendered + for i := 0; i < 100; i++ { + r := e.page.lookup[i][e.position.Y] + if r != nil && r.path != nil { + found = *r + break + } + } + target, build, err := zipper(e.source, found.path) + if err != nil { + panic("erorr making the zipper") + } + switch t := target.(type) { + case Let: + new := Let{t.label, t.value, Let{"", Vacant{}, t.then}} + s := build(new) + e.updateSource(s, append(found.path, 1), 0) + default: + new := Let{"", target, Vacant{}} + s := build(new) + e.updateSource(s, append(found.path, 1), 0) + } +} + +func (e *editor) deleteCharachter() { + r := e.page.lookup[e.position.X][e.position.Y] + if r == nil { + return + } + target, build, err := zipper(e.source, r.path) + if err != nil { + panic("erorr making the zipper") + } + + new, subPath, offset := target.deleteCharachter(r.offset) + s := build(new) + e.updateSource(s, append(r.path, subPath...), offset) +} + +func (e *editor) callWith() { + r := e.page.lookup[e.position.X][e.position.Y] + if r == nil { + return + } + target, build, err := zipper(e.source, r.path) + if err != nil { + panic("erorr making the zipper") + } + + switch t := target.(type) { + case Let: + new := Let{t.label, Call{Vacant{}, t.value}, t.then} + s := build(new) + e.updateSource(s, r.path, 0) + default: + new := Call{Vacant{}, t} + s := build(new) + e.updateSource(s, r.path, 0) + } +} + +func (e *editor) assignTo() { + r := e.page.lookup[e.position.X][e.position.Y] + if r == nil { + return + } + target, build, err := zipper(e.source, r.path) + if err != nil { + panic("erorr making the zipper") + } + new := Let{"", target, Vacant{}} + s := build(new) + e.updateSource(s, r.path, 0) +} + +func (e *editor) deleteTarget() { + r := e.page.lookup[e.position.X][e.position.Y] + if r == nil { + return + } + target, build, err := zipper(e.source, r.path) + if err != nil { + panic("erorr making the zipper") + } + + switch t := target.(type) { + case Let: + new := t.then + s := build(new) + e.updateSource(s, r.path, 0) + default: + new := Vacant{} + s := build(new) + e.updateSource(s, r.path, 0) + } +} + +func (e *editor) function() { + r := e.page.lookup[e.position.X][e.position.Y] + if r == nil { + return + } + target, build, err := zipper(e.source, r.path) + if err != nil { + panic("erorr making the zipper") + } + + switch t := target.(type) { + case Let: + new := Let{t.label, Fn{"", t.value}, t.then} + s := build(new) + e.updateSource(s, append(r.path, 0), 0) + default: + new := Fn{"", t} + s := build(new) + e.updateSource(s, r.path, 0) + } +} + +// Does TAB alway move to next linear index diff --git a/fern/encode.go b/fern/encode.go new file mode 100644 index 000000000..adece785d --- /dev/null +++ b/fern/encode.go @@ -0,0 +1,141 @@ +package fern + +import "encoding/json" + +func (fn Fn) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "f", + "l": fn.param, + "b": fn.body, + }) +} + +func (call Call) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + // a -> apply + "0": "a", + "f": call.fn, + "a": call.arg, + }) +} + +func (var_ Var) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "v", + "l": var_.label, + }) +} + +func (let Let) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "l", + "l": let.label, + "v": let.value, + "t": let.then, + }) +} + +// CSV.Yaml file defining grammer of encoding, but will probably end up as binary +func (vacant Vacant) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + // z -> zero + "0": "z", + // comment + "c": vacant.note, + }) +} + +func (integer Integer) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "i", + "v": integer.value, + }) +} + +func (str String) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "s", + "v": str.value, + }) +} + +func (tail Tail) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "ta", + }) +} + +func (cons Cons) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "c", + }) +} + +func (Empty) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + // u -> unit + "0": "u", + }) +} + +func (extend Extend) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "e", + "l": extend.label, + }) +} +func (select_ Select) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + // g -> get + "0": "g", + "l": select_.label, + }) +} +func (overwrite Overwrite) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "o", + "l": overwrite.label, + }) +} + +func (tag Tag) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "t", + "l": tag.label, + }) +} + +func (case_ Case) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + // m -> match + "0": "m", + "l": case_.label, + }) +} + +func (NoCases) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "n", + }) +} + +func (perform Perform) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "p", + "l": perform.label, + }) +} + +func (handle Handle) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "h", + "l": handle.label, + }) +} + +func (builtin Builtin) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "0": "v", + "l": builtin.label, + }) +} diff --git a/fern/encode_test.go b/fern/encode_test.go new file mode 100644 index 000000000..ab32c82bc --- /dev/null +++ b/fern/encode_test.go @@ -0,0 +1,22 @@ +package fern + +// import ( +// "encoding/json" +// "fmt" +// "testing" +// ) + +// func Test_encoding(t *testing.T) { +// bytes, err := json.Marshal(Fn{"x", String{"bob"}}) +// if err != nil { +// t.Fatal(err) +// } +// fmt.Println(string(bytes)) +// node, err := decode(bytes) +// if err != nil { +// t.Fatal(err) +// } + +// fmt.Printf("%#v\n", node) +// // panic("s") +// } diff --git a/fern/go.mod b/fern/go.mod new file mode 100644 index 000000000..41793ea34 --- /dev/null +++ b/fern/go.mod @@ -0,0 +1,20 @@ +module github.com/midas-framework/project_wisdom/fern + +go 1.20 + +require github.com/gdamore/tcell/v2 v2.6.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.3 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.2 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fern/go.sum b/fern/go.sum new file mode 100644 index 000000000..51919bfec --- /dev/null +++ b/fern/go.sum @@ -0,0 +1,57 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= +github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/fern/json.go b/fern/json.go new file mode 100644 index 000000000..2ef0d2413 --- /dev/null +++ b/fern/json.go @@ -0,0 +1 @@ +package fern diff --git a/fern/node.go b/fern/node.go new file mode 100644 index 000000000..1355610d5 --- /dev/null +++ b/fern/node.go @@ -0,0 +1,622 @@ +package fern + +import ( + "fmt" + "reflect" + + "github.com/gdamore/tcell/v2" +) + +// TODO editing last charachter in string +const red = 0xff0000 +const pink = 0xffb2ef +const purple = 0xff00ee +const green = 0x00ff00 + +type ref struct { + index int + offset int +} + +type Node interface { + // could return list of strings + draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) + child(int) (Node, func(Node) Node, error) + print(buffer *[]rendered, info map[string]int, situ situ) + // used by call + contentLength() int + keyPress(ch rune, offset int) (Node, []int, int) + deleteCharachter(offset int) (Node, []int, int) + MarshalJSON() ([]byte, error) +} + +type Fn struct { + param string + body Node +} + +var _ Node = Fn{} + +// is there a way to make this all on the grid for lookup +// Is there a node then continuation version of this draw +func (fn Fn) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + content := fn.param + style := tcell.StyleDefault + if content == "" { + content = "_" + style = style.Foreground(tcell.NewHexColor(pink)) + } + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, content, writer, focus, mode, grid, path, g2, self, style, true) + WriteString(s, " -> ", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + fn.body.draw(s, writer, focus, mode, grid, append(path, 0), g2, index, indent, true, false) +} + +func (fn Fn) child(c int) (Node, func(Node) Node, error) { + if c == 0 { + return fn.body, func(n Node) Node { return Fn{fn.param, n} }, nil + } + return Var{}, nil, fmt.Errorf("invalid child id for fn %d", c) +} + +func (fn Fn) contentLength() int { + return len(fn.param) +} + +type Call struct { + fn Node + arg Node +} + +var _ Node = Call{} + +// Only the brackets are the actual call node +func (call Call) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + switch inner := call.fn.(type) { + case Call: + switch t := inner.fn.(type) { + case Cons: + if !list { + WriteString(s, "[", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } + // second call + *index++ + // cons + *index++ + next := *index + inner.arg.draw(s, writer, focus, mode, grid, append(path, 0, 1), g2, index, indent, true, false) + // label block as true? + // turning x into tail makes no difference in cursor for , and value + // , comma points to first in pair of applies + // NO BECAUSE it's nested to 0,1 for value but commas are the apply thing + // making one element list special case that doesn't show up often + // child could draw comma is it's self at this point + // Render comma in the print list view. + WriteString(s, ", ", writer, focus, mode, grid, append(path, 1), g2, next, tcell.StyleDefault, false) + call.arg.draw(s, writer, focus, mode, grid, append(path, 1), g2, index, indent, true, true) + if !list { + WriteString(s, "]", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } + // Everything is a block (record, case, etc) cursor should keep track of last block + // Certain key commands should add in block + return + case Extend: + if !list { + WriteString(s, "{", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } + // second call + *index++ + // key + content := t.label + style := tcell.StyleDefault + if content == "" { + content = "_" + style = style.Foreground(tcell.NewHexColor(pink)) + } + // TODO reuse path to block type node + if reflect.DeepEqual(append(path, 0, 0), focus) { + style = style.Reverse(true) + } + WriteString(s, content, writer, focus, mode, grid, append(path, 0, 0), g2, *index, style, true) + WriteString(s, ": ", writer, focus, mode, grid, append(path, 0, 0), g2, *index, tcell.StyleDefault, false) + *index++ + next := *index + inner.arg.draw(s, writer, focus, mode, grid, append(path, 0, 1), g2, index, indent, true, false) + // label block as true? + // turning x into tail makes no difference in cursor for , and value + // , comma points to first in pair of applies + // NO BECAUSE it's nested to 0,1 for value but commas are the apply thing + // making one element list special case that doesn't show up often + // child could draw comma is it's self at this point + // Render comma in the print list view. + WriteString(s, ", ", writer, focus, mode, grid, append(path, 1), g2, next, tcell.StyleDefault, false) + call.arg.draw(s, writer, focus, mode, grid, append(path, 1), g2, index, indent, true, true) + if !list { + WriteString(s, "}", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } + return + // case Overwrite: + // case Case: + } + } + call.fn.draw(s, writer, focus, mode, grid, append(path, 0), g2, index, indent, true, false) + WriteString(s, "(", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + call.arg.draw(s, writer, focus, mode, grid, append(path, 1), g2, index, indent, true, false) + WriteString(s, ")", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) +} + +func (call Call) child(c int) (Node, func(Node) Node, error) { + if c == 0 { + return call.fn, func(n Node) Node { return Call{n, call.arg} }, nil + } + if c == 1 { + return call.arg, func(n Node) Node { return Call{call.fn, n} }, nil + } + return Var{}, nil, fmt.Errorf("invalid child id for call %d", c) +} + +func (Call) contentLength() int { + return -1 +} + +type Var struct { + label string +} + +var _ Node = Var{} + +func (var_ Var) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + if list { + WriteString(s, "..", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } + content := var_.label + style := tcell.StyleDefault + if content == "" { + content = "_" + style = style.Foreground(tcell.NewHexColor(pink)) + } + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, content, writer, focus, mode, grid, path, g2, self, style, true) +} +func (var_ Var) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Var %d", c) +} + +func (node Var) contentLength() int { + return len(node.label) +} + +type Let struct { + label string + value Node + then Node +} + +var _ Node = Let{} + +func (let Let) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + // ++ only once a node + if list { + WriteString(s, "..", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } + if block { + WriteString(s, "{", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + indent += 2 + writer.Y += 1 + writer.X = indent + defer func() { + writer.Y += 1 + writer.X = indent - 2 + WriteString(s, "}", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + }() + } + WriteString(s, "let ", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault.Dim(true), false) + content := let.label + style := tcell.StyleDefault + if content == "" { + content = "_" + style = style.Foreground(tcell.NewHexColor(pink)) + } + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, content, writer, focus, mode, grid, path, g2, self, style, true) + + WriteString(s, " = ", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + let.value.draw(s, writer, focus, mode, grid, append(path, 0), g2, index, indent, true, false) + writer.Y += 1 + writer.X = indent + let.then.draw(s, writer, focus, mode, grid, append(path, 1), g2, index, indent, false, false) +} + +func (let Let) child(c int) (Node, func(Node) Node, error) { + if c == 0 { + return let.value, func(n Node) Node { return Let{let.label, n, let.then} }, nil + } + if c == 1 { + return let.then, func(n Node) Node { return Let{let.label, let.value, n} }, nil + } + return Var{}, nil, fmt.Errorf("invalid child id for Let %d", c) +} +func (node Let) contentLength() int { + return len(node.label) +} + +type Vacant struct { + note string +} + +var _ Node = Vacant{} + +func (v Vacant) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + content := v.note + if content == "" { + content = "todo" + } + style := tcell.StyleDefault.Foreground(tcell.NewHexColor(red)) + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, content, writer, focus, mode, grid, path, g2, self, style, false) +} + +func (Vacant) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Vacant %d", c) +} + +func (node Vacant) contentLength() int { + return len(node.note) +} + +type Integer struct { + value int +} + +var _ Node = Integer{} + +func (i Integer) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + WriteString(s, fmt.Sprintf("%d", i.value), writer, focus, mode, grid, path, g2, self, tcell.StyleDefault.Foreground(tcell.NewHexColor(purple)), false) +} + +func (Integer) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Integer %d", c) +} +func (node Integer) contentLength() int { + return len(fmt.Sprintf("%d", node.value)) +} + +type String struct { + value string +} + +var _ Node = String{} + +func (str String) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + style := tcell.StyleDefault.Foreground(tcell.NewHexColor(green)) + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, "\"", writer, focus, mode, grid, path, g2, self, style, false) + WriteString(s, str.value, writer, focus, mode, grid, path, g2, self, style, true) + WriteString(s, "\"", writer, focus, mode, grid, path, g2, self, style, false) + +} + +func (String) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for String %d", c) +} + +func (node String) contentLength() int { + return len(node.value) +} + +type Tail struct { +} + +var _ Node = Tail{} + +func (Tail) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + if !list { + WriteString(s, "[]", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } +} + +func (Tail) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Tail %d", c) +} + +func (node Tail) contentLength() int { + return 0 +} + +type Cons struct { +} + +var _ Node = Cons{} + +func (Cons) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + WriteString(s, "cons", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) +} + +func (Cons) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Tail %d", c) +} + +func (node Cons) contentLength() int { + return 0 +} + +type Empty struct { +} + +var _ Node = Empty{} + +func (Empty) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + // can draw commas in here + if !list { + WriteString(s, "{}", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } +} + +func (Empty) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Empty %d", c) +} + +func (node Empty) contentLength() int { + return 0 +} + +type Extend struct { + label string +} + +var _ Node = Extend{} + +func (e Extend) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + WriteString(s, fmt.Sprintf("+%s", e.label), writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) +} + +func (Extend) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Extend %d", c) +} + +func (node Extend) contentLength() int { + return len(node.label) +} + +type Select struct { + label string +} + +var _ Node = Select{} + +func (e Select) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + content := e.label + style := tcell.StyleDefault + if content == "" { + content = "_" + style = style.Foreground(tcell.NewHexColor(pink)) + } + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, ".", writer, focus, mode, grid, path, g2, self, style, false) + WriteString(s, content, writer, focus, mode, grid, path, g2, self, style, true) +} + +func (Select) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Select %d", c) +} + +func (node Select) contentLength() int { + return len(node.label) +} + +type Overwrite struct { + label string +} + +var _ Node = Overwrite{} + +func (e Overwrite) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + content := e.label + style := tcell.StyleDefault + if content == "" { + content = "_" + style = style.Foreground(tcell.NewHexColor(pink)) + } + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, ":", writer, focus, mode, grid, path, g2, self, style, false) + WriteString(s, content, writer, focus, mode, grid, path, g2, self, style, true) + +} + +func (Overwrite) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Overwrite %d", c) +} + +func (node Overwrite) contentLength() int { + return len(node.label) +} + +type Tag struct { + label string +} + +var _ Node = Tag{} + +func (t Tag) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + label := t.label + if label == "" { + WriteString(s, "_", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault.Foreground(red).Dim(true), false) + } else { + WriteString(s, label, writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) + } +} + +func (Tag) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Tag %d", c) +} + +func (node Tag) contentLength() int { + return len(node.label) +} + +type Case struct { + label string +} + +var _ Node = Case{} + +func (e Case) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + WriteString(s, fmt.Sprintf("+%s", e.label), writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) +} + +func (Case) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Case %d", c) +} + +func (node Case) contentLength() int { + return len(node.label) +} + +type NoCases struct { +} + +var _ Node = NoCases{} + +func (e NoCases) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + WriteString(s, "nocases", writer, focus, mode, grid, path, g2, self, tcell.StyleDefault.Dim(true), false) +} + +func (NoCases) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for NoCases %d", c) +} + +func (node NoCases) contentLength() int { + return -1 +} + +type Perform struct { + label string +} + +var _ Node = Perform{} + +func (e Perform) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + content := e.label + style := tcell.StyleDefault + if content == "" { + content = "_" + style = style.Foreground(tcell.NewHexColor(pink)) + } + if reflect.DeepEqual(path, focus) { + style = style.Reverse(true) + } + WriteString(s, "perform ", writer, focus, mode, grid, path, g2, self, style.Dim(true), false) + WriteString(s, content, writer, focus, mode, grid, path, g2, self, style, true) +} + +func (Perform) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for perform %d", c) +} + +func (node Perform) contentLength() int { + return len(node.label) +} + +type Handle struct { + label string +} + +var _ Node = Handle{} + +func (e Handle) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + WriteString(s, fmt.Sprintf("handle %s", e.label), writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) +} + +func (Handle) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Handle %d", c) +} + +func (node Handle) contentLength() int { + return len(node.label) +} + +type Builtin struct { + label string +} + +var _ Node = Builtin{} + +func (e Builtin) draw(s tcell.Screen, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, index *int, indent int, block bool, list bool) { + self := *index + *index++ + WriteString(s, fmt.Sprintf("handle %s", e.label), writer, focus, mode, grid, path, g2, self, tcell.StyleDefault, false) +} + +func (Builtin) child(c int) (Node, func(Node) Node, error) { + return Var{}, nil, fmt.Errorf("invalid child id for Builtin %d", c) +} + +func (node Builtin) contentLength() int { + return len(node.label) +} + +// If editable and focus, mode simply add a space to the end +// TODO have a mode bounding box Method that limits motion when editing label +// Having a 2d grid to link path and index means we keep a lookup only for relevant nodes + +func WriteString(s tcell.Screen, content string, writer *Coordinate, focus []int, mode mode, grid *[][][]int, path []int, g2 *[][]ref, id int, style tcell.Style, editable bool) { + for offset, ch := range content { + s.SetContent(writer.X, writer.Y, ch, nil, style) + (*grid)[writer.X][writer.Y] = path + if !editable { + offset = -1 + } + (*g2)[writer.X][writer.Y] = ref{id, offset} + writer.X++ + } +} diff --git a/fern/page.go b/fern/page.go new file mode 100644 index 000000000..0cf7b1305 --- /dev/null +++ b/fern/page.go @@ -0,0 +1,56 @@ +package fern + +// Source code is rendered to a page. +// A page is stored linearly + +type page struct { + buffer []rendered + coordinates []Coordinate + size Coordinate + lookup [][]*rendered +} + +func NewPage(buffer []rendered) page { + // Need to include current charachter i.e. index + 1 + // always bump x so initial conditions are -1 for x + coordinates := make([]Coordinate, len(buffer)) + + x := -1 + y := 0 + newline := false + + maxX := 0 + maxY := 0 + for i, r := range buffer { + if newline { + x = 0 + y += 1 + newline = false + } else { + x += 1 + } + if r.character == '\n' { + newline = true + } + coordinates[i] = Coordinate{x, y} + maxX = Max(maxX, x) + maxY = Max(maxY, y) + } + size := Coordinate{} + if maxX != 0 { + size.X = maxX + 1 + size.Y = maxY + 1 + } + // lookup goes from grid to rendered + lookup := make([][]*rendered, size.X) + for x := range lookup { + lookup[x] = make([]*rendered, size.Y) + } + for i, r := range buffer { + // something silly with go references + r := r + c := coordinates[i] + lookup[c.X][c.Y] = &r + } + return page{buffer, coordinates, size, lookup} +} diff --git a/fern/page_test.go b/fern/page_test.go new file mode 100644 index 000000000..f472f7570 --- /dev/null +++ b/fern/page_test.go @@ -0,0 +1,44 @@ +package fern + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIndexToCoordinate(t *testing.T) { + rendered := []rendered{ + {character: 'a'}, + {character: 'b'}, + {character: '\n'}, + {character: 'c'}, + {character: '\n'}, + {character: '\n'}, + {character: 'd'}, + {character: 'e'}, + {character: '\n'}, + } + + // screen 2x 2 + tests := []Coordinate{ + {0, 0}, + {1, 0}, + {2, 0}, + {0, 1}, + {1, 1}, + {0, 2}, + {0, 3}, + {1, 3}, + {2, 3}, + } + assert.Equal(t, len(rendered), len(tests)) + page := NewPage(rendered) + assert.Equal(t, 3, page.size.X) + assert.Equal(t, 4, page.size.Y) + for i, want := range tests { + got := page.coordinates[i] + assert.Equal(t, want, got) + + assert.Equal(t, rendered[i].character, page.lookup[got.X][got.Y].character) + } +} diff --git a/fern/path.go b/fern/path.go new file mode 100644 index 000000000..ab6d0b74d --- /dev/null +++ b/fern/path.go @@ -0,0 +1,17 @@ +package fern + +import "fmt" + +func pathToString(path []int) string { + if path == nil { + return "nil" + } + out := "[" + for i, p := range path { + if i != 0 { + out += "," + } + out += fmt.Sprintf("%d", p) + } + return out + "]" +} diff --git a/fern/print.go b/fern/print.go new file mode 100644 index 000000000..b238cbd72 --- /dev/null +++ b/fern/print.go @@ -0,0 +1,821 @@ +package fern + +import ( + "fmt" + "strconv" + "unicode" + + "github.com/gdamore/tcell/v2" +) + +const blue3 = 0x87ceeb +const green4 = 0x7fbc8c +const yellow2 = 0xffdb58 +const orange4 = 0xff6b6b +const pink3 = 0xffb2ef +const purple4 = 0x9723c9 + +var keywordStyle = tcell.StyleDefault.Dim(true) +var missingStyle = tcell.StyleDefault.Foreground(tcell.NewHexColor(pink3)) +var todoStyle = tcell.StyleDefault.Foreground(tcell.NewHexColor(orange4)).Bold(true) +var intStyle = tcell.StyleDefault.Foreground(tcell.NewHexColor(purple4)) +var stringStyle = tcell.StyleDefault.Foreground(tcell.NewHexColor(green4)) +var unionStyle = tcell.StyleDefault.Foreground(tcell.NewHexColor(blue3)) +var effectStyle = tcell.StyleDefault.Foreground(tcell.NewHexColor(yellow2)) + +// view exhibit rendered +// scene or panel page is the list of rendered +type rendered struct { + character rune + path []int + offset int + style tcell.Style +} + +func (node Fn) print(buffer *[]rendered, info map[string]int, s situ) { + printLabel(node.param, buffer, info, s, tcell.StyleDefault) + *buffer = append(*buffer, rendered{' ', s.path, len(node.param), keywordStyle}) + *buffer = append(*buffer, rendered{'-', s.path, -1, keywordStyle}) + *buffer = append(*buffer, rendered{'>', s.path, -1, keywordStyle}) + *buffer = append(*buffer, rendered{' ', s.path, -1, keywordStyle}) + node.body.print(buffer, info, situ{s.indent, s.nested, true, append(s.path, 0)}) +} + +// TODO call passing into function into number etc + +// Each node can have it's own interpretation of what -1 means but this is a public iterface for other nodes like call +// Keeping print and keypress together because of offsets but maybe not needed +func (node Fn) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + if unicode.IsLetter(ch) || unicode.IsDigit(ch) { + param := insertRune(node.param, offset, ch) + return Fn{param, node.body}, []int{}, offset + 1 + } + return node, []int{}, 0 +} + +func (node Fn) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + param, offset := backspaceAt(node.param, offset) + return Fn{param, node.body}, []int{}, offset +} + +func printIndent(buffer *[]rendered, indent int) { + for i := 0; i < indent; i++ { + *buffer = append(*buffer, rendered{' ', nil, -1, tcell.StyleDefault}) + } +} + +func insertRune(s string, at int, new rune) string { + return s[:at] + string(new) + s[at:] +} + +func backspaceAt(s string, at int) (string, int) { + if at < 1 { + return s, 0 + } + return s[:at-1] + s[at:], at - 1 +} + +func printTail(node Node, buffer *[]rendered, info map[string]int, path []int, indent int, nested bool, start int) { + switch t := node.(type) { + case Call: + // TODO wrap up this switching to a fn on call + if inner, ok := t.fn.(Call); ok { + if _, ok := inner.fn.(Cons); ok { + start := len(*buffer) + *buffer = append(*buffer, rendered{',', path, 0, keywordStyle}) + *buffer = append(*buffer, rendered{' ', path, 1, keywordStyle}) + inner.arg.print(buffer, info, situ{indent, true, true, append(path, 0, 1)}) + printTail(t.arg, buffer, info, path, indent, nested, start) + } + } + return + case Tail: + offset := len(*buffer) - start + *buffer = append(*buffer, rendered{']', path, offset, keywordStyle}) + if !nested { + *buffer = append(*buffer, rendered{'\n', path, offset + 1, keywordStyle}) + } + return + } + start2 := len(*buffer) + // Pressing comma on this makes a list in the tail position which is what we want + // there is no choice between at element or tail position because it is not yet a lets itself. + *buffer = append(*buffer, rendered{',', path, 0, keywordStyle}) + *buffer = append(*buffer, rendered{' ', path, 1, keywordStyle}) + *buffer = append(*buffer, rendered{'.', path, 2, keywordStyle}) + *buffer = append(*buffer, rendered{'.', path, 3, keywordStyle}) + node.print(buffer, info, situ{indent, true, true, path}) + offset := len(*buffer) - start2 + *buffer = append(*buffer, rendered{']', path, offset, keywordStyle}) + if !nested { + *buffer = append(*buffer, rendered{'\n', path, offset + 1, keywordStyle}) + } +} + +func printExtension(node Node, buffer *[]rendered, info map[string]int, path []int, indent int, nested bool, start int) { + switch t := node.(type) { + case Call: + // TODO wrap up this switching to a fn on call + if inner, ok := t.fn.(Call); ok { + if group, ok := inner.fn.(Extend); ok { + start := len(*buffer) + *buffer = append(*buffer, rendered{',', path, 0, keywordStyle}) + *buffer = append(*buffer, rendered{' ', path, 1, keywordStyle}) + printLabel(group.label, buffer, info, situ{indent, false, false, append(path, 0, 0)}, tcell.StyleDefault) + // same comma logic here + *buffer = append(*buffer, rendered{':', path, 0, keywordStyle}) + *buffer = append(*buffer, rendered{' ', path, 1, keywordStyle}) + + inner.arg.print(buffer, info, situ{indent, true, true, append(path, 0, 1)}) + printExtension(t.arg, buffer, info, path, indent, nested, start) + } + } + return + case Empty: + offset := len(*buffer) - start + *buffer = append(*buffer, rendered{'}', path, offset, keywordStyle}) + if !nested { + *buffer = append(*buffer, rendered{'\n', path, offset + 1, keywordStyle}) + } + return + } + // TODO do we make non record tails invlid + // start2 := len(*buffer) + // // Pressing comma on this makes a list in the tail position which is what we want + // // there is no choice between at element or tail position because it is not yet a lets itself. + // *buffer = append(*buffer, rendered{',', path, 0, keywordStyle}) + // *buffer = append(*buffer, rendered{' ', path, 1, keywordStyle}) + // *buffer = append(*buffer, rendered{'.', path, 2, keywordStyle}) + // *buffer = append(*buffer, rendered{'.', path, 3, keywordStyle}) + // node.print(buffer, info, situ{indent, true, true, path}) + // offset := len(*buffer) - start2 + // *buffer = append(*buffer, rendered{']', path, offset, keywordStyle}) + // if !nested { + // *buffer = append(*buffer, rendered{'\n', path, offset + 1, keywordStyle}) + // } + +} + +func printBranch(node Node, buffer *[]rendered, info map[string]int, path []int, indent int, nested bool) { + switch t := node.(type) { + case Call: + // parent handles indent same as in let + printIndent(buffer, indent) + // TODO wrap up this switching to a fn on call + if inner, ok := t.fn.(Call); ok { + if case_, ok := inner.fn.(Case); ok { + printLabel(case_.label, buffer, info, situ{indent, false, false, append(path, 0, 0)}, unionStyle) + *buffer = append(*buffer, rendered{' ', append(path, 0, 0), len(case_.label), unionStyle}) + inner.arg.print(buffer, info, situ{indent, false, true, append(path, 0, 1)}) + printBranch(t.arg, buffer, info, append(path, 1), indent, nested) + return + } + } + case NoCases: + + // original indent + printIndent(buffer, indent-2) + *buffer = append(*buffer, rendered{'}', path, 0, keywordStyle}) + if !nested { + *buffer = append(*buffer, rendered{'\n', nil, -1, keywordStyle}) + } + return + } + node.print(buffer, info, situ{indent, false, true, path}) + // original indent + printIndent(buffer, indent-2) + *buffer = append(*buffer, rendered{'}', nil, -1, keywordStyle}) + if !nested { + *buffer = append(*buffer, rendered{'\n', nil, -1, keywordStyle}) + } +} + +func (node Call) print(buffer *[]rendered, info map[string]int, s situ) { + // TODO switches not if - maybe not if is list is a fn on call + if t, ok := node.fn.(Select); ok { + node.arg.print(buffer, info, situ{s.indent, true, true, append(s.path, 1)}) + *buffer = append(*buffer, rendered{'.', s.path, 0, keywordStyle}) + + printLabel(t.label, buffer, info, situ{s.indent, false, false, append(s.path, 0)}, tcell.StyleDefault) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', append(s.path, 0), len(t.label), keywordStyle}) + } + return + } + if inner, ok := node.fn.(Call); ok { + switch t := inner.fn.(type) { + case Cons: + start := len(*buffer) + *buffer = append(*buffer, rendered{'[', s.path, 0, keywordStyle}) + inner.arg.print(buffer, info, situ{s.indent, true, true, append(s.path, 0, 1)}) + printTail(node.arg, buffer, info, append(s.path, 1), s.indent, s.nested, start) + return + case Extend: + // first round of printing is outside print extend because it doesn't need ", " + start := len(*buffer) + *buffer = append(*buffer, rendered{'{', s.path, 0, keywordStyle}) + printLabel(t.label, buffer, info, situ{s.indent, false, false, append(s.path, 0, 0)}, tcell.StyleDefault) + // comma doesn't work on expand + *buffer = append(*buffer, rendered{':', s.path, 0, keywordStyle}) + *buffer = append(*buffer, rendered{' ', s.path, 0, keywordStyle}) + inner.arg.print(buffer, info, situ{s.indent, true, true, append(s.path, 0, 1)}) + printExtension(node.arg, buffer, info, append(s.path, 1), s.indent, s.nested, start) + return + case Case: + printNotNode("match {", buffer, s) + indent := s.indent + 2 + *buffer = append(*buffer, rendered{'\n', nil, -1, keywordStyle}) + // original indent + printIndent(buffer, indent) + printLabel(t.label, buffer, info, situ{indent, false, false, append(s.path, 0, 0)}, unionStyle) + *buffer = append(*buffer, rendered{' ', append(s.path, 0, 0), len(t.label), unionStyle}) + + inner.arg.print(buffer, info, situ{indent, false, true, append(s.path, 0, 1)}) + + printBranch(node.arg, buffer, info, append(s.path, 1), indent, s.nested) + + return + } + } + + node.fn.print(buffer, info, situ{s.indent, true, true, append(s.path, 0)}) + start := len(*buffer) + info[pathToString(s.path)] = start + + *buffer = append(*buffer, rendered{'(', s.path, 0, keywordStyle}) + node.arg.print(buffer, info, situ{s.indent, true, true, append(s.path, 1)}) + offset := len(*buffer) - start + *buffer = append(*buffer, rendered{')', s.path, offset, keywordStyle}) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, offset + 1, keywordStyle}) + } +} + +func (node Call) keyPress(ch rune, offset int) (Node, []int, int) { + // TODO keypress on inner elements + if offset == 0 { + inner, _, _ := node.fn.keyPress(ch, node.fn.contentLength()) + return Call{inner, node.arg}, []int{}, 0 + } + inner, _, _ := node.arg.keyPress(ch, node.arg.contentLength()) + return Call{node.fn, inner}, []int{}, offset + 1 + // return node, []int{}, offset +} + +func (node Call) deleteCharachter(offset int) (Node, []int, int) { + // TODO keypress on inner elements + return node, []int{}, offset +} + +func (node Var) print(buffer *[]rendered, info map[string]int, s situ) { + printLabel(node.label, buffer, info, s, tcell.StyleDefault) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), keywordStyle}) + } +} + +func (node Var) keyPress(ch rune, offset int) (Node, []int, int) { + + if offset == -1 { + return node, []int{}, 0 + } + if unicode.IsLetter(ch) || unicode.IsDigit(ch) { + label := insertRune(node.label, offset, ch) + return Var{label}, []int{}, offset + 1 + } + if ch == '(' && offset == node.contentLength() { + return Call{node, Vacant{}}, []int{1}, 0 + } + if ch == '.' && offset == node.contentLength() { + return Call{Select{}, node}, []int{0}, 0 + } + if ch == '=' && offset == node.contentLength() { + // Sort of the same as control e + return Let{node.label, Vacant{}, Vacant{}}, []int{0}, 0 + } + + return node, []int{}, offset +} + +func (node Var) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + if label == "" { + return Vacant{}, []int{}, offset + } + return Var{label}, []int{}, offset +} + +// TODO take only path in args +func printLabel(label string, buffer *[]rendered, info map[string]int, s situ, style tcell.Style) { + info[pathToString(s.path)] = len(*buffer) + if label == "" { + label = "_" + style = missingStyle + } + for i, ch := range label { + *buffer = append(*buffer, rendered{ch, s.path, i, style}) + } +} + +// ALso only needs the path +func printNotNode(content string, buffer *[]rendered, s situ) { + for _, ch := range content { + *buffer = append(*buffer, rendered{ch, s.path, -1, keywordStyle}) + } +} + +func (node Let) print(buffer *[]rendered, info map[string]int, s situ) { + indent := s.indent + if s.block { + indent += 2 + *buffer = append(*buffer, rendered{'{', nil, -1, keywordStyle}) + *buffer = append(*buffer, rendered{'\n', nil, -1, keywordStyle}) + // original indent + printIndent(buffer, indent) + + defer func() { + // needs original depth indent + printIndent(buffer, s.indent) + *buffer = append(*buffer, rendered{'}', nil, -1, keywordStyle}) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', nil, -1, keywordStyle}) + } + }() + } + printNotNode("let ", buffer, s) + printLabel(node.label, buffer, info, s, tcell.StyleDefault) + *buffer = append(*buffer, rendered{' ', s.path, len(node.label), keywordStyle}) + *buffer = append(*buffer, rendered{'=', s.path, -1, keywordStyle}) + *buffer = append(*buffer, rendered{' ', s.path, -1, keywordStyle}) + node.value.print(buffer, info, situ{indent, false, true, append(s.path, 0)}) + // nested /false prints a new line + printIndent(buffer, indent) + node.then.print(buffer, info, situ{indent, false, false, append(s.path, 1)}) +} + +func (node Let) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, offset + } + label := insertRune(node.label, offset, ch) + return Let{label, node.value, node.then}, []int{}, offset + 1 +} + +func (node Let) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Let{label, node.value, node.then}, []int{}, offset +} + +func (node Vacant) print(buffer *[]rendered, info map[string]int, s situ) { + info[pathToString(s.path)] = len(*buffer) + content := node.note + if content == "" { + content = "todo" + } + for i, ch := range content { + *buffer = append(*buffer, rendered{ch, s.path, i, todoStyle}) + } + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(content), keywordStyle}) + } +} + +func (node Vacant) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + // TODO use same actions in center of list + // . starts a tail + switch ch { + case '"': + return String{}, []int{}, 0 + case '[': + return Tail{}, []int{}, 0 + case '{': + return Empty{}, []int{}, 0 + case '=': + return Let{"", Vacant{}, Vacant{}}, []int{}, 0 + // Could be done with perform typed in + // auto suggestion if starting with p/perform + case '|': + // is this a good character to have representing perform + // what would handle be + // TODO path into case + return Call{Call{Case{}, Fn{"", Vacant{}}}, Vacant{}}, []int{0, 0}, 0 + + case '^': + // is this a good character to have representing perform + // what would handle be + return Perform{}, []int{}, 0 + } + if digit, ok := runeToDigit(ch); ok { + return Integer{digit}, []int{}, 1 + } + if unicode.IsLetter(ch) && unicode.IsLower(ch) { + return Var{string(ch)}, []int{}, 1 + } + if unicode.IsLetter(ch) { + return Tag{string(ch)}, []int{}, 1 + } + return node, []int{}, 0 +} + +func (node Vacant) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + note, offset := backspaceAt(node.note, offset) + return Vacant{note}, []int{}, offset +} + +func (node Integer) print(buffer *[]rendered, info map[string]int, s situ) { + info[pathToString(s.path)] = len(*buffer) + content := fmt.Sprintf("%d", node.value) + for i, ch := range content { + *buffer = append(*buffer, rendered{ch, s.path, i, intStyle}) + } + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(content), keywordStyle}) + } +} + +func (node Integer) keyPress(ch rune, offset int) (Node, []int, int) { + // offset needs to be real negative number so tha position can be kept + if offset == -1 { + return node, []int{}, 0 + } + if _, ok := runeToDigit(ch); ok { + i64, err := strconv.ParseInt(insertRune(fmt.Sprintf("%d", node.value), offset, ch), 10, 64) + if err != nil { + // TODO log error + return node, []int{}, 0 + } + return Integer{int(i64)}, []int{}, offset + 1 + } + if ch == '-' && offset == 0 && node.value > 0 { + return Integer{-node.value}, []int{}, 1 + } + return node, []int{}, offset +} + +func (node Integer) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + value, offset := backspaceAt(fmt.Sprintf("%d", node.value), offset) + if value == "" { + return Vacant{}, []int{}, 0 + } + i64, err := strconv.ParseInt(value, 10, 64) + if err != nil { + // TODO log error + return node, []int{}, 0 + } + return Integer{int(i64)}, []int{}, offset +} + +func runeToDigit(ch rune) (int, bool) { + if digit := ch - '0'; digit >= 0 && digit < 10 { + return int(digit), true + } + return 0, false +} + +// Does this need to be *buffer +func (node String) print(buffer *[]rendered, info map[string]int, s situ) { + *buffer = append(*buffer, rendered{'"', s.path, -1, stringStyle}) + // start of active, maybe origin is a better name + info[pathToString(s.path)] = len(*buffer) + for i, ch := range node.value { + *buffer = append(*buffer, rendered{ch, s.path, i, stringStyle}) + } + *buffer = append(*buffer, rendered{'"', s.path, len(node.value), stringStyle}) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, -1, keywordStyle}) + } +} + +func (node String) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + value := insertRune(node.value, offset, ch) + return String{value}, []int{}, offset + 1 +} + +func (node String) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + if offset == 0 && node.value == "" { + return Vacant{}, []int{}, offset + } + value, offset := backspaceAt(node.value, offset) + return String{value}, []int{}, offset +} + +func (node Tail) print(buffer *[]rendered, info map[string]int, s situ) { + *buffer = append(*buffer, rendered{'[', s.path, -1, keywordStyle}) + info[pathToString(s.path)] = len(*buffer) + *buffer = append(*buffer, rendered{']', s.path, 0, keywordStyle}) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, -1, keywordStyle}) + } +} + +func (node Tail) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + switch ch { + case ',': + return Call{Call{Cons{}, Vacant{}}, Tail{}}, []int{0, 1}, 0 + } + return node, []int{}, 0 +} + +func (node Tail) deleteCharachter(offset int) (Node, []int, int) { + return node, []int{}, 0 +} + +func (node Cons) print(buffer *[]rendered, info map[string]int, s situ) { + printNotNode("cons", buffer, s) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, -1, keywordStyle}) + } +} + +func (node Cons) keyPress(ch rune, offset int) (Node, []int, int) { + return node, []int{}, 0 +} + +func (node Cons) deleteCharachter(offset int) (Node, []int, int) { + return node, []int{}, 0 +} + +func (node Empty) print(buffer *[]rendered, info map[string]int, s situ) { + *buffer = append(*buffer, rendered{'{', s.path, -1, keywordStyle}) + info[pathToString(s.path)] = len(*buffer) + *buffer = append(*buffer, rendered{'}', s.path, 0, keywordStyle}) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, -1, keywordStyle}) + } +} + +func (node Empty) keyPress(ch rune, offset int) (Node, []int, int) { + return node, []int{}, 0 +} + +func (node Empty) deleteCharachter(offset int) (Node, []int, int) { + return node, []int{}, 0 +} + +func (node Extend) print(buffer *[]rendered, info map[string]int, s situ) { + printNotNode("+", buffer, s) + printLabel(node.label, buffer, info, s, unionStyle) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), keywordStyle}) + } +} + +func (node Extend) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, offset + } + label := insertRune(node.label, offset, ch) + return Extend{label}, []int{}, offset + 1 +} + +func (node Extend) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Extend{label}, []int{}, offset +} + +func (node Select) print(buffer *[]rendered, info map[string]int, s situ) { + printNotNode(".", buffer, s) + printLabel(node.label, buffer, info, s, tcell.StyleDefault) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), keywordStyle}) + } +} + +func (node Select) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, offset + } + label := insertRune(node.label, offset, ch) + return Select{label}, []int{}, offset + 1 +} + +func (node Select) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Select{label}, []int{}, offset +} + +func (node Overwrite) print(buffer *[]rendered, info map[string]int, s situ) { + // is : better + printNotNode("=", buffer, s) + printLabel(node.label, buffer, info, s, unionStyle) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), keywordStyle}) + } +} + +func (node Overwrite) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, offset + } + label := insertRune(node.label, offset, ch) + return Overwrite{label}, []int{}, offset + 1 +} + +func (node Overwrite) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Overwrite{label}, []int{}, offset +} + +func (node Tag) print(buffer *[]rendered, info map[string]int, s situ) { + printLabel(node.label, buffer, info, s, unionStyle) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), tcell.StyleDefault}) + } +} + +func (node Tag) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, offset + } + label := insertRune(node.label, offset, ch) + return Tag{label}, []int{}, offset + 1 +} + +func (node Tag) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Tag{label}, []int{}, offset +} + +func (node Case) print(buffer *[]rendered, info map[string]int, s situ) { + printNotNode("case ", buffer, s) +} + +func (node Case) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, offset + } + label := insertRune(node.label, offset, ch) + return Case{label}, []int{}, offset + 1 +} + +func (node Case) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Case{label}, []int{}, offset +} + +func (node NoCases) print(buffer *[]rendered, info map[string]int, s situ) { + // Should not node be Not label + printNotNode("-----", buffer, s) +} + +func (node NoCases) keyPress(ch rune, offset int) (Node, []int, int) { + return node, []int{}, 0 +} + +func (node NoCases) deleteCharachter(offset int) (Node, []int, int) { + return node, []int{}, 0 +} + +func (node Perform) print(buffer *[]rendered, info map[string]int, s situ) { + printNotNode("perform ", buffer, s) + printLabel(node.label, buffer, info, s, effectStyle) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), keywordStyle}) + } +} + +func (node Perform) keyPress(ch rune, offset int) (Node, []int, int) { + return labelKeyPress(node.label, ch, offset, func(s string) Node { return Perform{s} }) +} + +// This might only be perform and handle +// let never has label on the end +// select has ordering +func labelKeyPress(label string, ch rune, offset int, build func(string) Node) (Node, []int, int) { + if offset == -1 { + return build(label), []int{}, offset + } + if unicode.IsLetter(ch) || unicode.IsDigit(ch) { + node := build(insertRune(label, offset, ch)) + return node, []int{}, offset + 1 + } + if ch == '(' && offset == len(label) { + return Call{build(label), Vacant{}}, []int{1}, 0 + } + return build(label), []int{}, offset +} + +func (node Perform) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Perform{label}, []int{}, offset +} + +func (node Handle) print(buffer *[]rendered, info map[string]int, s situ) { + printNotNode("handle ", buffer, s) + printLabel(node.label, buffer, info, s, effectStyle) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), keywordStyle}) + } +} + +func (node Handle) keyPress(ch rune, offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, offset + } + label := insertRune(node.label, offset, ch) + return Handle{label}, []int{}, offset + 1 +} + +func (node Handle) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + return Handle{label}, []int{}, offset +} + +func (node Builtin) print(buffer *[]rendered, info map[string]int, s situ) { + printLabel(node.label, buffer, info, s, tcell.StyleDefault) + if !s.nested { + *buffer = append(*buffer, rendered{'\n', s.path, len(node.label), keywordStyle}) + } +} + +func (node Builtin) keyPress(ch rune, offset int) (Node, []int, int) { + + if offset == -1 { + return node, []int{}, 0 + } + if unicode.IsLetter(ch) || unicode.IsDigit(ch) { + label := insertRune(node.label, offset, ch) + return Builtin{label}, []int{}, offset + 1 + } + if ch == '(' && offset == node.contentLength() { + return Call{node, Vacant{}}, []int{1}, 0 + } + if ch == '.' && offset == node.contentLength() { + return Call{Select{}, node}, []int{0}, 0 + } + if ch == '=' && offset == node.contentLength() { + // Sort of the same as control e + return Let{node.label, Vacant{}, Vacant{}}, []int{0}, 0 + } + + return node, []int{}, offset +} + +func (node Builtin) deleteCharachter(offset int) (Node, []int, int) { + if offset == -1 { + return node, []int{}, 0 + } + label, offset := backspaceAt(node.label, offset) + if label == "" { + return Vacant{}, []int{}, offset + } + return Builtin{label}, []int{}, offset +} + +func Print(node Node) ([]rendered, map[string]int) { + buffer := []rendered{} + info := make(map[string]int) + node.print(&buffer, info, situ{path: []int{}}) + return buffer, info +} diff --git a/fern/print_test.go b/fern/print_test.go new file mode 100644 index 000000000..8b7debb6f --- /dev/null +++ b/fern/print_test.go @@ -0,0 +1,346 @@ +package fern + +import ( + "fmt" + "testing" + + "github.com/gdamore/tcell/v2" + "github.com/stretchr/testify/assert" +) + +func TestPrinting(t *testing.T) { + tests := []struct { + source Node + buffer []rendered + info map[string]int + }{ + { + Var{"x"}, + []rendered{ + {'x', []int{}, 0, tcell.StyleDefault}, + {'\n', []int{}, 1, tcell.StyleDefault}, + }, + map[string]int{"[]": 0}, + }, + { + Vacant{""}, + []rendered{ + {'t', []int{}, 0, tcell.StyleDefault}, + {'o', []int{}, 1, tcell.StyleDefault}, + {'d', []int{}, 2, tcell.StyleDefault}, + {'o', []int{}, 3, tcell.StyleDefault}, + {'\n', []int{}, 4, tcell.StyleDefault}, + }, + map[string]int{"[]": 0}, + }, + { + String{"hey"}, + []rendered{ + {'"', []int{}, -1, tcell.StyleDefault}, + {'h', []int{}, 0, tcell.StyleDefault}, + {'e', []int{}, 1, tcell.StyleDefault}, + {'y', []int{}, 2, tcell.StyleDefault}, + {'"', []int{}, 3, tcell.StyleDefault}, + {'\n', []int{}, -1, tcell.StyleDefault}, + }, + map[string]int{"[]": 1}, + }, + { + Integer{10}, + []rendered{ + {'1', []int{}, 0, tcell.StyleDefault}, + {'0', []int{}, 1, tcell.StyleDefault}, + {'\n', []int{}, 2, tcell.StyleDefault}, + }, + map[string]int{"[]": 0}, + }, + { + Tail{}, + []rendered{ + {'[', []int{}, -1, tcell.StyleDefault}, + {']', []int{}, 0, tcell.StyleDefault}, + {'\n', []int{}, -1, tcell.StyleDefault}, + }, + map[string]int{"[]": 1}, + }, + { + Select{"name"}, + []rendered{ + {'.', []int{}, -1, tcell.StyleDefault}, + {'n', []int{}, 0, tcell.StyleDefault}, + {'a', []int{}, 1, tcell.StyleDefault}, + {'m', []int{}, 2, tcell.StyleDefault}, + {'e', []int{}, 3, tcell.StyleDefault}, + {'\n', []int{}, 4, tcell.StyleDefault}, + }, + map[string]int{"[]": 1}, + }, + { + Perform{"Log"}, + []rendered{ + {'p', []int{}, -1, tcell.StyleDefault}, + {'e', []int{}, -1, tcell.StyleDefault}, + {'r', []int{}, -1, tcell.StyleDefault}, + {'f', []int{}, -1, tcell.StyleDefault}, + {'o', []int{}, -1, tcell.StyleDefault}, + {'r', []int{}, -1, tcell.StyleDefault}, + {'m', []int{}, -1, tcell.StyleDefault}, + {' ', []int{}, -1, tcell.StyleDefault}, + {'L', []int{}, 0, tcell.StyleDefault}, + {'o', []int{}, 1, tcell.StyleDefault}, + {'g', []int{}, 2, tcell.StyleDefault}, + {'\n', []int{}, 3, tcell.StyleDefault}, + }, + map[string]int{"[]": 8}, + }, + // Fn test + { + Call{Var{"x"}, String{""}}, + []rendered{ + {'x', []int{0}, 0, tcell.StyleDefault}, + {'(', []int{}, 0, tcell.StyleDefault}, + {'"', []int{1}, -1, tcell.StyleDefault}, + {'"', []int{1}, 0, tcell.StyleDefault}, + {')', []int{}, 3, tcell.StyleDefault}, + {'\n', []int{}, 4, tcell.StyleDefault}, + }, + map[string]int{"[]": 1, "[0]": 0, "[1]": 3}, + }, + { + Call{Perform{"Log"}, Integer{5}}, + []rendered{ + {'p', []int{0}, -1, tcell.StyleDefault}, + {'e', []int{0}, -1, tcell.StyleDefault}, + {'r', []int{0}, -1, tcell.StyleDefault}, + {'f', []int{0}, -1, tcell.StyleDefault}, + {'o', []int{0}, -1, tcell.StyleDefault}, + {'r', []int{0}, -1, tcell.StyleDefault}, + {'m', []int{0}, -1, tcell.StyleDefault}, + {' ', []int{0}, -1, tcell.StyleDefault}, + {'L', []int{0}, 0, tcell.StyleDefault}, + {'o', []int{0}, 1, tcell.StyleDefault}, + {'g', []int{0}, 2, tcell.StyleDefault}, + {'(', []int{}, 0, tcell.StyleDefault}, + {'5', []int{1}, 0, tcell.StyleDefault}, + {')', []int{}, 2, tcell.StyleDefault}, + {'\n', []int{}, 3, tcell.StyleDefault}, + }, + map[string]int{"[]": 11, "[0]": 8, "[1]": 12}, + }, + { + Call{Tag{"Ok"}, Var{"x"}}, + []rendered{ + {'O', []int{0}, 0, tcell.StyleDefault}, + {'k', []int{0}, 1, tcell.StyleDefault}, + {'(', []int{}, 0, tcell.StyleDefault}, + {'x', []int{1}, 0, tcell.StyleDefault}, + {')', []int{}, 2, tcell.StyleDefault}, + {'\n', []int{}, 3, tcell.StyleDefault}, + }, + map[string]int{"[]": 2, "[0]": 0, "[1]": 3}, + }, + { + Let{"a", Integer{1}, Let{"b", Integer{2}, Integer{3}}}, + []rendered{ + {'l', []int{}, -1, tcell.StyleDefault}, + {'e', []int{}, -1, tcell.StyleDefault}, + {'t', []int{}, -1, tcell.StyleDefault}, + {' ', []int{}, -1, tcell.StyleDefault}, + {'a', []int{}, 0, tcell.StyleDefault}, + {' ', []int{}, 1, tcell.StyleDefault}, + {'=', []int{}, -1, tcell.StyleDefault}, + {' ', []int{}, -1, tcell.StyleDefault}, + {'1', []int{0}, 0, tcell.StyleDefault}, + {'\n', []int{0}, 1, tcell.StyleDefault}, + {'l', []int{1}, -1, tcell.StyleDefault}, + {'e', []int{1}, -1, tcell.StyleDefault}, + {'t', []int{1}, -1, tcell.StyleDefault}, + {' ', []int{1}, -1, tcell.StyleDefault}, + {'b', []int{1}, 0, tcell.StyleDefault}, + {' ', []int{1}, 1, tcell.StyleDefault}, + {'=', []int{1}, -1, tcell.StyleDefault}, + {' ', []int{1}, -1, tcell.StyleDefault}, + {'2', []int{1, 0}, 0, tcell.StyleDefault}, + {'\n', []int{1, 0}, 1, tcell.StyleDefault}, + {'3', []int{1, 1}, 0, tcell.StyleDefault}, + {'\n', []int{1, 1}, 1, tcell.StyleDefault}, + }, + map[string]int{"[]": 4, "[0]": 8, "[1]": 14, "[1,0]": 18, "[1,1]": 20}, + }, + { + Let{"a", Let{"b", Integer{1}, Integer{2}}, Integer{3}}, + []rendered{ + {'l', []int{}, -1, tcell.StyleDefault}, + {'e', []int{}, -1, tcell.StyleDefault}, + {'t', []int{}, -1, tcell.StyleDefault}, + {' ', []int{}, -1, tcell.StyleDefault}, + {'a', []int{}, 0, tcell.StyleDefault}, + {' ', []int{}, 1, tcell.StyleDefault}, + {'=', []int{}, -1, tcell.StyleDefault}, + {' ', []int{}, -1, tcell.StyleDefault}, + {'{', nil, -1, tcell.StyleDefault}, + {'\n', nil, -1, tcell.StyleDefault}, + {' ', nil, -1, tcell.StyleDefault}, + {' ', nil, -1, tcell.StyleDefault}, + {'l', []int{0}, -1, tcell.StyleDefault}, + {'e', []int{0}, -1, tcell.StyleDefault}, + {'t', []int{0}, -1, tcell.StyleDefault}, + {' ', []int{0}, -1, tcell.StyleDefault}, + {'b', []int{0}, 0, tcell.StyleDefault}, + {' ', []int{0}, 1, tcell.StyleDefault}, + {'=', []int{0}, -1, tcell.StyleDefault}, + {' ', []int{0}, -1, tcell.StyleDefault}, + {'1', []int{0, 0}, 0, tcell.StyleDefault}, + {'\n', []int{0, 0}, 1, tcell.StyleDefault}, + {' ', nil, -1, tcell.StyleDefault}, + {' ', nil, -1, tcell.StyleDefault}, + {'2', []int{0, 1}, 0, tcell.StyleDefault}, + {'\n', []int{0, 1}, 1, tcell.StyleDefault}, + {'}', nil, -1, tcell.StyleDefault}, + {'\n', nil, -1, tcell.StyleDefault}, + {'3', []int{1}, 0, tcell.StyleDefault}, + {'\n', []int{1}, 1, tcell.StyleDefault}, + }, + map[string]int{"[]": 4, "[0]": 16, "[0,0]": 20, "[0,1]": 24, "[1]": 28}, + }, + // Sugar + { + // buld comma where + // [1, ] + // [1, ..x] + // , needs to be on call because may be block + // [{ .. }, 2] + // only top element and tail clickable + // press comma on brackets can't go into string because quotes separate but numbers can extend number + // [x] comma on tail -> [x, hole] + // [] comma on tail -> [hole] + // [a, b] comma on comma -> [a, hole, b] where what your on becomes is the call stack + Call{Call{Cons{}, Integer{1}}, Call{Call{Cons{}, Integer{2}}, Tail{}}}, + []rendered{ + {'[', []int{}, 0, tcell.StyleDefault}, + {'1', []int{0, 1}, 0, tcell.StyleDefault}, + {',', []int{1}, 0, tcell.StyleDefault}, + {' ', []int{1}, 1, tcell.StyleDefault}, + // Nothing is zero widith so can check if at tail with offset > 2 + // can start with '[' or ', ' + {'2', []int{1, 0, 1}, 0, tcell.StyleDefault}, // comma on number can make a list because that would be list in list + // make this one because try and not go up the list for edits + {']', []int{1}, 3, tcell.StyleDefault}, // or should this be the call above with offset for inse, tcell.StyleDefaultrt + {'\n', []int{1}, 4, tcell.StyleDefault}, + }, + map[string]int{"[0,1]": 1, "[1,0,1]": 4}, + }, + { + Call{Var{"x"}, Tail{}}, + []rendered{ + {'x', []int{0}, 0, tcell.StyleDefault}, + {'(', []int{}, 0, tcell.StyleDefault}, + {'[', []int{1}, -1, tcell.StyleDefault}, + {']', []int{1}, 0, tcell.StyleDefault}, + {')', []int{}, 3, tcell.StyleDefault}, + {'\n', []int{}, 4, tcell.StyleDefault}, + }, + map[string]int{"[]": 1, "[0]": 0, "[1]": 3}, + }, + // { + // Call{Call{Cons{}, Var{"x"}}, Var{"y"}}, + // []rendered{ + // {'[', []int{}, 0, tcell.StyleDefault}, + // {'x', []int{1, 0}, 0, tcell.StyleDefault}, + // {',', []int{1}, -1, tcell.StyleDefault}, + // {' ', []int{1}, 0, tcell.StyleDefault}, + // {'.', []int{1}, 3, tcell.StyleDefault}, + // {'.', []int{1}, 3, tcell.StyleDefault}, + // // TODO Doesn't work with offset in var an in tail at the same time + // // should call always be -1 it's a text thing after all + // // Only need is differationation between before and after + // {'y', []int{1}, 3, tcell.StyleDefault}, + // {']', []int{1}, 3, tcell.StyleDefault}, + // {'\n', []int{1}, 4, tcell.StyleDefault}, + // }, + // map[string]int{"[]": 1, "[0]": 0, "[1]": 3}, + // }, + { + Call{Select{"a"}, Var{"x"}}, + []rendered{ + {'x', []int{1}, 0, tcell.StyleDefault}, + // Don't have any reference to call node but this is ok - Nope + // Needs to be ref to call node because arg might be a block or other + {'.', []int{}, 0, tcell.StyleDefault}, + {'a', []int{0}, 0, tcell.StyleDefault}, + {'\n', []int{0}, 1, tcell.StyleDefault}, + }, + map[string]int{"[1]": 0, "[0]": 2}, + }, + // { + // Call{Call{Extend{"a"}, Integer{1}}, Call{Call{Extend{"b"}, Integer{2}}, Empty{}}}, + // []rendered{ + // {'{', []int{}, 0, tcell.StyleDefault}, + // {'a', []int{0, 0}, 0, tcell.StyleDefault}, + // {':', []int{0, 0}, 1, tcell.StyleDefault}, + // {' ', []int{}, 0, tcell.StyleDefault}, + // {'1', []int{0, 1}, 0, tcell.StyleDefault}, + // {',', []int{1}, 0, tcell.StyleDefault}, + // {' ', []int{1}, 1, tcell.StyleDefault}, + // {'b', []int{}, 0, tcell.StyleDefault}, + // {':', []int{}, 0, tcell.StyleDefault}, + // {' ', []int{}, 0, tcell.StyleDefault}, + // {'2', []int{1, 0, 1}, 0}, // comma on number can make a list because that would be list in list + // // make this one because try and not go up the list for edits + // {'}', []int{1}, 3}, // or should this be the call above with offset for inse, tcell.StyleDefaultrt + // {'\n', []int{1}, 4, tcell.StyleDefault}, + // }, + // map[string]int{"[0,1]": 1, "[1,0,1]": 4}, + // }, + // TODO case statement + } + + for _, tt := range tests { + rendered, info := Print(tt.source) + + assertRendered(t, tt.buffer, rendered) + assert.Equal(t, tt.info, info) + } +} + +func assertRendered(t *testing.T, expected, actual []rendered) { + expectedText := renderedText(expected) + actualText := renderedText(actual) + if expectedText != actualText { + t.Logf("Not equal:\nexpected: %s\nactual: %s", expectedText, actualText) + t.Fail() + } + + expectedPaths := renderedPaths(expected) + actualPaths := renderedPaths(actual) + assert.Equal(t, expectedPaths, actualPaths) + + expectedOffsets := renderedOffsets(expected) + actualOffsets := renderedOffsets(actual) + assert.Equal(t, expectedOffsets, actualOffsets) +} + +func renderedText(buffer []rendered) string { + out := "" + for _, r := range buffer { + out += string(r.character) + } + // Shows newline charachters + return fmt.Sprintf("%#v", out) +} + +func renderedPaths(buffer []rendered) []string { + var out []string + for _, r := range buffer { + out = append(out, pathToString(r.path)) + } + return out +} + +func renderedOffsets(buffer []rendered) []int { + var out []int + for _, r := range buffer { + out = append(out, r.offset) + } + return out +} diff --git a/fern/saved.json b/fern/saved.json new file mode 100644 index 000000000..8da721a7b --- /dev/null +++ b/fern/saved.json @@ -0,0 +1 @@ +{"0":"a","a":{"0":"l","l":"x","t":{"0":"z","c":""},"v":{"0":"z","c":""}},"f":{"0":"v","l":"z"}} \ No newline at end of file diff --git a/fern/source.go b/fern/source.go new file mode 100644 index 000000000..8f1900a52 --- /dev/null +++ b/fern/source.go @@ -0,0 +1,14 @@ +package fern + +func Source() Node { + // return Let{"", Let{"a", Fn{"x", String{"hello world!"}}, Call{Call{Var{"x"}, Integer{5}}, Integer{5}}}, Var{"x"}} + // return Call{Var{"x"}, Var{"y"}} + // return Call{Call{Cons{}, Integer{5}}, Call{Call{Cons{}, Integer{61}}, Let{"x", Integer{5}, Tail{}}}} + return Call{Call{Extend{"foo"}, Integer{10}}, Empty{}} +} + +// If there is no increase selection then no getting stuck on not real values +// [a, b, c, |] +// makes thing go to tail if on brackets as top call +// [a, ] +// ^ -> [hole, a, ] diff --git a/fern/store.go b/fern/store.go new file mode 100644 index 000000000..fe4f7980d --- /dev/null +++ b/fern/store.go @@ -0,0 +1,6 @@ +package fern + +type Store interface { + Load() ([]byte, error) + Save([]byte) error +} diff --git a/grass/go.mod b/grass/go.mod new file mode 100644 index 000000000..db9f994a1 --- /dev/null +++ b/grass/go.mod @@ -0,0 +1,3 @@ +module grass + +go 1.20 diff --git a/grass/main.go b/grass/main.go new file mode 100644 index 000000000..a6fe653ee --- /dev/null +++ b/grass/main.go @@ -0,0 +1,211 @@ +package main + +import ( + "fmt" + "strings" +) + +// Need persistent datastructures for env + +func main() { + var source Expression + fmt.Println("") + source = String{value: "hello"} + result := interpretWithStd(source) + fmt.Printf("%#v\n", result) + + source = Let{"x", source, Var{"x"}} + result = interpretWithStd(source) + fmt.Printf("%#v\n", result) + + source = Apply{Var{"uppercase"}, Apply{Fn{"x", Var{"x"}}, String{value: "blue"}}} + + result = interpretWithStd(source) + fmt.Printf("%#v\n", result) + + source = Apply{Apply{Cons{}, String{value: "item"}}, Tail{}} + + result = interpretWithStd(source) + fmt.Printf("%#v\n", result) + + // let server request -> dom -> dom.alert(request.method) + // let bar = foo(x) + // let sourceCode = serialize(bar) + // astToJSON(sourceCode) + // Don't AST but capture runtime. but how do I do this with hash reference + // let framework(f) { + // let client = f(request) + // serialize(client) + // } + +} + +// List AST n = 1 and expression n > 1 is apply n = 0 unit/tail + +// func step(exp Expression, env Env) { +// switch t := exp.(type) { +// case Let: +// a := t.label +// a +// } +// } + +type Value interface { + Call(Value, K) Cont +} + +type defaultValue struct { +} + +func (defaultValue) Call(Value, K) Cont { + panic("not a function") +} + +type Env = map[string]Value + +type K = func(Value) Cont + +type Cont struct { + value Value + k func(Value) Cont +} + +func interpretWithStd(e Expression) Value { + env := map[string]Value{ + "uppercase": Uppercase{}, + } + return interpret(e, env) +} + +func interpret(e Expression, env Env) Value { + var c Cont = e.Step(env, nil) + for { + if c.k == nil { + break + } + c = c.k(c.value) + } + return c.value +} + +type Expression interface { + Step(Env, K) Cont +} + +type Fn struct { + param string + body Expression +} + +func (fn Fn) Step(env Env, k K) Cont { + return Cont{value: RunFn{fn, env}, k: k} +} + +type RunFn struct { + fn Fn + env Env +} + +func (runFn RunFn) Call(arg Value, k K) Cont { + // TODO proper copy + env := make(map[string]Value, 0) + for k, v := range runFn.env { + env[k] = v + } + env[runFn.fn.param] = arg + return runFn.fn.body.Step(env, k) + // return Env +} + +// Actual builtins +type Uppercase struct { +} + +func (builtin Uppercase) Call(arg Value, k K) Cont { + t, ok := arg.(String) + if !ok { + panic("bad arg to 'uppercase'") + } + term := String{value: strings.ToUpper(t.value)} + return Cont{value: term, k: k} +} + +type Apply struct { + fn Expression + arg Expression +} + +func (a Apply) Step(env Env, k K) Cont { + return a.fn.Step(env, func(fn Value) Cont { + return a.arg.Step(env, func(arg Value) Cont { + return fn.Call(arg, k) + }) + }) +} + +type Let struct { + label string + value Expression + then Expression +} + +func (l Let) Step(env Env, k K) Cont { + return l.value.Step(env, func(v Value) Cont { + env[l.label] = v + return l.then.Step(env, k) + }) +} + +type Var struct { + label string +} + +func (v Var) Step(env Env, k K) Cont { + value, ok := env[v.label] + if !ok { + panic("bad value") + } + return Cont{value: value, k: k} +} + +type String struct { + defaultValue + value string +} + +func (s String) Step(_ Env, k K) Cont { + return Cont{value: s, k: k} +} + +type Tail struct { + defaultValue +} + +func (t Tail) Step(_ Env, k K) Cont { + return Cont{value: t, k: k} +} + +type Cons struct { + defaultValue + item Value + rest Value +} + +func (c Cons) Step(_ Env, k K) Cont { + return Cont{value: c, k: k} +} + +func (c Cons) Call(v Value, k K) Cont { + // Check nilness + if c.item == nil { + // TODO need to copy + c.item = v + return Cont{c, k} + } + if c.rest == nil { + // TODO need to copy + c.rest = v + return Cont{c, k} + } + return c.defaultValue.Call(v, k) +}