github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/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  	"syscall"
     9  	"time"
    10  
    11  	"github.com/elves/elvish/eval"
    12  	"github.com/elves/elvish/parse"
    13  	"github.com/elves/elvish/store"
    14  	"github.com/elves/elvish/sys"
    15  	"github.com/elves/elvish/util"
    16  )
    17  
    18  var Logger = util.GetLogger("[edit] ")
    19  
    20  const (
    21  	lackEOLRune = '\u23ce'
    22  	lackEOL     = "\033[7m" + string(lackEOLRune) + "\033[m"
    23  )
    24  
    25  // Editor keeps the status of the line editor.
    26  type Editor struct {
    27  	file   *os.File
    28  	writer *writer
    29  	reader *Reader
    30  	sigs   chan os.Signal
    31  	store  *store.Store
    32  	evaler *eval.Evaler
    33  	cmdSeq int
    34  
    35  	ps1           Prompt
    36  	rps1          Prompt
    37  	completers    map[string]ArgCompleter
    38  	abbreviations map[string]string
    39  
    40  	editorState
    41  }
    42  
    43  type editorState struct {
    44  	// States used during ReadLine. Reset at the beginning of ReadLine.
    45  	active       bool
    46  	savedTermios *sys.Termios
    47  
    48  	notifications []string
    49  	tips          []string
    50  
    51  	tokens  []Token
    52  	prompt  string
    53  	rprompt string
    54  	line    string
    55  	dot     int
    56  
    57  	mode Mode
    58  
    59  	insert     insert
    60  	command    command
    61  	completion completion
    62  	navigation navigation
    63  	hist       hist
    64  	histlist   *histlist
    65  	bang       *bang
    66  	location   *location
    67  
    68  	// A cache of external commands, used in stylist and completer of command
    69  	// names.
    70  	isExternal      map[string]bool
    71  	parseErrorAtEnd bool
    72  
    73  	// Used for builtins.
    74  	lastKey    Key
    75  	nextAction action
    76  }
    77  
    78  // NewEditor creates an Editor.
    79  func NewEditor(file *os.File, sigs chan os.Signal, ev *eval.Evaler, st *store.Store) *Editor {
    80  	seq := -1
    81  	if st != nil {
    82  		var err error
    83  		seq, err = st.NextCmdSeq()
    84  		if err != nil {
    85  			// TODO(xiaq): Also report the error
    86  			seq = -1
    87  		}
    88  	}
    89  
    90  	prompt, rprompt := defaultPrompts()
    91  
    92  	ed := &Editor{
    93  		file:   file,
    94  		writer: newWriter(file),
    95  		reader: NewReader(file),
    96  		sigs:   sigs,
    97  		store:  st,
    98  		evaler: ev,
    99  		cmdSeq: seq,
   100  		ps1:    prompt,
   101  		rps1:   rprompt,
   102  
   103  		abbreviations: make(map[string]string),
   104  	}
   105  	ev.Modules["le"] = makeModule(ed)
   106  	return ed
   107  }
   108  
   109  func (ed *Editor) flash() {
   110  	// TODO implement fish-like flash effect
   111  }
   112  
   113  func (ed *Editor) addTip(format string, args ...interface{}) {
   114  	ed.tips = append(ed.tips, fmt.Sprintf(format, args...))
   115  }
   116  
   117  func (ed *Editor) notify(format string, args ...interface{}) {
   118  	ed.notifications = append(ed.notifications, fmt.Sprintf(format, args...))
   119  }
   120  
   121  func (ed *Editor) refresh(fullRefresh bool, tips bool) error {
   122  	// Re-lex the line, unless we are in modeCompletion
   123  	src := ed.line
   124  	if ed.mode.Mode() != modeCompletion {
   125  		n, err := parse.Parse(src)
   126  		ed.parseErrorAtEnd = err != nil && atEnd(err, len(src))
   127  		if err != nil {
   128  			// If all the errors happen at the end, it is liekly complaining
   129  			// about missing texts that will eventually be inserted. Don't show
   130  			// such errors.
   131  			// XXX We may need a more reliable criteria.
   132  			if tips && !ed.parseErrorAtEnd {
   133  				ed.addTip("parser error: %s", err)
   134  			}
   135  		}
   136  		if n == nil {
   137  			ed.tokens = []Token{{ParserError, src, nil, ""}}
   138  		} else {
   139  			ed.tokens = tokenize(src, n)
   140  			_, err := ed.evaler.Compile(n)
   141  			if err != nil {
   142  				if tips && !atEnd(err, len(src)) {
   143  					ed.addTip("compiler error: %s", err)
   144  				}
   145  				if err, ok := err.(*util.PosError); ok {
   146  					p := err.Begin
   147  					for i, token := range ed.tokens {
   148  						if token.Node.Begin() <= p && p < token.Node.End() {
   149  							ed.tokens[i].addStyle(styleForCompilerError)
   150  							break
   151  						}
   152  					}
   153  				}
   154  			}
   155  		}
   156  		// Apply each stylist on each token.
   157  		for i, t := range ed.tokens {
   158  			for _, stylist := range stylists {
   159  				ed.tokens[i].addStyle(stylist(t.Node, ed))
   160  			}
   161  		}
   162  	}
   163  	return ed.writer.refresh(&ed.editorState, fullRefresh)
   164  }
   165  
   166  func atEnd(e error, n int) bool {
   167  	switch e := e.(type) {
   168  	case *util.PosError:
   169  		return e.Begin == n
   170  	case *util.Errors:
   171  		for _, child := range e.Errors {
   172  			if !atEnd(child, n) {
   173  				return false
   174  			}
   175  		}
   176  		return true
   177  	default:
   178  		return false
   179  	}
   180  }
   181  
   182  // insertAtDot inserts text at the dot and moves the dot after it.
   183  func (ed *Editor) insertAtDot(text string) {
   184  	ed.line = ed.line[:ed.dot] + text + ed.line[ed.dot:]
   185  	ed.dot += len(text)
   186  }
   187  
   188  func setupTerminal(file *os.File) (*sys.Termios, error) {
   189  	fd := int(file.Fd())
   190  	term, err := sys.NewTermiosFromFd(fd)
   191  	if err != nil {
   192  		return nil, fmt.Errorf("can't get terminal attribute: %s", err)
   193  	}
   194  
   195  	savedTermios := term.Copy()
   196  
   197  	term.SetICanon(false)
   198  	term.SetEcho(false)
   199  	term.SetVMin(1)
   200  	term.SetVTime(0)
   201  
   202  	err = term.ApplyToFd(fd)
   203  	if err != nil {
   204  		return nil, fmt.Errorf("can't set up terminal attribute: %s", err)
   205  	}
   206  
   207  	/*
   208  		err = sys.FlushInput(fd)
   209  		if err != nil {
   210  			return nil, fmt.Errorf("can't flush input: %s", err)
   211  		}
   212  	*/
   213  
   214  	return savedTermios, nil
   215  }
   216  
   217  // startReadLine prepares the terminal for the editor.
   218  func (ed *Editor) startReadLine() error {
   219  	savedTermios, err := setupTerminal(ed.file)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	ed.savedTermios = savedTermios
   224  
   225  	_, width := sys.GetWinsize(int(ed.file.Fd()))
   226  	// Turn on autowrap, write lackEOL along with enough padding to fill the
   227  	// whole screen. If the cursor was in the first column, we end up in the
   228  	// same line (just off the line boundary); otherwise we are now in the next
   229  	// line. We now rewind to the first column and erase anything there. The
   230  	// final effect is that a lackEOL gets written if and only if the cursor
   231  	// was not in the first column.
   232  	fmt.Fprintf(ed.file, "\033[?7h%s%*s\r \r", lackEOL, width-WcWidth(lackEOLRune), "")
   233  
   234  	// Turn off autowrap. The edito has its own wrapping mechanism. Doing
   235  	// wrapping manually means that when the actual width of some characters
   236  	// are greater than what our wcwidth implementation tells us, characters at
   237  	// the end of that line gets hidden -- compared to pushed to the next line,
   238  	// which is more disastrous.
   239  	ed.file.WriteString("\033[?7l")
   240  	// Turn on SGR-style mouse tracking.
   241  	//ed.file.WriteString("\033[?1000;1006h")
   242  
   243  	// Enable bracketed paste.
   244  	ed.file.WriteString("\033[?2004h")
   245  
   246  	return nil
   247  }
   248  
   249  // finishReadLine puts the terminal in a state suitable for other programs to
   250  // use.
   251  func (ed *Editor) finishReadLine(addError func(error)) {
   252  	ed.mode = &ed.insert
   253  	ed.tips = nil
   254  	ed.dot = len(ed.line)
   255  	// TODO Perhaps make it optional to NOT clear the rprompt
   256  	ed.rprompt = ""
   257  	addError(ed.refresh(false, false))
   258  	ed.file.WriteString("\n")
   259  
   260  	// ed.reader.Stop()
   261  	ed.reader.Quit()
   262  
   263  	// Turn on autowrap.
   264  	ed.file.WriteString("\033[?7h")
   265  	// Turn off mouse tracking.
   266  	//ed.file.WriteString("\033[?1000;1006l")
   267  
   268  	// Disable bracketed paste.
   269  	ed.file.WriteString("\033[?2004l")
   270  
   271  	// restore termios
   272  	err := ed.savedTermios.ApplyToFd(int(ed.file.Fd()))
   273  
   274  	if err != nil {
   275  		addError(fmt.Errorf("can't restore terminal attribute: %s", err))
   276  	}
   277  	ed.savedTermios = nil
   278  	ed.editorState = editorState{}
   279  }
   280  
   281  // ReadLine reads a line interactively.
   282  func (ed *Editor) ReadLine() (line string, err error) {
   283  	ed.editorState = editorState{active: true}
   284  	ed.mode = &ed.insert
   285  
   286  	isExternalCh := make(chan map[string]bool, 1)
   287  	go getIsExternal(ed.evaler, isExternalCh)
   288  
   289  	ed.writer.resetOldBuf()
   290  	go ed.reader.Run()
   291  
   292  	e := ed.startReadLine()
   293  	if e != nil {
   294  		return "", e
   295  	}
   296  	defer ed.finishReadLine(func(e error) {
   297  		if e != nil {
   298  			err = util.CatError(err, e)
   299  		}
   300  	})
   301  
   302  	fullRefresh := false
   303  MainLoop:
   304  	for {
   305  		ed.prompt = ed.ps1.Call(ed)
   306  		ed.rprompt = ed.rps1.Call(ed)
   307  
   308  		err := ed.refresh(fullRefresh, true)
   309  		fullRefresh = false
   310  		if err != nil {
   311  			return "", err
   312  		}
   313  
   314  		ed.tips = nil
   315  
   316  		select {
   317  		case m := <-isExternalCh:
   318  			ed.isExternal = m
   319  		case sig := <-ed.sigs:
   320  			// TODO(xiaq): Maybe support customizable handling of signals
   321  			switch sig {
   322  			case syscall.SIGINT:
   323  				// Start over
   324  				ed.editorState = editorState{
   325  					savedTermios: ed.savedTermios,
   326  					isExternal:   ed.isExternal,
   327  				}
   328  				ed.mode = &ed.insert
   329  				goto MainLoop
   330  			case syscall.SIGWINCH:
   331  				fullRefresh = true
   332  				continue MainLoop
   333  			case syscall.SIGCHLD:
   334  				// ignore
   335  			default:
   336  				ed.addTip("ignored signal %s", sig)
   337  			}
   338  		case err := <-ed.reader.ErrorChan():
   339  			ed.notify("reader error: %s", err.Error())
   340  		case mouse := <-ed.reader.MouseChan():
   341  			ed.addTip("mouse: %+v", mouse)
   342  		case <-ed.reader.CPRChan():
   343  			// Ignore CPR
   344  		case b := <-ed.reader.PasteChan():
   345  			if !b {
   346  				continue
   347  			}
   348  			var buf bytes.Buffer
   349  			timer := time.NewTimer(EscSequenceTimeout)
   350  		paste:
   351  			for {
   352  				// XXX Should also select on other chans. However those chans
   353  				// will be unified (agina) into one later so we don't do
   354  				// busywork here.
   355  				select {
   356  				case k := <-ed.reader.KeyChan():
   357  					if k.Mod != 0 {
   358  						ed.notify("function key within paste")
   359  						break paste
   360  					}
   361  					buf.WriteRune(k.Rune)
   362  					timer.Reset(EscSequenceTimeout)
   363  				case b := <-ed.reader.PasteChan():
   364  					if !b {
   365  						break paste
   366  					}
   367  				case <-timer.C:
   368  					ed.notify("bracketed paste timeout")
   369  					break paste
   370  				}
   371  			}
   372  			topaste := buf.String()
   373  			if ed.insert.quotePaste {
   374  				topaste = parse.Quote(topaste)
   375  			}
   376  			ed.insertAtDot(topaste)
   377  		case k := <-ed.reader.KeyChan():
   378  		lookupKey:
   379  			keyBinding, ok := keyBindings[ed.mode.Mode()]
   380  			if !ok {
   381  				ed.addTip("No binding for current mode")
   382  				continue
   383  			}
   384  
   385  			fn, bound := keyBinding[k]
   386  			if !bound {
   387  				fn = keyBinding[Default]
   388  			}
   389  
   390  			ed.insert.insertedLiteral = false
   391  			ed.lastKey = k
   392  			fn.Call(ed)
   393  			if ed.insert.insertedLiteral {
   394  				ed.insert.literalInserts++
   395  			} else {
   396  				ed.insert.literalInserts = 0
   397  			}
   398  			act := ed.nextAction
   399  			ed.nextAction = action{}
   400  
   401  			switch act.typ {
   402  			case noAction:
   403  				continue
   404  			case reprocessKey:
   405  				err = ed.refresh(false, true)
   406  				if err != nil {
   407  					return "", err
   408  				}
   409  				goto lookupKey
   410  			case exitReadLine:
   411  				if act.returnErr == nil && act.returnLine != "" {
   412  					ed.appendHistory(act.returnLine)
   413  				}
   414  				return act.returnLine, act.returnErr
   415  			}
   416  		}
   417  	}
   418  }
   419  
   420  // getIsExternal finds a set of all external commands and puts it on the result
   421  // channel.
   422  func getIsExternal(ev *eval.Evaler, result chan<- map[string]bool) {
   423  	names := make(chan string, 32)
   424  	go func() {
   425  		ev.AllExecutables(names)
   426  		close(names)
   427  	}()
   428  	isExternal := make(map[string]bool)
   429  	for name := range names {
   430  		isExternal[name] = true
   431  	}
   432  	result <- isExternal
   433  }