github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/cmds/elvish/edit/edit.go (about)

     1  // Package edit implements a command line editor.
     2  package edit
     3  
     4  import (
     5  	"bufio"
     6  	"bytes"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"sync"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/u-root/u-root/cmds/elvish/edit/eddefs"
    15  	"github.com/u-root/u-root/cmds/elvish/edit/highlight"
    16  	"github.com/u-root/u-root/cmds/elvish/edit/tty"
    17  	"github.com/u-root/u-root/cmds/elvish/edit/ui"
    18  	"github.com/u-root/u-root/cmds/elvish/eval"
    19  	"github.com/u-root/u-root/cmds/elvish/eval/vals"
    20  	"github.com/u-root/u-root/cmds/elvish/parse"
    21  	"github.com/u-root/u-root/cmds/elvish/sys"
    22  	"github.com/u-root/u-root/cmds/elvish/util"
    23  	"github.com/u-root/u-root/cmds/elvish/hashmap"
    24  )
    25  
    26  var logger = util.GetLogger("[edit] ")
    27  
    28  // editor implements the line editor.
    29  type editor struct {
    30  	in     *os.File
    31  	out    *os.File
    32  	writer tty.Writer
    33  	reader tty.Reader
    34  	sigs   <-chan os.Signal
    35  	evaler *eval.Evaler
    36  
    37  	active      bool
    38  	activeMutex sync.Mutex
    39  
    40  	// notifyPort is a write-only port that turns data written to it into editor
    41  	// notifications.
    42  	notifyPort *eval.Port
    43  	// notifyRead is the read end of notifyPort.File.
    44  	notifyRead *os.File
    45  
    46  	// Configurations. Each of the following fields have an initializer defined
    47  	// using atEditorInit.
    48  	editorHooks
    49  	abbr         hashmap.Map
    50  	argCompleter hashmap.Map
    51  	maxHeight    float64
    52  
    53  	prompt, rprompt   eddefs.Prompt
    54  	RpromptPersistent bool
    55  
    56  	// Modes.
    57  	insert     *insert
    58  	command    *command
    59  	navigation *navigation
    60  	listing    *listingMode
    61  
    62  	editorState
    63  }
    64  
    65  type editorState struct {
    66  	// States used during ReadLine. Reset at the beginning of ReadLine.
    67  	restoreTerminal func() error
    68  
    69  	notificationMutex sync.Mutex
    70  
    71  	notifications []string
    72  	tips          []string
    73  
    74  	buffer string
    75  	dot    int
    76  
    77  	chunk           *parse.Chunk
    78  	styling         *highlight.Styling
    79  	parseErrorAtEnd bool
    80  
    81  	promptContent, rpromptContent []*ui.Styled
    82  
    83  	mode eddefs.Mode
    84  
    85  	// A cache of external commands, used in stylist.
    86  	isExternal map[string]bool
    87  
    88  	// Used for builtins.
    89  	lastKey    ui.Key
    90  	nextAction eddefs.Action
    91  }
    92  
    93  // NewEditor creates an Editor. When the instance is no longer used, its Close
    94  // method should be called.
    95  func NewEditor(in *os.File, out *os.File, sigs <-chan os.Signal, ev *eval.Evaler) eddefs.Editor {
    96  	ed := &editor{
    97  		in:     in,
    98  		out:    out,
    99  		writer: tty.NewWriter(out),
   100  		reader: tty.NewReader(in),
   101  		sigs:   sigs,
   102  		evaler: ev,
   103  	}
   104  
   105  	notifyChan := make(chan interface{})
   106  	notifyRead, notifyWrite, err := os.Pipe()
   107  	if err != nil {
   108  		panic(err)
   109  	}
   110  	ed.notifyPort = &eval.Port{File: notifyWrite, Chan: notifyChan}
   111  	ed.notifyRead = notifyRead
   112  	// Forward reads from notifyRead to notification.
   113  	go func() {
   114  		reader := bufio.NewReader(notifyRead)
   115  		for {
   116  			line, err := reader.ReadString('\n')
   117  			if err != nil {
   118  				break
   119  			}
   120  			ed.Notify("[bytes out] %s", line[:len(line)-1])
   121  		}
   122  		if err != io.EOF {
   123  			logger.Println("notifyRead error:", err)
   124  		}
   125  	}()
   126  	// Forward reads from notifyChan to notification.
   127  	go func() {
   128  		for v := range notifyChan {
   129  			ed.Notify("[value out] %s", vals.Repr(v, vals.NoPretty))
   130  		}
   131  	}()
   132  
   133  	ev.Editor = ed
   134  
   135  	ns := makeNs(ed)
   136  	for _, f := range editorInitFuncs {
   137  		f(ed, ns)
   138  	}
   139  	ev.Builtin.AddNs("edit", ns)
   140  
   141  	err = ev.EvalSource(eval.NewScriptSource("[editor]", "[editor]", "use binding; binding:install"))
   142  	if err != nil {
   143  		fmt.Fprintln(out, "Failed to load default binding:", err)
   144  	}
   145  
   146  	return ed
   147  }
   148  
   149  func (ed *editor) Close() {
   150  	ed.reader.Close()
   151  	close(ed.notifyPort.Chan)
   152  	ed.notifyPort.File.Close()
   153  	ed.notifyRead.Close()
   154  	ed.prompt.Close()
   155  	ed.rprompt.Close()
   156  }
   157  
   158  func (ed *editor) Evaler() *eval.Evaler {
   159  	return ed.evaler
   160  }
   161  
   162  func (ed *editor) Buffer() (string, int) {
   163  	return ed.buffer, ed.dot
   164  }
   165  
   166  func (ed *editor) SetBuffer(buffer string, dot int) {
   167  	ed.buffer, ed.dot = buffer, dot
   168  }
   169  
   170  func (ed *editor) ParsedBuffer() *parse.Chunk {
   171  	return ed.chunk
   172  }
   173  
   174  func (ed *editor) SetMode(m eddefs.Mode) {
   175  	if ed.mode != nil {
   176  		ed.mode.Teardown()
   177  	}
   178  	ed.mode = m
   179  }
   180  
   181  func (ed *editor) SetModeInsert() {
   182  	ed.SetMode(ed.insert)
   183  }
   184  
   185  func (ed *editor) SetModeListing(b eddefs.BindingMap, p eddefs.ListingProvider) {
   186  	ed.listing.setup(b, p)
   187  	ed.SetMode(ed.listing)
   188  }
   189  
   190  func (ed *editor) RefreshListing() {
   191  	if l, ok := ed.mode.(*listingMode); ok {
   192  		l.refresh()
   193  	}
   194  }
   195  
   196  func (ed *editor) flash() {
   197  	// TODO implement fish-like flash effect
   198  }
   199  
   200  // AddTip adds a message to the tip area.
   201  func (ed *editor) AddTip(format string, args ...interface{}) {
   202  	ed.tips = append(ed.tips, fmt.Sprintf(format, args...))
   203  }
   204  
   205  // Notify writes out a message in a way that does not interrupt the editor
   206  // display. When the editor is not active, it simply writes the message to the
   207  // terminal. When the editor is active, it appends the message to the
   208  // notification queue, which will be written out during the update cycle. It can
   209  // be safely used concurrently.
   210  func (ed *editor) Notify(format string, args ...interface{}) {
   211  	msg := fmt.Sprintf(format, args...)
   212  	ed.activeMutex.Lock()
   213  	defer ed.activeMutex.Unlock()
   214  	// If the editor is not active, simply write out the message.
   215  	if !ed.active {
   216  		ed.out.WriteString(msg + "\n")
   217  		return
   218  	}
   219  	ed.notificationMutex.Lock()
   220  	defer ed.notificationMutex.Unlock()
   221  	ed.notifications = append(ed.notifications, msg)
   222  }
   223  
   224  func (ed *editor) LastKey() ui.Key {
   225  	return ed.lastKey
   226  }
   227  
   228  func (ed *editor) refresh(fullRefresh bool, addErrorsToTips bool) error {
   229  	src := ed.buffer
   230  	// Parse the current line
   231  	n, err := parse.Parse("[interactive]", src)
   232  	ed.chunk = n
   233  
   234  	ed.parseErrorAtEnd = err != nil && atEnd(err, len(src))
   235  	// If all parse errors are at the end, it is likely caused by incomplete
   236  	// input. In that case, do not complain about parse errors.
   237  	// TODO(xiaq): Find a more reliable way to determine incomplete input.
   238  	// Ideally the parser should report it.
   239  	if err != nil && addErrorsToTips && !ed.parseErrorAtEnd {
   240  		ed.AddTip("%s", err)
   241  	}
   242  
   243  	ed.styling = &highlight.Styling{}
   244  	doHighlight(n, ed)
   245  
   246  	_, err = ed.evaler.Compile(n, eval.NewInteractiveSource(src))
   247  	if err != nil && !atEnd(err, len(src)) {
   248  		if addErrorsToTips {
   249  			ed.AddTip("%s", err)
   250  		}
   251  		// Highlight errors in the input buffer.
   252  		ctx := err.(*eval.CompilationError).Context
   253  		ed.styling.Add(ctx.Begin, ctx.End, styleForCompilerError.String())
   254  	}
   255  
   256  	// Render onto a buffer.
   257  	height, width := sys.GetWinsize(ed.out)
   258  	height = min(height, maxHeightToInt(ed.maxHeight))
   259  	er := &editorRenderer{&ed.editorState, height, nil}
   260  	buf := ui.Render(er, width)
   261  	return ed.writer.CommitBuffer(er.bufNoti, buf, fullRefresh)
   262  }
   263  
   264  func atEnd(e error, n int) bool {
   265  	switch e := e.(type) {
   266  	case *eval.CompilationError:
   267  		return e.Context.Begin == n
   268  	case *parse.Error:
   269  		for _, entry := range e.Entries {
   270  			if entry.Context.Begin != n {
   271  				return false
   272  			}
   273  		}
   274  		return true
   275  	default:
   276  		logger.Printf("atEnd called with error type %T", e)
   277  		return false
   278  	}
   279  }
   280  
   281  // InsertAtDot inserts text at the dot and moves the dot after it.
   282  func (ed *editor) InsertAtDot(text string) {
   283  	ed.buffer = ed.buffer[:ed.dot] + text + ed.buffer[ed.dot:]
   284  	ed.dot += len(text)
   285  }
   286  
   287  func (ed *editor) SetPrompt(prompt eddefs.Prompt) {
   288  	ed.prompt = prompt
   289  }
   290  
   291  func (ed *editor) SetRPrompt(rprompt eddefs.Prompt) {
   292  	ed.rprompt = rprompt
   293  }
   294  
   295  // startReadLine prepares the terminal for the editor.
   296  func (ed *editor) startReadLine() error {
   297  	ed.activeMutex.Lock()
   298  	defer ed.activeMutex.Unlock()
   299  	ed.active = true
   300  
   301  	restoreTerminal, err := tty.Setup(ed.in, ed.out)
   302  	if err != nil {
   303  		if restoreTerminal != nil {
   304  			restoreTerminal()
   305  		}
   306  		return err
   307  	}
   308  	ed.restoreTerminal = restoreTerminal
   309  
   310  	return nil
   311  }
   312  
   313  // finishReadLine puts the terminal in a state suitable for other programs to
   314  // use.
   315  func (ed *editor) finishReadLine() error {
   316  	// After-readline hooks should be called before most teardown happens as
   317  	// they can cause the editor to refresh.
   318  	for _, f := range ed.afterReadline {
   319  		f(ed.buffer)
   320  	}
   321  
   322  	ed.activeMutex.Lock()
   323  	defer ed.activeMutex.Unlock()
   324  	ed.active = false
   325  
   326  	// Refresh the terminal for the last time in a clean-ish state.
   327  	ed.SetModeInsert()
   328  	ed.tips = nil
   329  	ed.dot = len(ed.buffer)
   330  	if !ed.RpromptPersistent {
   331  		ed.rpromptContent = nil
   332  	}
   333  	errRefresh := ed.refresh(false, false)
   334  	ed.mode.Teardown()
   335  	ed.out.WriteString("\n")
   336  	ed.writer.ResetCurrentBuffer()
   337  
   338  	ed.reader.Stop()
   339  
   340  	// Restore termios.
   341  	errRestore := ed.restoreTerminal()
   342  
   343  	// Reset all of editorState.
   344  	ed.editorState = editorState{}
   345  
   346  	return util.Errors(errRefresh, errRestore)
   347  }
   348  
   349  // ReadLine reads a line interactively.
   350  func (ed *editor) ReadLine() (string, error) {
   351  	err := ed.startReadLine()
   352  	if err != nil {
   353  		return "", err
   354  	}
   355  	defer func() {
   356  		err := ed.finishReadLine()
   357  		if err != nil {
   358  			fmt.Fprintln(ed.out, "error:", err)
   359  		}
   360  	}()
   361  
   362  	ed.SetModeInsert()
   363  
   364  	// Find external commands asynchronously, so that slow I/O won't block the
   365  	// editor.
   366  	isExternalCh := make(chan map[string]bool, 1)
   367  	go getIsExternal(ed.evaler, isExternalCh)
   368  
   369  	ed.reader.Start()
   370  
   371  	fullRefresh := false
   372  
   373  	for _, f := range ed.beforeReadline {
   374  		f()
   375  	}
   376  
   377  	ed.promptContent = ed.prompt.Last()
   378  	ed.rpromptContent = ed.rprompt.Last()
   379  	fresh := true
   380  MainLoop:
   381  	for {
   382  		ed.prompt.Update(fresh)
   383  		ed.rprompt.Update(fresh)
   384  		fresh = false
   385  
   386  	refresh:
   387  		err := ed.refresh(fullRefresh, true)
   388  		fullRefresh = false
   389  		if err != nil {
   390  			return "", err
   391  		}
   392  
   393  		ed.tips = nil
   394  
   395  		select {
   396  		case ed.promptContent = <-ed.prompt.Chan():
   397  			logger.Println("prompt fetched late")
   398  			goto refresh
   399  		case ed.rpromptContent = <-ed.rprompt.Chan():
   400  			logger.Println("rprompt fetched late")
   401  			goto refresh
   402  		case m := <-isExternalCh:
   403  			ed.isExternal = m
   404  			goto refresh
   405  		case sig := <-ed.sigs:
   406  			// TODO(xiaq): Maybe support customizable handling of signals
   407  			switch sig {
   408  			case syscall.SIGHUP:
   409  				return "", io.EOF
   410  			case syscall.SIGINT:
   411  				// Start over
   412  				ed.mode.Teardown()
   413  				ed.editorState = editorState{
   414  					restoreTerminal: ed.restoreTerminal,
   415  					isExternal:      ed.isExternal,
   416  				}
   417  				ed.SetModeInsert()
   418  				fresh = true
   419  				continue MainLoop
   420  			case sys.SIGWINCH:
   421  				fullRefresh = true
   422  				continue MainLoop
   423  			default:
   424  				ed.AddTip("ignored signal %s", sig)
   425  			}
   426  		case event := <-ed.reader.EventChan():
   427  			switch event := event.(type) {
   428  			case tty.NonfatalErrorEvent:
   429  				ed.Notify("error when reading terminal: %v", event.Err)
   430  			case tty.FatalErrorEvent:
   431  				ed.Notify("fatal error when reading terminal: %v", event.Err)
   432  				return "", event.Err
   433  			case tty.MouseEvent:
   434  				ed.AddTip("mouse: %+v", event)
   435  			case tty.CursorPosition:
   436  				// Ignore CPR
   437  			case tty.PasteSetting:
   438  				if !event {
   439  					continue
   440  				}
   441  				var buf bytes.Buffer
   442  				timer := time.NewTimer(tty.DefaultSeqTimeout)
   443  			paste:
   444  				for {
   445  					// XXX Should also select on other chans. However those chans
   446  					// will be unified (again) into one later so we don't do
   447  					// busywork here.
   448  					select {
   449  					case event := <-ed.reader.EventChan():
   450  						switch event := event.(type) {
   451  						case tty.KeyEvent:
   452  							k := ui.Key(event)
   453  							if k.Mod != 0 {
   454  								ed.Notify("function key within paste, aborting")
   455  								break paste
   456  							}
   457  							buf.WriteRune(k.Rune)
   458  							timer.Reset(tty.DefaultSeqTimeout)
   459  						case tty.PasteSetting:
   460  							if !event {
   461  								break paste
   462  							}
   463  						default: // Ignore other things.
   464  						}
   465  					case <-timer.C:
   466  						ed.Notify("bracketed paste timeout")
   467  						break paste
   468  					}
   469  				}
   470  				topaste := buf.String()
   471  				if ed.insert.quotePaste {
   472  					topaste = parse.Quote(topaste)
   473  				}
   474  				ed.InsertAtDot(topaste)
   475  			case tty.RawRune:
   476  				insertRaw(ed, rune(event))
   477  			case tty.KeyEvent:
   478  				k := ui.Key(event)
   479  			lookupKey:
   480  				fn := ed.mode.Binding(k)
   481  				if fn == nil {
   482  					ed.AddTip("Unbound and no default binding: %s", k)
   483  					continue MainLoop
   484  				}
   485  
   486  				ed.insert.insertedLiteral = false
   487  				ed.lastKey = k
   488  				ed.CallFn(fn)
   489  				if ed.insert.insertedLiteral {
   490  					ed.insert.literalInserts++
   491  				} else {
   492  					ed.insert.literalInserts = 0
   493  				}
   494  
   495  				switch ed.popAction() {
   496  				case reprocessKey:
   497  					err := ed.refresh(false, true)
   498  					if err != nil {
   499  						return "", err
   500  					}
   501  					goto lookupKey
   502  				case commitLine:
   503  					return ed.buffer, nil
   504  				case commitEOF:
   505  					return "", io.EOF
   506  				}
   507  			}
   508  		}
   509  	}
   510  }
   511  
   512  // getIsExternal finds a set of all external commands and puts it on the result
   513  // channel.
   514  func getIsExternal(ev *eval.Evaler, result chan<- map[string]bool) {
   515  	isExternal := make(map[string]bool)
   516  	eval.EachExternal(func(name string) {
   517  		isExternal[name] = true
   518  	})
   519  	result <- isExternal
   520  }