github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/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  	"fmt"
    10  	"sync"
    11  
    12  	"src.elv.sh/pkg/cli"
    13  	"src.elv.sh/pkg/eval"
    14  	"src.elv.sh/pkg/eval/vals"
    15  	"src.elv.sh/pkg/eval/vars"
    16  	"src.elv.sh/pkg/parse"
    17  	"src.elv.sh/pkg/store"
    18  )
    19  
    20  // Editor is the interactive line editor for Elvish.
    21  type Editor struct {
    22  	app cli.App
    23  	ns  *eval.Ns
    24  
    25  	excMutex sync.RWMutex
    26  	excList  vals.List
    27  
    28  	// Maybe move this to another type that represents the REPL cycle as a whole, not just the
    29  	// read/edit portion represented by the Editor type.
    30  	AfterCommand []func(src parse.Source, duration float64, err error)
    31  }
    32  
    33  // An interface that wraps notifyf and notifyError. It is only implemented by
    34  // the *Editor type; functions may take a notifier instead of *Editor argument
    35  // to make it clear that they do not depend on other parts of *Editor.
    36  type notifier interface {
    37  	notifyf(format string, args ...interface{})
    38  	notifyError(ctx string, e error)
    39  }
    40  
    41  // NewEditor creates a new editor. The TTY is used for input and output. The
    42  // Evaler is used for syntax highlighting, completion, and calling callbacks.
    43  // The Store is used for saving and retrieving command and directory history.
    44  func NewEditor(tty cli.TTY, ev *eval.Evaler, st store.Store) *Editor {
    45  	// Declare the Editor with a nil App first; some initialization functions
    46  	// require a notifier as an argument, but does not use it immediately.
    47  	ed := &Editor{excList: vals.EmptyList}
    48  	nb := eval.NsBuilder{}
    49  	appSpec := cli.AppSpec{TTY: tty}
    50  
    51  	hs, err := newHistStore(st)
    52  	if err != nil {
    53  		// TODO(xiaq): Report the error.
    54  	}
    55  
    56  	initHighlighter(&appSpec, ev)
    57  	initMaxHeight(&appSpec, nb)
    58  	initReadlineHooks(&appSpec, ev, nb)
    59  	initAddCmdFilters(&appSpec, ev, nb, hs)
    60  	initGlobalBindings(&appSpec, ed, ev, nb)
    61  	initInsertAPI(&appSpec, ed, ev, nb)
    62  	initPrompts(&appSpec, ed, ev, nb)
    63  	ed.app = cli.NewApp(appSpec)
    64  
    65  	initExceptionsAPI(ed, nb)
    66  	initVarsAPI(ed, nb)
    67  	initCommandAPI(ed, ev, nb)
    68  	initListings(ed, ev, st, hs, nb)
    69  	initNavigation(ed, ev, nb)
    70  	initCompletion(ed, ev, nb)
    71  	initHistWalk(ed, ev, hs, nb)
    72  	initInstant(ed, ev, nb)
    73  	initMinibuf(ed, ev, nb)
    74  
    75  	initRepl(ed, ev, nb)
    76  	initBufferBuiltins(ed.app, nb)
    77  	initTTYBuiltins(ed.app, tty, nb)
    78  	initMiscBuiltins(ed.app, nb)
    79  	initStateAPI(ed.app, nb)
    80  	initStoreAPI(ed.app, nb, hs)
    81  
    82  	ed.ns = nb.Ns()
    83  	initElvishState(ev, ed.ns)
    84  
    85  	return ed
    86  }
    87  
    88  //elvdoc:var exceptions
    89  //
    90  // A list of exceptions thrown from callbacks such as prompts. Useful for
    91  // examining tracebacks and other metadata.
    92  
    93  func initExceptionsAPI(ed *Editor, nb eval.NsBuilder) {
    94  	nb.Add("exceptions", vars.FromPtrWithMutex(&ed.excList, &ed.excMutex))
    95  }
    96  
    97  // Initialize the `edit` module by executing the pre-defined Elvish code for the module.
    98  func initElvishState(ev *eval.Evaler, ns *eval.Ns) {
    99  	src := parse.Source{Name: "[RC file]", Code: elvInit}
   100  	err := ev.Eval(src, eval.EvalCfg{Global: ns})
   101  	if err != nil {
   102  		panic(err)
   103  	}
   104  }
   105  
   106  // ReadCode reads input from the user.
   107  func (ed *Editor) ReadCode() (string, error) {
   108  	return ed.app.ReadCode()
   109  }
   110  
   111  // RunAfterCommandHooks runs callbacks involving the interactive completion of a command line.
   112  func (ed *Editor) RunAfterCommandHooks(src parse.Source, duration float64, err error) {
   113  	for _, f := range ed.AfterCommand {
   114  		f(src, duration, err)
   115  	}
   116  }
   117  
   118  // Ns returns a namespace for manipulating the editor from Elvish code.
   119  //
   120  // See https://elv.sh/ref/edit.html for the Elvish API.
   121  func (ed *Editor) Ns() *eval.Ns {
   122  	return ed.ns
   123  }
   124  
   125  func (ed *Editor) notifyf(format string, args ...interface{}) {
   126  	ed.app.Notify(fmt.Sprintf(format, args...))
   127  }
   128  
   129  func (ed *Editor) notifyError(ctx string, e error) {
   130  	if exc, ok := e.(eval.Exception); ok {
   131  		ed.excMutex.Lock()
   132  		defer ed.excMutex.Unlock()
   133  		ed.excList = ed.excList.Cons(exc)
   134  		ed.notifyf("[%v error] %v\n"+
   135  			`see stack trace with "show $edit:exceptions[%d]"`,
   136  			ctx, e, ed.excList.Len()-1)
   137  	} else {
   138  		ed.notifyf("[%v error] %v", ctx, e)
   139  	}
   140  }