github.com/ladydascalie/elvish@v0.0.0-20170703214355-2964dd3ece7f/edit/editor.go (about)

     1  // Package edit implements a command line editor.
     2  package edit
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"os"
     8  	"sync"
     9  	"syscall"
    10  	"time"
    11  
    12  	"github.com/elves/elvish/daemon/api"
    13  	"github.com/elves/elvish/edit/highlight"
    14  	"github.com/elves/elvish/edit/history"
    15  	"github.com/elves/elvish/edit/tty"
    16  	"github.com/elves/elvish/edit/ui"
    17  	"github.com/elves/elvish/eval"
    18  	"github.com/elves/elvish/parse"
    19  	"github.com/elves/elvish/sys"
    20  	"github.com/elves/elvish/util"
    21  )
    22  
    23  var logger = util.GetLogger("[edit] ")
    24  
    25  const (
    26  	lackEOLRune = '\u23ce'
    27  	lackEOL     = "\033[7m" + string(lackEOLRune) + "\033[m"
    28  )
    29  
    30  // Editor keeps the status of the line editor.
    31  type Editor struct {
    32  	in     *os.File
    33  	out    *os.File
    34  	writer *Writer
    35  	reader *tty.Reader
    36  	sigs   chan os.Signal
    37  	daemon *api.Client
    38  	evaler *eval.Evaler
    39  
    40  	variables map[string]eval.Variable
    41  
    42  	active      bool
    43  	activeMutex sync.Mutex
    44  
    45  	historyFuser *history.Fuser
    46  	historyMutex sync.RWMutex
    47  
    48  	editorState
    49  }
    50  
    51  type editorState struct {
    52  	// States used during ReadLine. Reset at the beginning of ReadLine.
    53  	savedTermios *sys.Termios
    54  
    55  	notificationMutex sync.Mutex
    56  
    57  	notifications []string
    58  	tips          []string
    59  
    60  	line           string
    61  	lexedLine      *string
    62  	chunk          *parse.Chunk
    63  	styling        *highlight.Styling
    64  	promptContent  []*ui.Styled
    65  	rpromptContent []*ui.Styled
    66  	dot            int
    67  
    68  	mode Mode
    69  
    70  	insert     insert
    71  	command    command
    72  	completion completion
    73  	navigation navigation
    74  	hist       hist
    75  
    76  	// A cache of external commands, used in stylist.
    77  	isExternal      map[string]bool
    78  	parseErrorAtEnd bool
    79  
    80  	// Used for builtins.
    81  	lastKey    ui.Key
    82  	nextAction action
    83  }
    84  
    85  // NewEditor creates an Editor.
    86  func NewEditor(in *os.File, out *os.File, sigs chan os.Signal, ev *eval.Evaler, daemon *api.Client) *Editor {
    87  	ed := &Editor{
    88  		in:     in,
    89  		out:    out,
    90  		writer: newWriter(out),
    91  		reader: tty.NewReader(in),
    92  		sigs:   sigs,
    93  		daemon: daemon,
    94  		evaler: ev,
    95  
    96  		variables: makeVariables(),
    97  	}
    98  	if daemon != nil {
    99  		f, err := history.NewFuser(daemon)
   100  		if err != nil {
   101  			fmt.Fprintln(os.Stderr, "Failed to initialize command history. Disabled.")
   102  		} else {
   103  			ed.historyFuser = f
   104  		}
   105  	}
   106  	ev.Editor = ed
   107  
   108  	installModules(ev.Modules, ed)
   109  
   110  	return ed
   111  }
   112  
   113  // Active returns the activeness of the Editor.
   114  func (ed *Editor) Active() bool {
   115  	return ed.active
   116  }
   117  
   118  // ActiveMutex returns a mutex that must be used when changing the activeness of
   119  // the Editor.
   120  func (ed *Editor) ActiveMutex() *sync.Mutex {
   121  	return &ed.activeMutex
   122  }
   123  
   124  func (ed *Editor) flash() {
   125  	// TODO implement fish-like flash effect
   126  }
   127  
   128  func (ed *Editor) addTip(format string, args ...interface{}) {
   129  	ed.tips = append(ed.tips, fmt.Sprintf(format, args...))
   130  }
   131  
   132  // Notify adds one notification entry. It is concurrency-safe.
   133  func (ed *Editor) Notify(format string, args ...interface{}) {
   134  	ed.notificationMutex.Lock()
   135  	defer ed.notificationMutex.Unlock()
   136  	ed.notifications = append(ed.notifications, fmt.Sprintf(format, args...))
   137  }
   138  
   139  func (ed *Editor) refresh(fullRefresh bool, addErrorsToTips bool) error {
   140  	src := ed.line
   141  	// Re-lex the line if needed
   142  	if ed.lexedLine == nil || *ed.lexedLine != src {
   143  		ed.lexedLine = &src
   144  		n, err := parse.Parse("[interactive]", src)
   145  		ed.chunk = n
   146  
   147  		ed.parseErrorAtEnd = err != nil && atEnd(err, len(src))
   148  		// If all parse errors are at the end, it is likely caused by incomplete
   149  		// input. In that case, do not complain about parse errors.
   150  		// TODO(xiaq): Find a more reliable way to determine incomplete input.
   151  		// Ideally the parser should report it.
   152  		if err != nil && addErrorsToTips && !ed.parseErrorAtEnd {
   153  			ed.addTip("%s", err)
   154  		}
   155  
   156  		ed.styling = &highlight.Styling{}
   157  		doHighlight(n, ed)
   158  
   159  		_, err = ed.evaler.Compile(n, "[interactive]", src)
   160  		if err != nil && !atEnd(err, len(src)) {
   161  			if addErrorsToTips {
   162  				ed.addTip("%s", err)
   163  			}
   164  			// Highlight errors in the input buffer.
   165  			// TODO(xiaq): There might be multiple tokens involved in the
   166  			// compiler error; they should all be highlighted as erroneous.
   167  			p := err.(*eval.CompilationError).Context.Begin
   168  			badn := findLeafNode(n, p)
   169  			ed.styling.Add(badn.Begin(), badn.End(), styleForCompilerError.String())
   170  		}
   171  	}
   172  	return ed.writer.refresh(&ed.editorState, fullRefresh)
   173  }
   174  
   175  func atEnd(e error, n int) bool {
   176  	switch e := e.(type) {
   177  	case *eval.CompilationError:
   178  		return e.Context.Begin == n
   179  	case *parse.Error:
   180  		for _, entry := range e.Entries {
   181  			if entry.Context.Begin != n {
   182  				return false
   183  			}
   184  		}
   185  		return true
   186  	default:
   187  		logger.Printf("atEnd called with error type %T", e)
   188  		return false
   189  	}
   190  }
   191  
   192  // insertAtDot inserts text at the dot and moves the dot after it.
   193  func (ed *Editor) insertAtDot(text string) {
   194  	ed.line = ed.line[:ed.dot] + text + ed.line[ed.dot:]
   195  	ed.dot += len(text)
   196  }
   197  
   198  const flushInputDuringSetup = false
   199  
   200  func setupTerminal(file *os.File) (*sys.Termios, error) {
   201  	fd := int(file.Fd())
   202  	term, err := sys.NewTermiosFromFd(fd)
   203  	if err != nil {
   204  		return nil, fmt.Errorf("can't get terminal attribute: %s", err)
   205  	}
   206  
   207  	savedTermios := term.Copy()
   208  
   209  	term.SetICanon(false)
   210  	term.SetEcho(false)
   211  	term.SetVMin(1)
   212  	term.SetVTime(0)
   213  
   214  	err = term.ApplyToFd(fd)
   215  	if err != nil {
   216  		return nil, fmt.Errorf("can't set up terminal attribute: %s", err)
   217  	}
   218  
   219  	if flushInputDuringSetup {
   220  		err = sys.FlushInput(fd)
   221  		if err != nil {
   222  			return nil, fmt.Errorf("can't flush input: %s", err)
   223  		}
   224  	}
   225  
   226  	return savedTermios, nil
   227  }
   228  
   229  // startReadLine prepares the terminal for the editor.
   230  func (ed *Editor) startReadLine() error {
   231  	ed.activeMutex.Lock()
   232  	defer ed.activeMutex.Unlock()
   233  	ed.active = true
   234  
   235  	savedTermios, err := setupTerminal(ed.in)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	ed.savedTermios = savedTermios
   240  
   241  	_, width := sys.GetWinsize(int(ed.in.Fd()))
   242  	/*
   243  		Write a lackEOLRune if the cursor is not in the leftmost column. This is
   244  		done as follows:
   245  
   246  		1. Turn on autowrap;
   247  
   248  		2. Write lackEOL along with enough padding, so that the total width is
   249  		   equal to the width of the screen.
   250  
   251  		   If the cursor was in the first column, we are still in the same line,
   252  		   just off the line boundary. Otherwise, we are now in the next line.
   253  
   254  		3. Rewind to the first column, write one space and rewind again. If the
   255  		   cursor was in the first column to start with, we have just erased the
   256  		   LackEOL character. Otherwise, we are now in the next line and this is
   257  		   a no-op. The LackEOL character remains.
   258  	*/
   259  	fmt.Fprintf(ed.out, "\033[?7h%s%*s\r \r", lackEOL, width-util.Wcwidth(lackEOLRune), "")
   260  
   261  	/*
   262  		Turn off autowrap.
   263  
   264  		The terminals sometimes has different opinions about how wide some
   265  		characters are (notably emojis and some dingbats) with elvish. When that
   266  		happens, elvish becomes wrong about where the cursor is when it writes
   267  		its output, and the effect can be disastrous.
   268  
   269  		If we turn off autowrap, the terminal won't insert any newlines behind
   270  		the scene, so elvish is always right about which line the cursor is.
   271  		With a bit more caution, this can restrict the consequence of the
   272  		mismatch within one line.
   273  	*/
   274  	ed.out.WriteString("\033[?7l")
   275  	// Turn on SGR-style mouse tracking.
   276  	//ed.out.WriteString("\033[?1000;1006h")
   277  
   278  	// Enable bracketed paste.
   279  	ed.out.WriteString("\033[?2004h")
   280  
   281  	return nil
   282  }
   283  
   284  // finishReadLine puts the terminal in a state suitable for other programs to
   285  // use.
   286  func (ed *Editor) finishReadLine(addError func(error)) {
   287  	ed.activeMutex.Lock()
   288  	defer ed.activeMutex.Unlock()
   289  	ed.active = false
   290  
   291  	// Refresh the terminal for the last time in a clean-ish state.
   292  	ed.mode = &ed.insert
   293  	ed.tips = nil
   294  	ed.dot = len(ed.line)
   295  	if !ed.rpromptPersistent() {
   296  		ed.rpromptContent = nil
   297  	}
   298  	addError(ed.refresh(false, false))
   299  	ed.out.WriteString("\n")
   300  	ed.writer.resetOldBuf()
   301  
   302  	ed.reader.Quit()
   303  
   304  	// Turn on autowrap.
   305  	ed.out.WriteString("\033[?7h")
   306  	// Turn off mouse tracking.
   307  	//ed.out.WriteString("\033[?1000;1006l")
   308  
   309  	// Disable bracketed paste.
   310  	ed.out.WriteString("\033[?2004l")
   311  
   312  	// Restore termios.
   313  	err := ed.savedTermios.ApplyToFd(int(ed.in.Fd()))
   314  	if err != nil {
   315  		addError(fmt.Errorf("can't restore terminal attribute: %s", err))
   316  	}
   317  
   318  	// Save the line before resetting all of editorState.
   319  	line := ed.line
   320  
   321  	ed.editorState = editorState{}
   322  
   323  	callHooks(ed.evaler, ed.afterReadLine(), eval.String(line))
   324  }
   325  
   326  // ReadLine reads a line interactively.
   327  func (ed *Editor) ReadLine() (line string, err error) {
   328  	e := ed.startReadLine()
   329  	if e != nil {
   330  		return "", e
   331  	}
   332  	defer ed.finishReadLine(func(e error) {
   333  		if e != nil {
   334  			err = util.CatError(err, e)
   335  		}
   336  	})
   337  
   338  	ed.mode = &ed.insert
   339  
   340  	// Find external commands asynchronously, so that slow I/O won't block the
   341  	// editor.
   342  	isExternalCh := make(chan map[string]bool, 1)
   343  	go getIsExternal(ed.evaler, isExternalCh)
   344  
   345  	go ed.reader.Run()
   346  
   347  	fullRefresh := false
   348  
   349  	callHooks(ed.evaler, ed.beforeReadLine())
   350  
   351  MainLoop:
   352  	for {
   353  		ed.promptContent = callPrompt(ed, ed.prompt())
   354  		ed.rpromptContent = callPrompt(ed, ed.rprompt())
   355  
   356  		err := ed.refresh(fullRefresh, true)
   357  		fullRefresh = false
   358  		if err != nil {
   359  			return "", err
   360  		}
   361  
   362  		ed.tips = nil
   363  
   364  		select {
   365  		case m := <-isExternalCh:
   366  			ed.isExternal = m
   367  		case sig := <-ed.sigs:
   368  			// TODO(xiaq): Maybe support customizable handling of signals
   369  			switch sig {
   370  			case syscall.SIGINT:
   371  				// Start over
   372  				ed.editorState = editorState{
   373  					savedTermios: ed.savedTermios,
   374  					isExternal:   ed.isExternal,
   375  				}
   376  				ed.mode = &ed.insert
   377  				continue MainLoop
   378  			case syscall.SIGWINCH:
   379  				fullRefresh = true
   380  				continue MainLoop
   381  			case syscall.SIGCHLD:
   382  				// ignore
   383  			default:
   384  				ed.addTip("ignored signal %s", sig)
   385  			}
   386  		case err := <-ed.reader.ErrorChan():
   387  			ed.Notify("reader error: %s", err.Error())
   388  		case unit := <-ed.reader.UnitChan():
   389  			switch unit := unit.(type) {
   390  			case tty.MouseEvent:
   391  				ed.addTip("mouse: %+v", unit)
   392  			case tty.CursorPosition:
   393  				// Ignore CPR
   394  			case tty.PasteSetting:
   395  				if !unit {
   396  					continue
   397  				}
   398  				var buf bytes.Buffer
   399  				timer := time.NewTimer(tty.EscSequenceTimeout)
   400  			paste:
   401  				for {
   402  					// XXX Should also select on other chans. However those chans
   403  					// will be unified (again) into one later so we don't do
   404  					// busywork here.
   405  					select {
   406  					case unit := <-ed.reader.UnitChan():
   407  						switch unit := unit.(type) {
   408  						case tty.Key:
   409  							k := ui.Key(unit)
   410  							if k.Mod != 0 {
   411  								ed.Notify("function key within paste, aborting")
   412  								break paste
   413  							}
   414  							buf.WriteRune(k.Rune)
   415  							timer.Reset(tty.EscSequenceTimeout)
   416  						case tty.PasteSetting:
   417  							if !unit {
   418  								break paste
   419  							}
   420  						default: // Ignore other things.
   421  						}
   422  					case <-timer.C:
   423  						ed.Notify("bracketed paste timeout")
   424  						break paste
   425  					}
   426  				}
   427  				topaste := buf.String()
   428  				if ed.insert.quotePaste {
   429  					topaste = parse.Quote(topaste)
   430  				}
   431  				ed.insertAtDot(topaste)
   432  			case tty.RawRune:
   433  				insertRaw(ed, rune(unit))
   434  			case tty.Key:
   435  				k := ui.Key(unit)
   436  			lookupKey:
   437  				fn := ed.mode.Binding(k)
   438  				if fn == nil {
   439  					ed.addTip("Unbound and no default binding: %s", k)
   440  					continue MainLoop
   441  				}
   442  
   443  				ed.insert.insertedLiteral = false
   444  				ed.lastKey = k
   445  				ed.CallFn(fn)
   446  				if ed.insert.insertedLiteral {
   447  					ed.insert.literalInserts++
   448  				} else {
   449  					ed.insert.literalInserts = 0
   450  				}
   451  				act := ed.nextAction
   452  				ed.nextAction = action{}
   453  
   454  				switch act.typ {
   455  				case noAction:
   456  					continue
   457  				case reprocessKey:
   458  					err := ed.refresh(false, true)
   459  					if err != nil {
   460  						return "", err
   461  					}
   462  					goto lookupKey
   463  				case exitReadLine:
   464  					if act.returnErr == nil && act.returnLine != "" {
   465  						ed.appendHistory(act.returnLine)
   466  					}
   467  					return act.returnLine, act.returnErr
   468  				}
   469  			}
   470  		}
   471  	}
   472  }
   473  
   474  // getIsExternal finds a set of all external commands and puts it on the result
   475  // channel.
   476  func getIsExternal(ev *eval.Evaler, result chan<- map[string]bool) {
   477  	isExternal := make(map[string]bool)
   478  	ev.EachExternal(func(name string) {
   479  		isExternal[name] = true
   480  	})
   481  	result <- isExternal
   482  }