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