src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/builtins.go (about) 1 package edit 2 3 import ( 4 "errors" 5 6 "src.elv.sh/pkg/cli" 7 "src.elv.sh/pkg/cli/modes" 8 "src.elv.sh/pkg/cli/term" 9 "src.elv.sh/pkg/cli/tk" 10 "src.elv.sh/pkg/eval" 11 "src.elv.sh/pkg/eval/errs" 12 "src.elv.sh/pkg/eval/vals" 13 "src.elv.sh/pkg/parse" 14 "src.elv.sh/pkg/parse/parseutil" 15 "src.elv.sh/pkg/ui" 16 ) 17 18 func closeMode(app cli.App) { 19 app.PopAddon() 20 } 21 22 func endOfHistory(app cli.App) { 23 app.Notify(ui.T("End of history")) 24 } 25 26 type redrawOpts struct{ Full bool } 27 28 func (redrawOpts) SetDefaultOptions() {} 29 30 func redraw(app cli.App, opts redrawOpts) { 31 if opts.Full { 32 app.RedrawFull() 33 } else { 34 app.Redraw() 35 } 36 } 37 38 func clear(app cli.App, tty cli.TTY) { 39 tty.HideCursor() 40 tty.ClearScreen() 41 app.RedrawFull() 42 tty.ShowCursor() 43 } 44 45 func insertRaw(app cli.App, tty cli.TTY) { 46 codeArea, ok := focusedCodeArea(app) 47 if !ok { 48 return 49 } 50 tty.SetRawInput(1) 51 w := modes.NewStub(modes.StubSpec{ 52 Bindings: tk.FuncBindings(func(w tk.Widget, event term.Event) bool { 53 switch event := event.(type) { 54 case term.KeyEvent: 55 codeArea.MutateState(func(s *tk.CodeAreaState) { 56 s.Buffer.InsertAtDot(string(event.Rune)) 57 }) 58 app.PopAddon() 59 return true 60 default: 61 return false 62 } 63 }), 64 Name: " RAW ", 65 }) 66 app.PushAddon(w) 67 } 68 69 var errMustBeKeyOrString = errors.New("must be key or string") 70 71 func toKey(v any) (ui.Key, error) { 72 switch v := v.(type) { 73 case ui.Key: 74 return v, nil 75 case string: 76 return ui.ParseKey(v) 77 default: 78 return ui.Key{}, errMustBeKeyOrString 79 } 80 } 81 82 func notify(app cli.App, x any) error { 83 // TODO: De-duplicate with the implementation of the styled builtin. 84 var t ui.Text 85 switch x := x.(type) { 86 case string: 87 t = ui.T(x) 88 case ui.Text: 89 t = x.Clone() 90 default: 91 return errs.BadValue{What: "argument to edit:notify", 92 Valid: "string, styled segment or styled text", Actual: vals.Kind(x)} 93 } 94 app.Notify(t) 95 return nil 96 } 97 98 func smartEnter(ed *Editor) { 99 codeArea, ok := focusedCodeArea(ed.app) 100 if !ok { 101 return 102 } 103 insertedNewline := false 104 codeArea.MutateState(func(s *tk.CodeAreaState) { 105 buf := &s.Buffer 106 if !isSyntaxComplete(buf.Content) { 107 buf.InsertAtDot("\n") 108 insertedNewline = true 109 } 110 }) 111 if insertedNewline { 112 return 113 } 114 // TODO: Check whether the code area is actually the main code area. This 115 // isn't a problem for now because smart-enter is only bound to Enter in 116 // $edit:insert:binding, which is used by the main code area. 117 // 118 // TODO: This is prone to race condition if the code area was just mutated. 119 ed.applyAutofix() 120 ed.app.CommitCode() 121 } 122 123 func isSyntaxComplete(code string) bool { 124 _, err := parse.Parse(parse.Source{Name: "[syntax check]", Code: code}, parse.Config{}) 125 for _, e := range parse.UnpackErrors(err) { 126 if e.Context.From == len(code) { 127 return false 128 } 129 } 130 return true 131 } 132 133 func wordify(fm *eval.Frame, code string) error { 134 out := fm.ValueOutput() 135 for _, s := range parseutil.Wordify(code) { 136 err := out.Put(s) 137 if err != nil { 138 return err 139 } 140 } 141 return nil 142 } 143 144 func initTTYBuiltins(app cli.App, tty cli.TTY, nb eval.NsBuilder) { 145 nb.AddGoFns(map[string]any{ 146 "insert-raw": func() { insertRaw(app, tty) }, 147 "clear": func() { clear(app, tty) }, 148 }) 149 } 150 151 func initMiscBuiltins(ed *Editor, nb eval.NsBuilder) { 152 nb.AddGoFns(map[string]any{ 153 "binding-table": makeBindingMap, 154 "close-mode": func() { closeMode(ed.app) }, 155 "end-of-history": func() { endOfHistory(ed.app) }, 156 "key": toKey, 157 "notify": func(x any) error { return notify(ed.app, x) }, 158 "redraw": func(opts redrawOpts) { redraw(ed.app, opts) }, 159 "return-line": ed.app.CommitCode, 160 "return-eof": ed.app.CommitEOF, 161 "smart-enter": func() { smartEnter(ed) }, 162 "wordify": wordify, 163 }) 164 } 165 166 // Like mode.FocusedCodeArea, but handles the error by writing a notification. 167 func focusedCodeArea(app cli.App) (tk.CodeArea, bool) { 168 codeArea, err := modes.FocusedCodeArea(app) 169 if err != nil { 170 app.Notify(modes.ErrorText(err)) 171 return nil, false 172 } 173 return codeArea, true 174 }