src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/editor.go (about)

     1  // Package edit implements the line editor for Elvish.
     2  //
     3  // The line editor is based on the cli package, which implements a general,
     4  // Elvish-agnostic line editor, and multiple "addon" packages. This package
     5  // glues them together and provides Elvish bindings for them.
     6  package edit
     7  
     8  import (
     9  	_ "embed"
    10  	"fmt"
    11  	"sync"
    12  	"sync/atomic"
    13  
    14  	"src.elv.sh/pkg/cli"
    15  	"src.elv.sh/pkg/eval"
    16  	"src.elv.sh/pkg/eval/vals"
    17  	"src.elv.sh/pkg/eval/vars"
    18  	"src.elv.sh/pkg/parse"
    19  	"src.elv.sh/pkg/store/storedefs"
    20  	"src.elv.sh/pkg/ui"
    21  )
    22  
    23  // Editor is the interactive line editor for Elvish.
    24  type Editor struct {
    25  	app cli.App
    26  	ns  *eval.Ns
    27  
    28  	excMutex sync.RWMutex
    29  	excList  vals.List
    30  
    31  	autofix atomic.Value
    32  	// This is an ugly hack to let the implementation of edit:smart-enter and
    33  	// edit:completion:smart-start to apply the autofix easily. This field is
    34  	// set in initHighlighter.
    35  	applyAutofix func()
    36  
    37  	// Maybe move this to another type that represents the REPL cycle as a whole, not just the
    38  	// read/edit portion represented by the Editor type.
    39  	AfterCommand []func(src parse.Source, duration float64, err error)
    40  }
    41  
    42  // An interface that wraps notifyf and notifyError. It is only implemented by
    43  // the *Editor type; functions may take a notifier instead of *Editor argument
    44  // to make it clear that they do not depend on other parts of *Editor.
    45  type notifier interface {
    46  	notifyf(format string, args ...any)
    47  	notifyError(ctx string, e error)
    48  }
    49  
    50  // NewEditor creates a new editor. The TTY is used for input and output. The
    51  // Evaler is used for syntax highlighting, completion, and calling callbacks.
    52  // The Store is used for saving and retrieving command and directory history.
    53  func NewEditor(tty cli.TTY, ev *eval.Evaler, st storedefs.Store) *Editor {
    54  	// Declare the Editor with a nil App first; some initialization functions
    55  	// require a notifier as an argument, but does not use it immediately.
    56  	ed := &Editor{excList: vals.EmptyList}
    57  	ed.autofix.Store("")
    58  	nb := eval.BuildNsNamed("edit")
    59  	appSpec := cli.AppSpec{TTY: tty}
    60  
    61  	hs, err := newHistStore(st)
    62  	if err != nil {
    63  		_ = err // TODO(xiaq): Report the error.
    64  	}
    65  
    66  	initMaxHeight(&appSpec, nb)
    67  	initReadlineHooks(&appSpec, ev, nb)
    68  	initAddCmdFilters(&appSpec, ev, nb, hs)
    69  	initGlobalBindings(&appSpec, ed, ev, nb)
    70  	initInsertAPI(&appSpec, ed, ev, nb)
    71  	initHighlighter(&appSpec, ed, ev, nb)
    72  	initPrompts(&appSpec, ed, ev, nb)
    73  	ed.app = cli.NewApp(appSpec)
    74  
    75  	initExceptionsAPI(ed, nb)
    76  	initVarsAPI(nb)
    77  	initCommandAPI(ed, ev, nb)
    78  	initListings(ed, ev, st, hs, nb)
    79  	initNavigation(ed, ev, nb)
    80  	initCompletion(ed, ev, nb)
    81  	initHistWalk(ed, ev, hs, nb)
    82  	initInstant(ed, ev, nb)
    83  	initMinibuf(ed, ev, nb)
    84  
    85  	initRepl(ed, ev, nb)
    86  	initBufferBuiltins(ed.app, nb)
    87  	initTTYBuiltins(ed.app, tty, nb)
    88  	initMiscBuiltins(ed, nb)
    89  	initStateAPI(ed.app, nb)
    90  	initStoreAPI(ed.app, nb, hs)
    91  
    92  	ed.ns = nb.Ns()
    93  	initElvishState(ev, ed.ns)
    94  
    95  	return ed
    96  }
    97  
    98  func initExceptionsAPI(ed *Editor, nb eval.NsBuilder) {
    99  	nb.AddVar("exceptions", vars.FromPtrWithMutex(&ed.excList, &ed.excMutex))
   100  }
   101  
   102  //go:embed init.elv
   103  var initElv string
   104  
   105  // Initialize the `edit` module by executing the pre-defined Elvish code for the module.
   106  func initElvishState(ev *eval.Evaler, ns *eval.Ns) {
   107  	src := parse.Source{Name: "[init.elv]", Code: initElv}
   108  	err := ev.Eval(src, eval.EvalCfg{Global: ns})
   109  	if err != nil {
   110  		panic(err)
   111  	}
   112  }
   113  
   114  // ReadCode reads input from the user.
   115  func (ed *Editor) ReadCode() (string, error) {
   116  	return ed.app.ReadCode()
   117  }
   118  
   119  // Notify adds a note to the notification buffer.
   120  func (ed *Editor) Notify(note ui.Text) {
   121  	ed.app.Notify(note)
   122  }
   123  
   124  // RunAfterCommandHooks runs callbacks involving the interactive completion of a command line.
   125  func (ed *Editor) RunAfterCommandHooks(src parse.Source, duration float64, err error) {
   126  	for _, f := range ed.AfterCommand {
   127  		f(src, duration, err)
   128  	}
   129  }
   130  
   131  // Ns returns a namespace for manipulating the editor from Elvish code.
   132  //
   133  // See https://elv.sh/ref/edit.html for the Elvish API.
   134  func (ed *Editor) Ns() *eval.Ns {
   135  	return ed.ns
   136  }
   137  
   138  func (ed *Editor) notifyf(format string, args ...any) {
   139  	ed.app.Notify(ui.T(fmt.Sprintf(format, args...)))
   140  }
   141  
   142  func (ed *Editor) notifyError(ctx string, e error) {
   143  	if exc, ok := e.(eval.Exception); ok {
   144  		ed.excMutex.Lock()
   145  		defer ed.excMutex.Unlock()
   146  		ed.excList = ed.excList.Conj(exc)
   147  		ed.notifyf("[%v error] %v\n"+
   148  			`see stack trace with "show $edit:exceptions[%d]"`,
   149  			ctx, e, ed.excList.Len()-1)
   150  	} else {
   151  		ed.notifyf("[%v error] %v", ctx, e)
   152  	}
   153  }