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  }