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 }