github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/keyloop.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "path/filepath" 7 "strings" 8 "time" 9 "unicode" 10 11 "github.com/xyproto/clip" 12 "github.com/xyproto/digraph" 13 "github.com/xyproto/env/v2" 14 "github.com/xyproto/iferr" 15 "github.com/xyproto/mode" 16 "github.com/xyproto/syntax" 17 "github.com/xyproto/vt100" 18 ) 19 20 // For when the user scrolls too far 21 const endOfFileMessage = "EOF" 22 23 // Create a LockKeeper for keeping track of which files are being edited 24 var fileLock = NewLockKeeper(defaultLockFile) 25 26 // Loop will set up and run the main loop of the editor 27 // a *vt100.TTY struct 28 // fnord contains either data or a filename to open 29 // a LineNumber (may be 0 or -1) 30 // a forceFlag for if the file should be force opened 31 // If an error and "true" is returned, it is a quit message to the user, and not an error. 32 // If an error and "false" is returned, it is an error. 33 func Loop(tty *vt100.TTY, fnord FilenameOrData, lineNumber LineNumber, colNumber ColNumber, forceFlag bool, theme Theme, syntaxHighlight, monitorAndReadOnly, nanoMode, manPageMode, createDirectoriesIfMissing, displayQuickHelp bool) (userMessage string, stopParent bool, err error) { 34 35 // Create a Canvas for drawing onto the terminal 36 vt100.Init() 37 c := vt100.NewCanvas() 38 c.ShowCursor() 39 vt100.EchoOff() 40 41 var ( 42 statusDuration = 2700 * time.Millisecond 43 44 copyLines []string // for the cut/copy/paste functionality 45 previousCopyLines []string // for checking if a paste is the same as last time 46 bookmark *Position // for the bookmark/jump functionality 47 48 firstPasteAction = true 49 firstCopyAction = true 50 51 lastCopyY LineIndex = -1 // used for keeping track if ctrl-c has been pressed twice on the same line 52 lastPasteY LineIndex = -1 // used for keeping track if ctrl-v has been pressed twice on the same line 53 lastCutY LineIndex = -1 // used for keeping track if ctrl-x has been pressed twice on the same line 54 55 clearKeyHistory bool // for clearing the last pressed key, for exiting modes that also reads keys 56 kh = NewKeyHistory() // keep track of the previous key presses 57 key string // for the main loop 58 jsonFormatToggle bool // for toggling indentation or not when pressing ctrl-w for JSON 59 60 markdownTableEditorCounter int // the number of times the Markdown table editor has been displayed 61 ) 62 63 // New editor struct. Scroll 10 lines at a time, no word wrap. 64 e, messageAfterRedraw, displayedImage, err := NewEditor(tty, c, fnord, lineNumber, colNumber, theme, syntaxHighlight, true, monitorAndReadOnly, nanoMode, createDirectoriesIfMissing, displayQuickHelp) 65 if err != nil { 66 return "", false, err 67 } else if displayedImage { 68 // A special case for if an image was displayed instead of a file being opened 69 return "", false, nil 70 } 71 72 // Find the absolute path to this filename 73 absFilename := fnord.filename 74 if !fnord.stdin { 75 if filename, err := e.AbsFilename(); err == nil { // success 76 absFilename = filename 77 } 78 } 79 80 if manPageMode { 81 e.mode = mode.ManPage 82 } 83 84 // Minor adjustments to some modes 85 switch e.mode { 86 case mode.Email, mode.Git: 87 e.StatusForeground = vt100.LightBlue 88 e.StatusBackground = vt100.BackgroundDefault 89 case mode.ManPage: 90 e.readOnly = true 91 } 92 93 // Prepare a status bar 94 status := NewStatusBar(e.StatusForeground, e.StatusBackground, e.StatusErrorForeground, e.StatusErrorBackground, e, statusDuration, messageAfterRedraw, e.nanoMode) 95 96 e.SetTheme(e.Theme) 97 98 // ctrl-c, USR1 and terminal resize handlers 99 e.SetUpSignalHandlers(c, tty, status) 100 101 // Monitor a read-only file? 102 if monitorAndReadOnly { 103 e.readOnly = true 104 e.StartMonitoring(c, tty, status) 105 } 106 if e.mode == mode.Log && e.readOnly { 107 e.syntaxHighlight = true 108 } 109 110 e.previousX = 1 111 e.previousY = 1 112 113 tty.SetTimeout(2 * time.Millisecond) 114 115 var ( 116 canUseLocks = !fnord.stdin && !monitorAndReadOnly 117 lockTimestamp time.Time 118 ) 119 120 if canUseLocks { 121 122 // If the lock keeper does not have an overview already, that's fine. Ignore errors from lk.Load(). 123 if err := fileLock.Load(); err != nil { 124 // Could not load an existing lock overview, this might be the first run? Try saving. 125 if err := fileLock.Save(); err != nil { 126 // Could not save a lock overview. Can not use locks. 127 canUseLocks = false 128 } 129 } 130 131 // Check if the lock should be forced (also force when running git commit, because it is likely that o was killed in that case) 132 if forceFlag || filepath.Base(absFilename) == "COMMIT_EDITMSG" || env.Bool("O_FORCE") { 133 // Lock and save, regardless of what the previous status is 134 fileLock.Lock(absFilename) 135 // TODO: If the file was already marked as locked, this is not strictly needed? The timestamp might be modified, though. 136 fileLock.Save() 137 } else { 138 // Lock the current file, if it's not already locked 139 if err := fileLock.Lock(absFilename); err != nil { 140 return fmt.Sprintf("Locked by another (possibly dead) instance of this editor.\nTry: o -f %s", filepath.Base(absFilename)), false, errors.New(absFilename + " is locked") 141 } 142 // Immediately save the lock file as a signal to other instances of the editor 143 fileLock.Save() 144 } 145 lockTimestamp = fileLock.GetTimestamp(absFilename) 146 147 // Set up a catch for panics, so that the current file can be unlocked 148 defer func() { 149 if x := recover(); x != nil { 150 // Unlock and save the lock file 151 fileLock.Unlock(absFilename) 152 fileLock.Save() 153 154 // Save the current file. The assumption is that it's better than not saving, if something crashes. 155 // TODO: Save to a crash file, then let the editor discover this when it starts. 156 157 // Create a suitable error message, depending on if the file is saved or not 158 msg := fmt.Sprintf("Saved the file first!\n%v", x) 159 if err := e.Save(c, tty); err != nil { 160 // Output the error message 161 msg = fmt.Sprintf("Could not save the file first! %v\n%v", err, x) 162 } 163 164 // Output the error message 165 quitMessageWithStack(tty, msg) 166 } 167 }() 168 } 169 170 // Draw everything once, with slightly different behavior if used over ssh 171 e.InitialRedraw(c, status) 172 173 // QuickHelp screen + help for new users 174 if !QuickHelpScreenIsDisabled() || e.displayQuickHelp { 175 e.DrawQuickHelp(c, false) 176 } 177 178 // This is the main loop for the editor 179 for !e.quit { 180 181 if e.macro == nil || (e.playBackMacroCount == 0 && !e.macro.Recording) { 182 // Read the next key in the regular way 183 key = tty.String() 184 undo.IgnoreSnapshots(false) 185 } else { 186 if e.macro.Recording { 187 undo.IgnoreSnapshots(true) 188 // Read and record the next key 189 key = tty.String() 190 if key != "c:20" { // ctrl-t 191 // But never record the macro toggle button 192 e.macro.Add(key) 193 } 194 } else if e.playBackMacroCount > 0 { 195 undo.IgnoreSnapshots(true) 196 key = e.macro.Next() 197 if key == "" || key == "c:20" { // ctrl-t 198 e.macro.Home() 199 e.playBackMacroCount-- 200 // No more macro keys. Read the next key. 201 key = tty.String() 202 } 203 } 204 } 205 206 switch key { 207 case "c:17": // ctrl-q, quit 208 209 if e.nanoMode { // nano: ctrl-w, search backwards 210 const clearPreviousSearch = true 211 const searchForward = false 212 e.SearchMode(c, status, tty, clearPreviousSearch, searchForward, undo) 213 break 214 } 215 216 e.quit = true 217 case "c:23": // ctrl-w, format or insert template (or if in git mode, cycle interactive rebase keywords) 218 219 if e.nanoMode { // nano: ctrl-w, search 220 const clearPreviousSearch = true 221 const searchForward = true 222 e.SearchMode(c, status, tty, clearPreviousSearch, searchForward, undo) 223 break 224 } 225 226 undo.Snapshot(e) 227 228 // Clear the search term 229 e.ClearSearch() 230 231 // First check if we are editing Markdown and are in a Markdown table (and that this is not the previous thing that we did) 232 if e.mode == mode.Markdown && e.InTable() && !kh.PrevIs("c:23") { 233 e.GoToStartOfTextLine(c) 234 // Just format the Markdown table 235 const justFormat = true 236 const displayQuickHelp = false 237 e.EditMarkdownTable(tty, c, status, bookmark, justFormat, displayQuickHelp) 238 break 239 } 240 241 // Add a watch 242 if e.debugMode { // AddWatch will start a new gdb session if needed 243 // Ask the user to type in a watch expression 244 if expression, ok := e.UserInput(c, tty, status, "Variable name to watch", "", []string{}, false, ""); ok { 245 if _, err := e.AddWatch(expression); err != nil { 246 status.ClearAll(c) 247 status.SetError(err) 248 status.ShowNoTimeout(c, e) 249 break 250 } 251 } 252 break 253 } 254 255 // Cycle git rebase keywords 256 if line := e.CurrentLine(); e.mode == mode.Git && hasAnyPrefixWord(line, gitRebasePrefixes) { 257 newLine := nextGitRebaseKeyword(line) 258 e.SetCurrentLine(newLine) 259 e.redraw = true 260 e.redrawCursor = true 261 break 262 } 263 264 if e.Empty() { 265 // Empty file, nothing to format, insert a program template, if available 266 if err := e.InsertTemplateProgram(c); err != nil { 267 status.ClearAll(c) 268 status.SetMessage("nothing to format and no template available") 269 status.Show(c, e) 270 } else { 271 e.redraw = true 272 e.redrawCursor = true 273 } 274 break 275 } 276 277 status.ClearAll(c) 278 e.formatCode(c, tty, status, &jsonFormatToggle) 279 280 // Move the cursor if after the end of the line 281 if e.AtOrAfterEndOfLine() { 282 e.End(c) 283 } 284 285 // Keep the message on screen for 1 second, despite e.redraw being set. 286 // This is only to have a minimum amount of display time for the message. 287 status.HoldMessage(c, 250*time.Millisecond) 288 289 case "c:6": // ctrl-f, search for a string 290 291 if e.nanoMode { // nano: ctrl-f, cursor forward 292 e.CursorForward(c, status) 293 break 294 } 295 296 // If in Debug mode, let ctrl-f mean "finish" 297 if e.debugMode { 298 if e.gdb == nil { // success 299 status.SetMessageAfterRedraw("Not running") 300 break 301 } 302 status.ClearAll(c) 303 if err := e.DebugFinish(); err != nil { 304 e.DebugEnd() 305 status.SetMessage(err.Error()) 306 e.GoToEnd(c, nil) 307 } else { 308 status.SetMessage("Finish") 309 } 310 status.SetMessageAfterRedraw(status.Message()) 311 break 312 } 313 314 const clearPreviousSearch = true 315 const searchForward = true 316 e.SearchMode(c, status, tty, clearPreviousSearch, searchForward, undo) 317 318 case "c:0": // ctrl-space, build source code to executable, or export, depending on the mode 319 if e.nanoMode { 320 break // do nothing 321 } 322 323 switch e.mode { 324 case mode.ManPage, mode.Config: 325 break // do nothing 326 case mode.Markdown: 327 e.ToggleCheckboxCurrentLine() 328 default: 329 // Then build, and run if ctrl-space was double-tapped 330 var alsoRun = kh.DoubleTapped("c:0") 331 const markdownDoubleSpacePrevention = true 332 e.Build(c, status, tty, alsoRun, markdownDoubleSpacePrevention) 333 e.redrawCursor = true 334 } 335 336 case "c:20": // ctrl-t 337 338 // for C or C++: jump to header/source, or insert symbol 339 // for Agda: insert symbol 340 // for the rest: record and play back macros 341 // debug mode: next instruction 342 343 // Save the current file, but only if it has changed 344 if e.changed && !e.nanoMode { 345 if err := e.Save(c, tty); err != nil { 346 status.ClearAll(c) 347 status.SetError(err) 348 status.Show(c, e) 349 break 350 } 351 } 352 353 if e.nanoMode { 354 e.NanoNextTypo(c, status) 355 break 356 } 357 358 e.redrawCursor = true 359 360 // Is there no corresponding header or source file? 361 noCorresponding := false 362 AGAIN_NO_CORRESPONDING: 363 364 // First check if we are editing Markdown and are in a Markdown table 365 if e.mode == mode.Markdown && (e.EmptyLine() || e.InTable()) { // table editor 366 if e.EmptyLine() { 367 e.InsertStringAndMove(c, "| | |\n|-|-|\n| | |\n") 368 e.Up(c, status) 369 } 370 undo.Snapshot(e) 371 e.GoToStartOfTextLine(c) 372 // Edit the Markdown table 373 const justFormat = false 374 var displayQuickHelp = markdownTableEditorCounter < 1 375 e.EditMarkdownTable(tty, c, status, bookmark, justFormat, displayQuickHelp) 376 markdownTableEditorCounter++ 377 // Full redraw 378 const drawLines = true 379 e.FullResetRedraw(c, status, drawLines) 380 e.redraw = true 381 e.redrawCursor = true 382 } else if !noCorresponding && (e.mode == mode.C || e.mode == mode.Cpp || e.mode == mode.ObjC) && hasS([]string{".cpp", ".cc", ".c", ".cxx", ".c++", ".m", ".mm", ".M"}, filepath.Ext(e.filename)) { // jump from source to header file 383 // If this is a C++ source file, try finding and opening the corresponding header file 384 // Check if there is a corresponding header file 385 if absFilename, err := e.AbsFilename(); err == nil { // no error 386 headerExtensions := []string{".h", ".hpp", ".h++"} 387 if headerFilename, err := ExtFileSearch(absFilename, headerExtensions, fileSearchMaxTime); err == nil && headerFilename != "" { // no error 388 // Switch to another file (without forcing it) 389 e.Switch(c, tty, status, fileLock, headerFilename) 390 break 391 } 392 } 393 noCorresponding = true 394 goto AGAIN_NO_CORRESPONDING 395 } else if !noCorresponding && (e.mode == mode.C || e.mode == mode.Cpp || e.mode == mode.ObjC) && hasS([]string{".h", ".hpp", ".h++"}, filepath.Ext(e.filename)) { // jump from header to source file 396 // If this is a header file, present a menu option for open the corresponding source file 397 // Check if there is a corresponding header file 398 if absFilename, err := e.AbsFilename(); err == nil { // no error 399 sourceExtensions := []string{".c", ".cpp", ".cxx", ".cc", ".c++"} 400 if headerFilename, err := ExtFileSearch(absFilename, sourceExtensions, fileSearchMaxTime); err == nil && headerFilename != "" { // no error 401 // Switch to another file (without forcing it) 402 e.Switch(c, tty, status, fileLock, headerFilename) 403 break 404 } 405 } 406 noCorresponding = true 407 goto AGAIN_NO_CORRESPONDING 408 } else if e.mode == mode.Agda || e.mode == mode.Ivy { // insert symbol 409 var ( 410 menuChoices [][]string 411 selectedSymbol string 412 ) 413 if e.mode == mode.Agda { 414 menuChoices = agdaSymbols 415 selectedSymbol = "¤" 416 } else if e.mode == mode.Ivy { 417 menuChoices = ivySymbols 418 selectedSymbol = "×" 419 } 420 e.redraw = true 421 selectedX, selectedY, cancel := e.SymbolMenu(tty, status, "Insert symbol", menuChoices, e.MenuTitleColor, e.MenuTextColor, e.MenuArrowColor) 422 if !cancel { 423 undo.Snapshot(e) 424 if selectedY < len(menuChoices) { 425 row := menuChoices[selectedY] 426 if selectedX < len(row) { 427 selectedSymbol = menuChoices[selectedY][selectedX] 428 } 429 } 430 e.InsertString(c, selectedSymbol) 431 } 432 // Full redraw 433 const drawLines = true 434 e.FullResetRedraw(c, status, drawLines) 435 e.redraw = true 436 e.redrawCursor = true 437 } else if e.macro == nil { 438 // Start recording a macro, then stop the recording when ctrl-t is pressed again, 439 // then ask for the number of repetitions to play it back when it's pressed after that, 440 // then clear the macro when esc is pressed. 441 undo.Snapshot(e) 442 undo.IgnoreSnapshots(true) 443 status.Clear(c) 444 status.SetMessage("Recording macro") 445 status.Show(c, e) 446 e.macro = NewMacro() 447 e.macro.Recording = true 448 e.playBackMacroCount = 0 449 } else if e.macro.Recording { // && e.macro != nil 450 e.macro.Recording = false 451 undo.IgnoreSnapshots(true) 452 e.playBackMacroCount = 0 453 status.Clear(c) 454 if macroLen := e.macro.Len(); macroLen == 0 { 455 status.SetMessage("Stopped recording") 456 e.macro = nil 457 } else if macroLen < 10 { 458 status.SetMessage("Recorded " + strings.Join(e.macro.KeyPresses, " ")) 459 } else { 460 status.SetMessage(fmt.Sprintf("Recorded %d steps", macroLen)) 461 } 462 status.Show(c, e) 463 } else if e.playBackMacroCount > 0 { 464 undo.IgnoreSnapshots(false) 465 status.Clear(c) 466 status.SetMessage("Stopped macro") // stop macro playback 467 status.Show(c, e) 468 e.playBackMacroCount = 0 469 e.macro.Home() 470 } else { // && e.macro != nil && e.playBackMacroCount == 0 // start macro playback 471 undo.IgnoreSnapshots(false) 472 undo.Snapshot(e) 473 status.ClearAll(c) 474 // Play back the macro, once 475 e.playBackMacroCount = 1 476 } 477 case "c:28": // ctrl-\, toggle comment for this block 478 undo.Snapshot(e) 479 e.ToggleCommentBlock(c) 480 e.redraw = true 481 e.redrawCursor = true 482 case "c:15": // ctrl-o, launch the command menu 483 484 if e.nanoMode { // ctrl-o, save 485 // Ask the user which filename to save to 486 if newFilename, ok := e.UserInput(c, tty, status, "Save as", e.filename, []string{e.filename}, false, e.filename); ok { 487 e.filename = newFilename 488 e.Save(c, tty) 489 e.Switch(c, tty, status, fileLock, newFilename) 490 } else { 491 status.Clear(c) 492 status.SetMessage("Saved nothing") 493 status.Show(c, e) 494 } 495 break 496 } 497 498 status.ClearAll(c) 499 undo.Snapshot(e) 500 undoBackup := undo 501 lastCommandMenuIndex = e.CommandMenu(c, tty, status, bookmark, undo, lastCommandMenuIndex, forceFlag, fileLock) 502 undo = undoBackup 503 case "c:31": // ctrl-_, jump to a matching parenthesis or enter a digraph 504 505 if e.nanoMode { // nano: ctrl-/ 506 // go to line 507 e.JumpMode(c, status, tty) 508 e.redraw = true 509 e.redrawCursor = true 510 break 511 } 512 513 // First check if we can jump to the matching paren or bracket 514 if e.OnParenOrBracket() && e.JumpToMatching(c) { 515 break 516 } 517 518 // Ask the user to type in a digraph 519 const tabInputText = "ae" 520 if digraphString, ok := e.UserInput(c, tty, status, "Type in a 2-letter digraph", "", digraph.All(), false, tabInputText); ok { 521 if r, ok := digraph.Lookup(digraphString); !ok { 522 status.ClearAll(c) 523 status.SetErrorMessage("Could not find the " + digraphString + " digraph") 524 status.ShowNoTimeout(c, e) 525 } else { 526 undo.Snapshot(e) 527 // Insert the found rune 528 wrapped := e.InsertRune(c, r) 529 if !wrapped { 530 e.WriteRune(c) 531 // Move to the next position 532 e.Next(c) 533 } 534 e.redraw = true 535 } 536 } 537 538 case "←": // left arrow 539 540 // Don't move if ChatGPT is currently generating tokens that are being inserted 541 if e.generatingTokens { 542 break 543 } 544 545 // Check if it's a special case 546 if kh.SpecialArrowKeypressWith("←") { 547 // TODO: Instead of moving up twice, play back the reverse of the latest keypress history 548 e.Up(c, status) 549 e.Up(c, status) 550 // Ask the user for a command and run it 551 e.CommandPrompt(c, tty, status, bookmark, undo) 552 // It's important to reset the key history after hitting this combo 553 clearKeyHistory = true 554 break 555 } 556 557 e.CursorBackward(c, status) 558 559 case "→": // right arrow 560 561 // Don't move if ChatGPT is currently generating tokens that are being inserted 562 if e.generatingTokens { 563 break 564 } 565 566 // Check if it's a special case 567 if kh.SpecialArrowKeypressWith("→") { 568 // TODO: Instead of moving up twice, play back the reverse of the latest keypress history 569 e.Up(c, status) 570 e.Up(c, status) 571 // Ask the user for a command and run it 572 e.CommandPrompt(c, tty, status, bookmark, undo) 573 // It's important to reset the key history after hitting this combo 574 clearKeyHistory = true 575 break 576 } 577 578 e.CursorForward(c, status) 579 580 case "c:16": // ctrl-p, scroll up or jump to the previous match, using the sticky search term. In debug mode, change the pane layout. 581 582 if !e.nanoMode { 583 if e.debugMode { 584 // e.showRegisters has three states, 0 (SmallRegisterWindow), 1 (LargeRegisterWindow) and 2 (NoRegisterWindow) 585 e.debugShowRegisters++ 586 if e.debugShowRegisters > noRegisterWindow { 587 e.debugShowRegisters = smallRegisterWindow 588 } 589 break 590 } 591 e.UseStickySearchTerm() 592 if e.SearchTerm() != "" { 593 // Go to previous match 594 wrap := true 595 forward := false 596 if err := e.GoToNextMatch(c, status, wrap, forward); err == errNoSearchMatch { 597 status.ClearAll(c) 598 msg := e.SearchTerm() + " not found" 599 if e.spellCheckMode { 600 msg = "No typos found" 601 } 602 if !wrap { 603 msg += " from here" 604 } 605 status.SetMessageAfterRedraw(msg) 606 e.spellCheckMode = false 607 e.ClearSearch() 608 } 609 } else { 610 e.redraw = e.ScrollUp(c, status, e.pos.scrollSpeed) 611 e.redrawCursor = true 612 if e.AfterLineScreenContents() { 613 e.End(c) 614 } 615 } 616 e.drawProgress = true 617 break 618 } 619 620 // nano mode 621 622 e.UseStickySearchTerm() 623 if e.SearchTerm() != "" { 624 // Go to previous match 625 wrap := true 626 forward := false 627 if err := e.GoToNextMatch(c, status, wrap, forward); err == errNoSearchMatch { 628 status.ClearAll(c) 629 msg := e.SearchTerm() + " not found" 630 if e.spellCheckMode { 631 msg = "No typos found" 632 } 633 if !wrap { 634 msg += " from here" 635 } 636 status.SetMessageAfterRedraw(msg) 637 e.spellCheckMode = false 638 e.ClearSearch() 639 } 640 break 641 } 642 643 fallthrough // ctrl-p in nano mode 644 645 case "↑": // up arrow 646 647 // Don't move if ChatGPT is currently generating tokens that are being inserted 648 if e.generatingTokens { 649 break 650 } 651 652 // Check if it's a special case 653 if kh.SpecialArrowKeypressWith("↑") { 654 // Ask the user for a command and run it 655 e.CommandPrompt(c, tty, status, bookmark, undo) 656 // It's important to reset the key history after hitting this combo 657 clearKeyHistory = true 658 break 659 } 660 661 if e.DataY() > 0 { 662 // Move the position up in the current screen 663 if e.UpEnd(c) != nil { 664 // If below the top, scroll the contents up 665 if e.DataY() > 0 { 666 e.redraw = e.ScrollUp(c, status, 1) 667 e.pos.Down(c) 668 e.UpEnd(c) 669 } 670 } 671 // If the cursor is after the length of the current line, move it to the end of the current line 672 if e.AfterLineScreenContents() { 673 e.End(c) 674 } 675 } 676 677 // If the cursor is after the length of the current line, move it to the end of the current line 678 if e.AfterLineScreenContents() || e.AfterEndOfLine() { 679 e.End(c) 680 681 // Then, if the rune to the left is '}', move one step to the left 682 if r := e.LeftRune(); r == '}' { 683 e.Prev(c) 684 } 685 686 e.redraw = true 687 } 688 689 e.redrawCursor = true 690 691 case "c:14": // ctrl-n, scroll down or jump to next match, using the sticky search term 692 693 if !e.nanoMode { 694 695 // If in Debug mode, let ctrl-n mean "next instruction" 696 if e.debugMode { 697 if e.gdb != nil { 698 if !programRunning { 699 e.DebugEnd() 700 status.SetMessage("Program stopped") 701 status.SetMessageAfterRedraw(status.Message()) 702 e.redraw = true 703 e.redrawCursor = true 704 break 705 } 706 if err := e.DebugNextInstruction(); err != nil { 707 if errorMessage := err.Error(); strings.Contains(errorMessage, "is not being run") { 708 e.DebugEnd() 709 status.SetMessage("Could not start GDB") 710 } else if err == errProgramStopped { 711 e.DebugEnd() 712 status.SetMessage("Program stopped, could not step") 713 } else { // got an unrecognized error 714 e.DebugEnd() 715 status.SetMessage(errorMessage) 716 } 717 } else { 718 if !programRunning { 719 e.DebugEnd() 720 status.SetMessage("Program stopped when stepping") // Next instruction 721 } else { 722 // Don't show a status message per instruction/step when pressing ctrl-n 723 break 724 } 725 } 726 e.redrawCursor = true 727 status.SetMessageAfterRedraw(status.Message()) 728 break 729 } // e.gdb == nil 730 // Build or export the current file 731 // The last argument is if the command should run in the background or not 732 outputExecutable, err := e.BuildOrExport(c, tty, status, e.filename, e.mode == mode.Markdown) 733 // All clear when it comes to status messages and redrawing 734 status.ClearAll(c) 735 if err != nil && err != errNoSuitableBuildCommand { 736 // Error while building 737 status.SetError(err) 738 status.ShowNoTimeout(c, e) 739 e.debugMode = false 740 e.redrawCursor = true 741 e.redraw = true 742 break 743 } 744 // Was no suitable compilation or export command found? 745 if err == errNoSuitableBuildCommand { 746 // status.ClearAll(c) 747 if e.debugMode { 748 // Both in debug mode and can not find a command to build this file with. 749 status.SetError(err) 750 status.ShowNoTimeout(c, e) 751 e.debugMode = false 752 e.redrawCursor = true 753 e.redraw = true 754 break 755 } 756 // Building this file extension is not implemented yet. 757 // Just display the current time and word count. 758 // TODO: status.ClearAll() should have cleared the status bar first, but this is not always true, 759 // which is why the message is hackily surrounded by spaces. Fix. 760 statsMessage := fmt.Sprintf(" %d words, %s ", e.WordCount(), time.Now().Format("15:04")) // HH:MM 761 status.SetMessage(statsMessage) 762 status.Show(c, e) 763 e.redrawCursor = true 764 break 765 } 766 // Start debugging 767 if err := e.DebugStartSession(c, tty, status, outputExecutable); err != nil { 768 status.ClearAll(c) 769 status.SetError(err) 770 status.ShowNoTimeout(c, e) 771 e.redrawCursor = true 772 } 773 break 774 } 775 e.UseStickySearchTerm() 776 if e.SearchTerm() != "" { 777 // Go to next match 778 wrap := true 779 forward := true 780 if err := e.GoToNextMatch(c, status, wrap, forward); err == errNoSearchMatch { 781 status.ClearAll(c) 782 msg := e.SearchTerm() + " not found" 783 if e.spellCheckMode { 784 msg = "No typos found" 785 } 786 if wrap { 787 status.SetMessage(msg) 788 } else { 789 status.SetMessage(msg + " from here") 790 } 791 status.ShowNoTimeout(c, e) 792 e.spellCheckMode = false 793 e.ClearSearch() 794 } 795 } else { 796 // Scroll down 797 e.redraw = e.ScrollDown(c, status, e.pos.scrollSpeed) 798 // If e.redraw is false, the end of file is reached 799 if !e.redraw { 800 status.Clear(c) 801 status.SetMessage(endOfFileMessage) 802 status.Show(c, e) 803 } 804 e.redrawCursor = true 805 if e.AfterLineScreenContents() { 806 e.End(c) 807 } 808 } 809 e.drawProgress = true 810 break 811 } 812 813 // nano mode: ctrl-n 814 815 e.UseStickySearchTerm() 816 if e.SearchTerm() != "" { 817 // Go to next match 818 wrap := true 819 forward := true 820 if err := e.GoToNextMatch(c, status, wrap, forward); err == errNoSearchMatch { 821 status.Clear(c) 822 msg := e.SearchTerm() + " not found" 823 if e.spellCheckMode { 824 msg = "No typos found" 825 } 826 if wrap { 827 status.SetMessageAfterRedraw(msg) 828 } else { 829 status.SetMessageAfterRedraw(msg + " from here") 830 } 831 e.redraw = true 832 e.spellCheckMode = false 833 e.ClearSearch() 834 } 835 break 836 } 837 838 fallthrough // nano mode: ctrl-n 839 840 case "↓": // down arrow 841 842 // Don't move if ChatGPT is currently generating tokens that are being inserted 843 if e.generatingTokens { 844 break 845 } 846 847 // Check if it's a special case 848 if kh.SpecialArrowKeypressWith("↓") { 849 // Ask the user for a command and run it 850 e.CommandPrompt(c, tty, status, bookmark, undo) 851 // It's important to reset the key history after hitting this combo 852 clearKeyHistory = true 853 break 854 } 855 856 if e.DataY() < LineIndex(e.Len()) { 857 // Move the position down in the current screen 858 if e.DownEnd(c) != nil { 859 // If at the bottom, don't move down, but scroll the contents 860 // Output a helpful message 861 if !e.AfterEndOfDocument() { 862 e.redraw = e.ScrollDown(c, status, 1) 863 e.pos.Up() 864 e.DownEnd(c) 865 } 866 } 867 // If the cursor is after the length of the current line, move it to the end of the current line 868 if e.AfterLineScreenContents() { 869 e.End(c) 870 871 // Then, if the rune to the left is '}', move one step to the left 872 if r := e.LeftRune(); r == '}' { 873 e.Prev(c) 874 } 875 } 876 } 877 878 // If the cursor is after the length of the current line, move it to the end of the current line 879 if e.AfterLineScreenContents() || e.AfterEndOfLine() { 880 e.End(c) 881 e.redraw = true 882 } 883 884 e.redrawCursor = true 885 886 case "c:12": // ctrl-l, go to line number or percentage 887 if !e.nanoMode { 888 e.ClearSearch() // clear the current search first 889 switch e.JumpMode(c, status, tty) { 890 case showHotkeyOverviewAction: 891 const repositionCursorAfterDrawing = true 892 e.DrawHotkeyOverview(tty, c, status, repositionCursorAfterDrawing) 893 e.redraw = true 894 e.redrawCursor = true 895 case launchTutorialAction: 896 const drawLines = true 897 e.FullResetRedraw(c, status, drawLines) 898 LaunchTutorial(tty, c, e, status) 899 e.redraw = true 900 e.redrawCursor = true 901 case scrollUpAction: 902 e.redraw = e.ScrollUp(c, status, e.pos.scrollSpeed) 903 e.redrawCursor = true 904 if e.AfterLineScreenContents() { 905 e.End(c) 906 } 907 e.drawProgress = true 908 case scrollDownAction: 909 e.redraw = e.ScrollDown(c, status, e.pos.scrollSpeed) 910 e.redrawCursor = true 911 if e.AfterLineScreenContents() { 912 e.End(c) 913 } 914 e.drawProgress = true 915 case displayQuickHelpAction: 916 const repositionCursorAfterDrawing = true 917 e.DrawQuickHelp(c, repositionCursorAfterDrawing) 918 e.redraw = false 919 e.redrawCursor = false 920 } 921 break 922 } 923 fallthrough // nano: ctrl-l to refresh 924 case "c:27": // esc, clear search term (but not the sticky search term), reset, clean and redraw 925 // If o is used as a man page viewer, exit at the press of esc 926 if e.mode == mode.ManPage { 927 e.clearOnQuit = false 928 e.quit = true 929 break 930 } 931 // Exit debug mode, if active 932 if e.debugMode { 933 e.DebugEnd() 934 e.debugMode = false 935 status.SetMessageAfterRedraw("Normal mode") 936 e.redraw = true 937 e.redrawCursor = true 938 break 939 } 940 // Stop the call to ChatGPT, if it is running 941 e.generatingTokens = false 942 // Reset the cut/copy/paste double-keypress detection 943 lastCopyY = -1 944 lastPasteY = -1 945 lastCutY = -1 946 // Do a full clear and redraw + clear search term + jump 947 const drawLines = true 948 e.FullResetRedraw(c, status, drawLines) 949 if e.macro != nil || e.playBackMacroCount > 0 { 950 // Stop the playback 951 e.playBackMacroCount = 0 952 // Clear the macro 953 e.macro = nil 954 // Show a message after the redraw 955 status.SetMessageAfterRedraw("Macro cleared") 956 } 957 e.redraw = true 958 e.redrawCursor = true 959 case " ": // space 960 // Scroll down if a man page is being viewed, or if the editor is read-only 961 if e.readOnly { 962 // Scroll down at double scroll speed 963 e.redraw = e.ScrollDown(c, status, e.pos.scrollSpeed*2) 964 // If e.redraw is false, the end of file is reached 965 if !e.redraw { 966 status.Clear(c) 967 status.SetMessage(endOfFileMessage) 968 status.Show(c, e) 969 } 970 e.redrawCursor = true 971 if e.AfterLineScreenContents() { 972 e.End(c) 973 } 974 break 975 } 976 977 // Regular behavior, take an undo snapshot and insert a space 978 undo.Snapshot(e) 979 980 // De-indent this line by 1 if the line above starts with "case " and this line is only "case" at this time. 981 if cLikeSwitch(e.mode) && e.TrimmedLine() == "case" && strings.HasPrefix(e.PreviousTrimmedLine(), "case ") { 982 oneIndentation := e.indentation.String() 983 deIndented := strings.Replace(e.CurrentLine(), oneIndentation, "", 1) 984 e.SetCurrentLine(deIndented) 985 e.End(c) 986 } 987 988 // Place a space 989 wrapped := e.InsertRune(c, ' ') 990 if !wrapped { 991 e.WriteRune(c) 992 // Move to the next position 993 e.Next(c) 994 } 995 e.redraw = true 996 997 case "c:13": // return 998 999 // Show a "Read only" status message if a man page is being viewed or if the editor is read-only 1000 // It is an alternative way to quickly check if the file is read-only, 1001 // and space can still be used for scrolling. 1002 if e.readOnly { 1003 status.Clear(c) 1004 status.SetMessage("Read only") 1005 status.Show(c, e) 1006 break 1007 } 1008 1009 // Regular behavior 1010 1011 // Modify the paste double-keypress detection to allow for a manual return before pasting the rest 1012 if lastPasteY != -1 && kh.Prev() != "c:13" { 1013 lastPasteY++ 1014 } 1015 1016 undo.Snapshot(e) 1017 1018 var ( 1019 lineContents = e.CurrentLine() 1020 1021 trimmedLine = strings.TrimSpace(lineContents) 1022 currentLeadingWhitespace = e.LeadingWhitespace() 1023 // Grab the leading whitespace from the current line, and indent depending on the end of trimmedLine 1024 leadingWhitespace = e.smartIndentation(currentLeadingWhitespace, trimmedLine, false) // the last parameter is "also dedent" 1025 1026 noHome = false 1027 indent = true 1028 ) 1029 1030 // TODO: add and use something like "e.shouldAutoIndent" for these file types 1031 if e.mode == mode.Markdown || e.mode == mode.Text || e.mode == mode.Blank { 1032 indent = false 1033 } 1034 1035 triggerWordsForAI := []string{"Generate", "generate", "Write", "write", "!"} 1036 shouldUseAI := false 1037 1038 if e.AtOrAfterEndOfLine() && e.NextLineIsBlank() { 1039 for _, triggerWord := range triggerWordsForAI { 1040 if e.mode == mode.Markdown && triggerWord == "!" { 1041 continue 1042 } 1043 if strings.HasPrefix(trimmedLine, e.SingleLineCommentMarker()+" "+triggerWord+" ") { 1044 shouldUseAI = true 1045 break 1046 } else if strings.HasPrefix(trimmedLine, e.SingleLineCommentMarker()+triggerWord+" ") { 1047 shouldUseAI = true 1048 break 1049 } else if e.mode != mode.Markdown && e.SingleLineCommentMarker() != "!" && strings.HasPrefix(trimmedLine, "!") { 1050 shouldUseAI = true 1051 break 1052 } 1053 } 1054 } 1055 alreadyUsedAI := false 1056 RETURN_PRESSED_AI_DONE: 1057 1058 if trimmedLine == "private:" || trimmedLine == "protected:" || trimmedLine == "public:" { 1059 // De-indent the current line before moving on to the next 1060 e.SetCurrentLine(trimmedLine) 1061 leadingWhitespace = currentLeadingWhitespace 1062 } else if e.fixAsYouType && openAIKeyHolder != nil && !alreadyUsedAI { 1063 // Fix the code and grammar of the written line, using AI 1064 const disableFixAsYouTypeOnError = true 1065 e.FixCodeOrText(c, status, disableFixAsYouTypeOnError) 1066 alreadyUsedAI = true 1067 e.redrawCursor = true 1068 goto RETURN_PRESSED_AI_DONE 1069 } else if shouldUseAI && openAIKeyHolder != nil { 1070 // Generate code or text, using AI 1071 e.GenerateCodeOrText(c, status, bookmark) 1072 break 1073 } else if cLikeFor(e.mode) { 1074 // Add missing parenthesis for "if ... {", "} else if", "} elif", "for", "while" and "when" for C-like languages 1075 for _, kw := range []string{"for", "foreach", "foreach_reverse", "if", "switch", "when", "while", "while let", "} else if", "} elif"} { 1076 if strings.HasPrefix(trimmedLine, kw+" ") && !strings.HasPrefix(trimmedLine, kw+" (") { 1077 kwLenPlus1 := len(kw) + 1 1078 if kwLenPlus1 < len(trimmedLine) { 1079 if strings.HasSuffix(trimmedLine, " {") && kwLenPlus1 < len(trimmedLine) && len(trimmedLine) > 3 { 1080 // Add ( and ), keep the final "{" 1081 e.SetCurrentLine(currentLeadingWhitespace + kw + " (" + trimmedLine[kwLenPlus1:len(trimmedLine)-2] + ") {") 1082 e.pos.mut.Lock() 1083 e.pos.sx += 2 1084 e.pos.mut.Unlock() 1085 } else if !strings.HasSuffix(trimmedLine, ")") { 1086 // Add ( and ), there is no final "{" 1087 e.SetCurrentLine(currentLeadingWhitespace + kw + " (" + trimmedLine[kwLenPlus1:] + ")") 1088 e.pos.mut.Lock() 1089 e.pos.sx += 2 1090 e.pos.mut.Unlock() 1091 indent = true 1092 leadingWhitespace = e.indentation.String() + currentLeadingWhitespace 1093 } 1094 } 1095 } 1096 } 1097 } else if (e.mode == mode.Go || e.mode == mode.Odin) && trimmedLine == "iferr" { 1098 oneIndentation := e.indentation.String() 1099 // default "if err != nil" block if iferr.IfErr can not find a more suitable one 1100 ifErrBlock := "if err != nil {\n" + oneIndentation + "return nil, err\n" + "}\n" 1101 // search backwards for "func ", return the full contents, the resulting line index and if it was found 1102 contents, functionLineIndex, found := e.ContentsAndReverseSearchPrefix("func ") 1103 if found { 1104 // count the bytes from the start to the end of the "func " line, since this is what iferr.IfErr uses 1105 byteCount := 0 1106 for i := LineIndex(0); i <= functionLineIndex; i++ { 1107 byteCount += len(e.Line(i)) 1108 } 1109 // fetch a suitable "if err != nil" block for the current function signature 1110 if generatedIfErrBlock, err := iferr.IfErr([]byte(contents), byteCount); err == nil { // success 1111 ifErrBlock = generatedIfErrBlock 1112 } 1113 } 1114 // insert the block of text 1115 for i, line := range strings.Split(strings.TrimSpace(ifErrBlock), "\n") { 1116 if i != 0 { 1117 e.InsertLineBelow() 1118 e.pos.sy++ 1119 } 1120 e.SetCurrentLine(currentLeadingWhitespace + line) 1121 } 1122 e.End(c) 1123 } else if (e.mode == mode.XML || e.mode == mode.HTML) && e.expandTags && trimmedLine != "" && !strings.Contains(trimmedLine, "<") && !strings.Contains(trimmedLine, ">") && strings.ToLower(string(trimmedLine[0])) == string(trimmedLine[0]) { 1124 // Words one a line without < or >? Expand into <tag asdf> above and </tag> below. 1125 words := strings.Fields(trimmedLine) 1126 tagName := words[0] // must be at least one word 1127 // the second word after the tag name needs to be ie. x=42 or href=..., 1128 // and the tag name must only contain letters a-z A-Z 1129 if (len(words) == 1 || strings.Contains(words[1], "=")) && onlyAZaz(tagName) { 1130 above := "<" + trimmedLine + ">" 1131 if tagName == "img" && !strings.Contains(trimmedLine, "alt=") && strings.Contains(trimmedLine, "src=") { 1132 // Pick out the image URI from the "src=" declaration 1133 imageURI := "" 1134 for _, word := range strings.Fields(trimmedLine) { 1135 if strings.HasPrefix(word, "src=") { 1136 imageURI = strings.SplitN(word, "=", 2)[1] 1137 imageURI = strings.TrimPrefix(imageURI, "\"") 1138 imageURI = strings.TrimSuffix(imageURI, "\"") 1139 imageURI = strings.TrimPrefix(imageURI, "'") 1140 imageURI = strings.TrimSuffix(imageURI, "'") 1141 break 1142 } 1143 } 1144 // If we got something that looks like and image URI, use the description before "." and capitalize it, 1145 // then use that as the default "alt=" declaration. 1146 if strings.Contains(imageURI, ".") { 1147 imageName := capitalizeWords(strings.TrimSuffix(imageURI, filepath.Ext(imageURI))) 1148 above = "<" + trimmedLine + " alt=\"" + imageName + "\">" 1149 } 1150 } 1151 // Now replace the current line 1152 e.SetCurrentLine(currentLeadingWhitespace + above) 1153 e.End(c) 1154 // And insert a line below 1155 e.InsertLineBelow() 1156 // Then if it's not an img tag, insert the closing tag below the current line 1157 if tagName != "img" { 1158 e.pos.mut.Lock() 1159 e.pos.sy++ 1160 e.pos.mut.Unlock() 1161 below := "</" + tagName + ">" 1162 e.SetCurrentLine(currentLeadingWhitespace + below) 1163 e.pos.mut.Lock() 1164 e.pos.sy-- 1165 e.pos.sx += 2 1166 e.pos.mut.Unlock() 1167 indent = true 1168 leadingWhitespace = e.indentation.String() + currentLeadingWhitespace 1169 } 1170 } 1171 } else if cLikeSwitch(e.mode) { 1172 currentLine := e.CurrentLine() 1173 trimmedLine := e.TrimmedLine() 1174 // De-indent this line by 1 if this line starts with "case " and the next line also starts with "case ", but the current line is indented differently. 1175 currentCaseIndex := strings.Index(trimmedLine, "case ") 1176 nextCaseIndex := strings.Index(e.NextTrimmedLine(), "case ") 1177 if currentCaseIndex != -1 && nextCaseIndex != -1 && strings.Index(currentLine, "case ") != strings.Index(e.NextLine(), "case ") { 1178 oneIndentation := e.indentation.String() 1179 deIndented := strings.Replace(currentLine, oneIndentation, "", 1) 1180 e.SetCurrentLine(deIndented) 1181 e.End(c) 1182 leadingWhitespace = currentLeadingWhitespace 1183 } 1184 } 1185 1186 scrollBack := false 1187 1188 // TODO: Collect the criteria that trigger the same behavior 1189 1190 switch { 1191 case e.AtOrAfterLastLineOfDocument() && (e.AtStartOfTheLine() || e.AtOrBeforeStartOfTextScreenLine()): 1192 e.InsertLineAbove() 1193 noHome = true 1194 case e.AtOrAfterEndOfDocument() && !e.AtStartOfTheLine() && !e.AtOrAfterEndOfLine(): 1195 e.InsertStringAndMove(c, "") 1196 e.InsertLineBelow() 1197 scrollBack = true 1198 case e.AfterEndOfLine(): 1199 e.InsertLineBelow() 1200 scrollBack = true 1201 case !e.AtFirstLineOfDocument() && e.AtOrAfterLastLineOfDocument() && (e.AtStartOfTheLine() || e.AtOrAfterEndOfLine()): 1202 e.InsertStringAndMove(c, "") 1203 scrollBack = true 1204 case e.AtStartOfTheLine(): 1205 e.InsertLineAbove() 1206 noHome = true 1207 default: 1208 // Split the current line in two 1209 if !e.SplitLine() { 1210 e.InsertLineBelow() 1211 } 1212 scrollBack = true 1213 // Indent the next line if at the end, not else 1214 if !e.AfterEndOfLine() { 1215 indent = false 1216 } 1217 } 1218 e.MakeConsistent() 1219 1220 h := int(c.Height()) 1221 if e.pos.sy > (h - 1) { 1222 e.pos.Down(c) 1223 e.redraw = e.ScrollDown(c, status, 1) 1224 e.redrawCursor = true 1225 } else if e.pos.sy == (h - 1) { 1226 e.redraw = e.ScrollDown(c, status, 1) 1227 e.redrawCursor = true 1228 } else { 1229 e.pos.Down(c) 1230 } 1231 1232 if !noHome { 1233 e.pos.mut.Lock() 1234 e.pos.sx = 0 1235 e.pos.mut.Unlock() 1236 // e.Home() 1237 if scrollBack { 1238 e.pos.SetX(c, 0) 1239 } 1240 } 1241 1242 if indent && len(leadingWhitespace) > 0 { 1243 // If the leading whitespace starts with a tab and ends with a space, remove the final space 1244 if strings.HasPrefix(leadingWhitespace, "\t") && strings.HasSuffix(leadingWhitespace, " ") { 1245 leadingWhitespace = leadingWhitespace[:len(leadingWhitespace)-1] 1246 } 1247 if !noHome { 1248 // Insert the same leading whitespace for the new line 1249 e.SetCurrentLine(leadingWhitespace + e.LineContentsFromCursorPosition()) 1250 // Then move to the start of the text 1251 e.GoToStartOfTextLine(c) 1252 } 1253 } 1254 1255 e.SaveX(true) 1256 e.redraw = true 1257 e.redrawCursor = true 1258 case "c:8", "c:127": // ctrl-h or backspace 1259 1260 // Scroll up if a man page is being viewed, or if the editor is read-only 1261 if e.readOnly { 1262 // Scroll up at double speed 1263 e.redraw = e.ScrollUp(c, status, e.pos.scrollSpeed*2) 1264 e.redrawCursor = true 1265 if e.AfterLineScreenContents() { 1266 e.End(c) 1267 } 1268 break 1269 } 1270 1271 // Just clear the search term, if there is an active search 1272 if len(e.SearchTerm()) > 0 { 1273 e.ClearSearch() 1274 e.redraw = true 1275 e.redrawCursor = true 1276 // Don't break, continue to delete to the left after clearing the search, 1277 // since Esc can be used to only clear the search. 1278 // break 1279 } 1280 1281 undo.Snapshot(e) 1282 1283 // Delete the character to the left 1284 if e.EmptyLine() { 1285 e.DeleteCurrentLineMoveBookmark(bookmark) 1286 e.pos.Up() 1287 e.TrimRight(e.DataY()) 1288 e.End(c) 1289 } else if e.AtStartOfTheLine() { // at the start of the screen line, the line may be scrolled 1290 // remove the rest of the current line and move to the last letter of the line above 1291 // before deleting it 1292 if e.DataY() > 0 { 1293 e.pos.Up() 1294 e.TrimRight(e.DataY()) 1295 e.End(c) 1296 e.Delete() 1297 } 1298 } else if (e.EmptyLine() || e.AtStartOfTheLine()) && e.indentation.Spaces && e.indentation.WSLen(e.LeadingWhitespace()) >= e.indentation.PerTab { 1299 // Delete several spaces 1300 for i := 0; i < e.indentation.PerTab; i++ { 1301 // Move back 1302 e.Prev(c) 1303 // Type a blank 1304 e.SetRune(' ') 1305 e.WriteRune(c) 1306 e.Delete() 1307 } 1308 } else { 1309 // Move back 1310 e.Prev(c) 1311 // Type a blank 1312 e.SetRune(' ') 1313 e.WriteRune(c) 1314 if !e.AtOrAfterEndOfLine() { 1315 // Delete the blank 1316 e.Delete() 1317 // scroll left instead of moving the cursor left, if possible 1318 e.pos.mut.Lock() 1319 if e.pos.offsetX > 0 { 1320 e.pos.offsetX-- 1321 e.pos.sx++ 1322 } 1323 e.pos.mut.Unlock() 1324 } 1325 } 1326 1327 e.redrawCursor = true 1328 e.redraw = true 1329 case "c:9": // tab or ctrl-i 1330 1331 if e.spellCheckMode { 1332 // TODO: Save a "custom words" and "ignored words" list to disk 1333 if ignoredWord := e.RemoveCurrentWordFromWordList(); ignoredWord != "" { 1334 typo, corrected := e.NanoNextTypo(c, status) 1335 msg := "Ignored " + ignoredWord 1336 if spellChecker != nil && typo != "" { 1337 msg += ". Found " + typo 1338 if corrected != "" { 1339 msg += " which could be " + corrected + "." 1340 } else { 1341 msg += "." 1342 } 1343 } 1344 status.SetMessageAfterRedraw(msg) 1345 } 1346 break 1347 } 1348 1349 if e.debugMode { 1350 e.debugStepInto = !e.debugStepInto 1351 break 1352 } 1353 1354 y := int(e.DataY()) 1355 r := e.Rune() 1356 leftRune := e.LeftRune() 1357 ext := filepath.Ext(e.filename) 1358 1359 // Tab completion of words for Go 1360 if word := e.LettersBeforeCursor(); e.mode != mode.Blank && e.mode != mode.GoAssembly && e.mode != mode.Assembly && leftRune != '.' && !unicode.IsLetter(r) && len(word) > 0 { 1361 found := false 1362 expandedWord := "" 1363 for kw := range syntax.Keywords { 1364 if len(kw) < 3 { 1365 // skip too short suggestions 1366 continue 1367 } 1368 if strings.HasPrefix(kw, word) { 1369 if !found || (len(kw) < len(expandedWord)) && (len(expandedWord) > 0) { 1370 expandedWord = kw 1371 found = true 1372 } 1373 } 1374 } 1375 1376 // Found a suitable keyword to expand to? Insert the rest of the string. 1377 if found { 1378 toInsert := strings.TrimPrefix(expandedWord, word) 1379 undo.Snapshot(e) 1380 e.redrawCursor = true 1381 e.redraw = true 1382 // Insert the part of expandedWord that comes after the current word 1383 e.InsertStringAndMove(c, toInsert) 1384 break 1385 } 1386 1387 // Tab completion after a '.' 1388 } else if word := e.LettersOrDotBeforeCursor(); e.mode != mode.Blank && e.mode != mode.GoAssembly && e.mode != mode.Assembly && leftRune == '.' && !unicode.IsLetter(r) && len(word) > 0 { 1389 // Now the preceding word before the "." has been found 1390 1391 // Trim the trailing ".", if needed 1392 word = strings.TrimSuffix(strings.TrimSpace(word), ".") 1393 1394 // Grep all files in this directory with the same extension as the currently edited file 1395 // for what could follow the word and a "." 1396 suggestions := corpus(word, "*"+ext) 1397 1398 // Choose a suggestion (tab cycles to the next suggestion) 1399 chosen := e.SuggestMode(c, status, tty, suggestions) 1400 e.redrawCursor = true 1401 e.redraw = true 1402 1403 if chosen != "" { 1404 undo.Snapshot(e) 1405 // Insert the chosen word 1406 e.InsertStringAndMove(c, chosen) 1407 break 1408 } 1409 1410 } 1411 1412 // Enable auto indent if the extension is not "" and either: 1413 // * The mode is set to Go and the position is not at the very start of the line (empty or not) 1414 // * Syntax highlighting is enabled and the cursor is not at the start of the line (or before) 1415 trimmedLine := e.TrimmedLine() 1416 1417 // Check if a line that is more than just a '{', '(', '[' or ':' ends with one of those 1418 endsWithSpecial := len(trimmedLine) > 1 && r == '{' || r == '(' || r == '[' || r == ':' 1419 1420 // Smart indent if: 1421 // * the rune to the left is not a blank character or the line ends with {, (, [ or : 1422 // * and also if it the cursor is not to the very left 1423 // * and also if this is not a text file or a blank file 1424 noSmartIndentation := e.mode == mode.GoAssembly || e.mode == mode.Perl || e.mode == mode.Assembly || e.mode == mode.OCaml || e.mode == mode.StandardML || e.mode == mode.Blank 1425 if (!unicode.IsSpace(leftRune) || endsWithSpecial) && e.pos.sx > 0 && !noSmartIndentation { 1426 lineAbove := 1 1427 if strings.TrimSpace(e.Line(LineIndex(y-lineAbove))) == "" { 1428 // The line above is empty, use the indentation before the line above that 1429 lineAbove-- 1430 } 1431 indexAbove := LineIndex(y - lineAbove) 1432 // If we have a line (one or two lines above) as a reference point for the indentation 1433 if strings.TrimSpace(e.Line(indexAbove)) != "" { 1434 1435 // Move the current indentation to the same as the line above 1436 undo.Snapshot(e) 1437 1438 var ( 1439 spaceAbove = e.LeadingWhitespaceAt(indexAbove) 1440 strippedLineAbove = e.StripSingleLineComment(strings.TrimSpace(e.Line(indexAbove))) 1441 newLeadingSpace string 1442 ) 1443 1444 oneIndentation := e.indentation.String() 1445 1446 // Smart-ish indentation 1447 if !strings.HasPrefix(strippedLineAbove, "switch ") && (strings.HasPrefix(strippedLineAbove, "case ")) || 1448 strings.HasSuffix(strippedLineAbove, "{") || strings.HasSuffix(strippedLineAbove, "[") || 1449 strings.HasSuffix(strippedLineAbove, "(") || strings.HasSuffix(strippedLineAbove, ":") || 1450 strings.HasSuffix(strippedLineAbove, " \\") || 1451 strings.HasPrefix(strippedLineAbove, "if ") { 1452 // Use one more indentation than the line above 1453 newLeadingSpace = spaceAbove + oneIndentation 1454 } else if ((len(spaceAbove) - len(oneIndentation)) > 0) && strings.HasSuffix(trimmedLine, "}") { 1455 // Use one less indentation than the line above 1456 newLeadingSpace = spaceAbove[:len(spaceAbove)-len(oneIndentation)] 1457 } else { 1458 // Use the same indentation as the line above 1459 newLeadingSpace = spaceAbove 1460 } 1461 1462 e.SetCurrentLine(newLeadingSpace + trimmedLine) 1463 if e.AtOrAfterEndOfLine() { 1464 e.End(c) 1465 } 1466 e.redrawCursor = true 1467 e.redraw = true 1468 1469 // job done 1470 break 1471 1472 } 1473 } 1474 1475 undo.Snapshot(e) 1476 if e.indentation.Spaces { 1477 for i := 0; i < e.indentation.PerTab; i++ { 1478 e.InsertRune(c, ' ') 1479 // Write the spaces that represent the tab to the canvas 1480 e.WriteTab(c) 1481 // Move to the next position 1482 e.Next(c) 1483 } 1484 } else { 1485 // Insert a tab character to the file 1486 e.InsertRune(c, '\t') 1487 // Write the spaces that represent the tab to the canvas 1488 e.WriteTab(c) 1489 // Move to the next position 1490 e.Next(c) 1491 } 1492 1493 // Prepare to redraw 1494 e.redrawCursor = true 1495 e.redraw = true 1496 case "c:25": // ctrl-y 1497 1498 if e.nanoMode { // nano: ctrl-y, page up 1499 h := int(c.H()) 1500 e.redraw = e.ScrollUp(c, status, h) 1501 e.redrawCursor = true 1502 if e.AfterLineScreenContents() { 1503 e.End(c) 1504 } 1505 break 1506 } 1507 fallthrough 1508 case "c:1": // ctrl-a, home (or ctrl-y for scrolling up in the st terminal) 1509 1510 if e.spellCheckMode { 1511 if addedWord := e.AddCurrentWordToWordList(); addedWord != "" { 1512 typo, corrected := e.NanoNextTypo(c, status) 1513 msg := "Added " + addedWord 1514 if spellChecker != nil && typo != "" { 1515 msg += ". Found " + typo 1516 if corrected != "" { 1517 msg += " which could be " + corrected + "." 1518 } else { 1519 msg += "." 1520 } 1521 } 1522 status.SetMessageAfterRedraw(msg) 1523 } 1524 break 1525 } 1526 1527 // Do not reset cut/copy/paste status 1528 1529 // First check if we just moved to this line with the arrow keys 1530 justMovedUpOrDown := kh.PrevIs("↓") || kh.PrevIs("↑") 1531 // If at an empty line, go up one line 1532 if !justMovedUpOrDown && e.EmptyRightTrimmedLine() && e.SearchTerm() == "" { 1533 e.Up(c, status) 1534 // e.GoToStartOfTextLine() 1535 e.End(c) 1536 } else if x, err := e.DataX(); err == nil && x == 0 && !justMovedUpOrDown && e.SearchTerm() == "" { 1537 // If at the start of the line, 1538 // go to the end of the previous line 1539 e.Up(c, status) 1540 e.End(c) 1541 } else if e.AtStartOfTextScreenLine() { 1542 // If at the start of the text for this scroll position, go to the start of the line 1543 e.Home() 1544 } else { 1545 // If none of the above, go to the start of the text 1546 e.GoToStartOfTextLine(c) 1547 } 1548 1549 e.redrawCursor = true 1550 e.SaveX(true) 1551 case "c:5": // ctrl-e, end 1552 1553 // Do not reset cut/copy/paste status 1554 1555 // First check if we just moved to this line with the arrow keys, or just cut a line with ctrl-x 1556 justMovedUpOrDown := kh.PrevIs("↓") || kh.PrevIs("↑") || kh.PrevIs("c:24") 1557 if e.AtEndOfDocument() { 1558 e.End(c) 1559 break 1560 } 1561 // If we didn't just move here, and are at the end of the line, 1562 // move down one line and to the end, if not, 1563 // just move to the end. 1564 if !justMovedUpOrDown && e.AfterEndOfLine() && e.SearchTerm() == "" { 1565 e.Down(c, status) 1566 e.Home() 1567 } else { 1568 e.End(c) 1569 } 1570 1571 e.redrawCursor = true 1572 e.SaveX(true) 1573 case "c:4": // ctrl-d, delete 1574 undo.Snapshot(e) 1575 if e.Empty() { 1576 status.SetMessage("Empty") 1577 status.Show(c, e) 1578 } else { 1579 e.Delete() 1580 e.redraw = true 1581 } 1582 e.redrawCursor = true 1583 1584 case "c:29", "c:30": // ctrl-~, jump to matching parenthesis or curly bracket 1585 if e.JumpToMatching(c) { 1586 break 1587 } 1588 status.Clear(c) 1589 status.SetMessage("No matching (, ), [, ], { or }") 1590 status.Show(c, e) 1591 case "c:19": // ctrl-s, save (or step, if in debug mode) 1592 e.UserSave(c, tty, status) 1593 case "c:7": // ctrl-g, either go to definition OR toggle the status bar 1594 1595 if e.nanoMode { // nano: ctrl-g, help 1596 status.ClearAll(c) 1597 const repositionCursorAfterDrawing = true 1598 e.DrawNanoHelp(c, repositionCursorAfterDrawing) 1599 break 1600 } 1601 1602 // If a search is in progress, clear the search first 1603 if e.searchTerm != "" { 1604 e.ClearSearch() 1605 e.redraw = true 1606 e.redrawCursor = true 1607 } 1608 1609 oldFilename := e.filename 1610 oldLineIndex := e.LineIndex() 1611 1612 // func prefix must exist for this language/mode for GoToDefinition to be supported 1613 jumpedToDefinition := e.FuncPrefix() != "" && e.GoToDefinition(tty, c, status) 1614 1615 // If the definition could not be found, toggle the status line at the bottom 1616 if !jumpedToDefinition { 1617 status.ClearAll(c) 1618 e.statusMode = !e.statusMode 1619 if e.statusMode { 1620 status.ShowLineColWordCount(c, e, e.filename) 1621 e.redraw = true 1622 } 1623 } 1624 1625 if !jumpedToDefinition && e.searchTerm != "" && strings.Contains(e.String(), e.searchTerm) { 1626 // Push a function for how to go back 1627 backFunctions = append(backFunctions, func() { 1628 oldFilename := oldFilename 1629 oldLineIndex := oldLineIndex 1630 if e.filename != oldFilename { 1631 // The switch is not strictly needed, since we will probably be in the same file 1632 e.Switch(c, tty, status, fileLock, oldFilename) 1633 } 1634 e.redraw, _ = e.GoTo(oldLineIndex, c, status) 1635 e.ClearSearch() 1636 }) 1637 } 1638 1639 case "c:21": // ctrl-u to undo 1640 1641 if e.nanoMode { // nano: paste after cutting 1642 e.Paste(c, status, ©Lines, &previousCopyLines, &firstPasteAction, &lastCopyY, &lastPasteY, &lastCutY, kh.PrevIs("c:13")) 1643 break 1644 } 1645 1646 fallthrough // undo behavior 1647 case "c:26": // ctrl-z to undo (my also background the application, unfortunately) 1648 1649 // Forget the cut, copy and paste line state 1650 lastCutY = -1 1651 lastPasteY = -1 1652 lastCopyY = -1 1653 1654 // Try to restore the previous editor state in the undo buffer 1655 if err := undo.Restore(e); err == nil { 1656 // c.Draw() 1657 x := e.pos.ScreenX() 1658 y := e.pos.ScreenY() 1659 vt100.SetXY(uint(x), uint(y)) 1660 e.redrawCursor = true 1661 e.redraw = true 1662 } else { 1663 status.SetMessage("Nothing more to undo") 1664 status.Show(c, e) 1665 } 1666 case "c:24": // ctrl-x, cut line 1667 1668 if e.nanoMode { // nano: ctrl-x, quit 1669 1670 if e.changed { 1671 // Ask the user which filename to save to 1672 if newFilename, ok := e.UserInput(c, tty, status, "Write to", e.filename, []string{e.filename}, false, e.filename); ok { 1673 e.filename = newFilename 1674 e.Save(c, tty) 1675 } else { 1676 status.Clear(c) 1677 status.SetMessage("Wrote nothing") 1678 status.Show(c, e) 1679 } 1680 } 1681 1682 e.quit = true 1683 break 1684 } 1685 1686 // Prepare to cut 1687 undo.Snapshot(e) 1688 1689 // First try a single line cut 1690 if y, multilineCut := e.CutSingleLine(status, bookmark, &lastCutY, &lastCopyY, &lastPasteY, ©Lines, &firstCopyAction); multilineCut { // Multi line cut (add to the clipboard, since it's the second press) 1691 lastCutY = y 1692 lastCopyY = -1 1693 lastPasteY = -1 1694 1695 // Also close the portal, if any 1696 e.ClosePortal() 1697 1698 s := e.Block(y) 1699 lines := strings.Split(s, "\n") 1700 if len(lines) == 0 { 1701 // Need at least 1 line to be able to cut "the rest" after the first line has been cut 1702 break 1703 } 1704 copyLines = append(copyLines, lines...) 1705 s = strings.Join(copyLines, "\n") 1706 1707 // Place the block of text in the clipboard 1708 if isDarwin() { 1709 pbcopy(s) 1710 } else { 1711 // Place it in the non-primary clipboard 1712 _ = clip.WriteAll(s, e.primaryClipboard) 1713 } 1714 1715 // Delete the corresponding number of lines 1716 for range lines { 1717 e.DeleteLineMoveBookmark(y, bookmark) 1718 } 1719 1720 // No status message is needed for the cut operation, because it's visible that lines are cut 1721 e.redrawCursor = true 1722 e.redraw = true 1723 } 1724 // Go to the end of the current line 1725 e.End(c) 1726 case "c:11": // ctrl-k, delete to end of line 1727 undo.Snapshot(e) 1728 if e.nanoMode { // nano: ctrl-k, cut line 1729 // Prepare to cut 1730 e.CutSingleLine(status, bookmark, &lastCutY, &lastCopyY, &lastPasteY, ©Lines, &firstCopyAction) 1731 break 1732 } 1733 e.DeleteToEndOfLine(c, status, bookmark, &lastCopyY, &lastPasteY, &lastCutY) 1734 case "c:3": // ctrl-c, copy the stripped contents of the current line 1735 1736 if e.nanoMode { // nano: ctrl-c, report cursor position 1737 status.ClearAll(c) 1738 status.NanoInfo(c, e) 1739 break 1740 } 1741 1742 // ctrl-c might interrupt the program, but saving at the wrong time might be just as destructive. 1743 // e.Save(c, tty) 1744 1745 go func() { 1746 1747 y := e.DataY() 1748 1749 // Forget the cut and paste line state 1750 lastCutY = -1 1751 lastPasteY = -1 1752 1753 // check if this operation is done on the same line as last time 1754 singleLineCopy := lastCopyY != y 1755 lastCopyY = y 1756 1757 // close the portal, if any 1758 closedPortal := e.ClosePortal() == nil 1759 1760 if singleLineCopy { // Single line copy 1761 status.Clear(c) 1762 // Pressed for the first time for this line number 1763 trimmed := strings.TrimSpace(e.Line(y)) 1764 if trimmed != "" { 1765 // Copy the line to the internal clipboard 1766 copyLines = []string{trimmed} 1767 // Copy the line to the clipboard 1768 s := "Copied 1 line" 1769 var err error 1770 if isDarwin() { 1771 err = pbcopy(strings.Join(copyLines, "\n")) 1772 } else { 1773 // Place it in the non-primary clipboard 1774 err = clip.WriteAll(strings.Join(copyLines, "\n"), e.primaryClipboard) 1775 } 1776 if err == nil { // OK 1777 // The copy operation worked out, using the clipboard 1778 s += " to the clipboard" 1779 } 1780 // The portal was closed? 1781 if closedPortal { 1782 s += " and closed the portal" 1783 } 1784 status.SetMessage(s) 1785 status.Show(c, e) 1786 // Go to the end of the line, for easy line duplication with ctrl-c, enter, ctrl-v, 1787 // but only if the copied line is shorter than the terminal width. 1788 if uint(len(trimmed)) < c.Width() { 1789 e.End(c) 1790 } 1791 } 1792 } else { // Multi line copy 1793 // Pressed multiple times for this line number, copy the block of text starting from this line 1794 s := e.Block(y) 1795 if s != "" { 1796 copyLines = strings.Split(s, "\n") 1797 lineCount := strings.Count(s, "\n") 1798 // Prepare a status message 1799 plural := "s" 1800 if lineCount == 1 { 1801 plural = "" 1802 } 1803 // Place the block of text in the clipboard 1804 if isDarwin() { 1805 err = pbcopy(s) 1806 } else { 1807 // Place it in the non-primary clipboard 1808 err = clip.WriteAll(s, e.primaryClipboard) 1809 } 1810 fmtMsg := "Copied %d line%s from %s" 1811 if err != nil { 1812 fmtMsg = "Copied %d line%s from %s to internal buffer" 1813 } 1814 status.SetMessage(fmt.Sprintf(fmtMsg, lineCount, plural, filepath.Base(e.filename))) 1815 status.Show(c, e) 1816 } 1817 } 1818 }() 1819 1820 case "c:22": // ctrl-v, paste 1821 1822 if e.nanoMode { // nano: ctrl-v, page down 1823 h := int(c.H()) 1824 e.redraw = e.ScrollDown(c, status, h) 1825 e.redrawCursor = true 1826 if e.AfterLineScreenContents() { 1827 e.End(c) 1828 } 1829 break 1830 } 1831 1832 // paste from the portal, clipboard or line buffer. Takes an undo snapshot if text is pasted. 1833 e.Paste(c, status, ©Lines, &previousCopyLines, &firstPasteAction, &lastCopyY, &lastPasteY, &lastCutY, kh.PrevIs("c:13")) 1834 1835 case "c:18": // ctrl-r, to open or close a portal. In debug mode, continue running the program. 1836 1837 if e.nanoMode { // nano: ctrl-r, insert file 1838 // Ask the user which filename to insert 1839 if insertFilename, ok := e.UserInput(c, tty, status, "Insert file", "", []string{e.filename}, false, e.filename); ok { 1840 err := e.RunCommand(c, tty, status, bookmark, undo, "insertfile", insertFilename) 1841 if err != nil { 1842 status.SetError(err) 1843 status.Show(c, e) 1844 break 1845 } 1846 } 1847 1848 break 1849 } 1850 1851 if e.debugMode { 1852 e.DebugContinue() 1853 break 1854 } 1855 1856 // Are we in git mode? 1857 if line := e.CurrentLine(); e.mode == mode.Git && hasAnyPrefixWord(line, gitRebasePrefixes) { 1858 undo.Snapshot(e) 1859 newLine := nextGitRebaseKeyword(line) 1860 e.SetCurrentLine(newLine) 1861 e.redraw = true 1862 e.redrawCursor = true 1863 break 1864 } 1865 1866 // Deal with the portal 1867 status.Clear(c) 1868 if HasPortal() { 1869 status.SetMessage("Closing portal") 1870 e.ClosePortal() 1871 } else { 1872 portal, err := e.NewPortal() 1873 if err != nil { 1874 status.SetError(err) 1875 status.Show(c, e) 1876 break 1877 } 1878 // Portals in the same file is a special case, since lines may move around when pasting 1879 if portal.SameFile(e) { 1880 e.sameFilePortal = portal 1881 } 1882 if err := portal.Save(); err != nil { 1883 status.SetError(err) 1884 status.Show(c, e) 1885 break 1886 } 1887 status.SetMessage("Opening a portal at " + portal.String()) 1888 } 1889 status.Show(c, e) 1890 case "c:2": // ctrl-b, go back after jumping to a definition, bookmark, unbookmark or jump to bookmark. Toggle breakpoint if in debug mode. 1891 1892 if e.nanoMode { // nano: ctrl-b, cursor forward 1893 e.CursorBackward(c, status) 1894 break 1895 } 1896 1897 status.Clear(c) 1898 1899 // Check if we have jumped to a definition and need to go back 1900 if len(backFunctions) > 0 { 1901 lastIndex := len(backFunctions) - 1 1902 // call the function for getting back 1903 backFunctions[lastIndex]() 1904 // pop a function from the end of backFunctions 1905 backFunctions = backFunctions[:lastIndex] 1906 if len(backFunctions) == 0 { 1907 // last possibility to jump back 1908 status.SetMessageAfterRedraw("Back at the first location") 1909 } 1910 break 1911 } 1912 1913 if e.debugMode { 1914 if e.breakpoint == nil { 1915 e.breakpoint = e.pos.Copy() 1916 _, err := e.DebugActivateBreakpoint(filepath.Base(e.filename)) 1917 if err != nil { 1918 status.SetError(err) 1919 break 1920 } 1921 s := "Placed breakpoint at line " + e.LineNumber().String() 1922 status.SetMessage(" " + s + " ") 1923 } else if e.breakpoint.LineNumber() == e.LineNumber() { 1924 // setting a breakpoint at the same line twice: remove the breakpoint 1925 s := "Removed breakpoint at line " + e.breakpoint.LineNumber().String() 1926 status.SetMessage(s) 1927 e.breakpoint = nil 1928 } else { 1929 undo.Snapshot(e) 1930 // Go to the breakpoint position 1931 e.GoToPosition(c, status, *e.breakpoint) 1932 // TODO: Just use status.SetMessageAfterRedraw instead? 1933 // Do the redraw manually before showing the status message 1934 e.DrawLines(c, true, false) 1935 e.redraw = false 1936 // Show the status message 1937 s := "Jumped to breakpoint at line " + e.LineNumber().String() 1938 status.SetMessage(s) 1939 } 1940 } else { 1941 if bookmark == nil { 1942 // no bookmark, create a bookmark at the current line 1943 bookmark = e.pos.Copy() 1944 // TODO: Modify the statusbar implementation so that extra spaces are not needed here. 1945 s := "Bookmarked line " + e.LineNumber().String() 1946 status.SetMessage(" " + s + " ") 1947 } else if bookmark.LineNumber() == e.LineNumber() { 1948 // bookmarking the same line twice: remove the bookmark 1949 s := "Removed bookmark for line " + bookmark.LineNumber().String() 1950 status.SetMessage(s) 1951 bookmark = nil 1952 } else { 1953 undo.Snapshot(e) 1954 // Go to the saved bookmark position 1955 e.GoToPosition(c, status, *bookmark) 1956 // TODO: Just use status.SetMessageAfterRedraw instead? 1957 // Do the redraw manually before showing the status message 1958 e.DrawLines(c, true, false) 1959 e.redraw = false 1960 // Show the status message 1961 s := "Jumped to bookmark at line " + e.LineNumber().String() 1962 status.SetMessage(s) 1963 } 1964 } 1965 status.Show(c, e) 1966 e.redrawCursor = true 1967 case "c:10": // ctrl-j, join line 1968 if e.Empty() { 1969 status.SetMessage("Empty") 1970 status.Show(c, e) 1971 break 1972 } 1973 undo.Snapshot(e) 1974 if e.nanoMode { 1975 // Up to 999 times, join the current line with the next, until the next line is empty 1976 joinCount := 0 1977 for i := 0; i < 999; i++ { 1978 if !e.JoinLineWithNext(c, bookmark) { 1979 break 1980 } 1981 joinCount++ 1982 } 1983 downCounter := 0 1984 for i := 0; i < joinCount; i++ { 1985 e.Down(c, status) 1986 downCounter++ 1987 } 1988 for i := 0; i < downCounter; i++ { 1989 e.Up(c, status) 1990 } 1991 break 1992 } 1993 1994 // The normal join behavior 1995 e.JoinLineWithNext(c, bookmark) 1996 1997 // Go to the start of the line when pressing ctrl-j, but only if it is pressed repeatedly 1998 if kh.Prev() == "c:10" { 1999 e.GoToStartOfTextLine(c) 2000 } 2001 default: // any other key 2002 keyRunes := []rune(key) 2003 if len(keyRunes) > 0 && unicode.IsLetter(keyRunes[0]) { // letter 2004 2005 if keyRunes[0] == 'n' && kh.TwoLastAre("c:14") && kh.PrevWithin(500*time.Millisecond) { 2006 // Avoid inserting "n" if the user very recently pressed ctrl-n twice 2007 break 2008 } else if keyRunes[0] == 'p' && kh.TwoLastAre("c:16") && kh.PrevWithin(500*time.Millisecond) { 2009 // Avoid inserting "p" if the user very recently pressed ctrl-p twice 2010 break 2011 } 2012 2013 undo.Snapshot(e) 2014 2015 if e.mode == mode.Go { // TODO: And e.onlyValidCode 2016 if e.Empty() { 2017 r := keyRunes[0] 2018 // Only "/" or "p" is allowed 2019 if r != 'p' && r != '/' { 2020 status.Clear(c) 2021 status.SetMessage("Not valid Go: " + string(r)) 2022 status.Show(c, e) 2023 break 2024 } 2025 } 2026 } 2027 2028 // Type in the letters that were pressed 2029 for _, r := range keyRunes { 2030 // Insert a letter. This is what normally happens. 2031 wrapped := e.InsertRune(c, r) 2032 if !wrapped { 2033 e.WriteRune(c) 2034 e.Next(c) 2035 } 2036 e.redraw = true 2037 } 2038 } else if len(keyRunes) > 0 && unicode.IsGraphic(keyRunes[0]) { // any other key that can be drawn 2039 undo.Snapshot(e) 2040 e.redraw = true 2041 2042 // Place *something* 2043 r := keyRunes[0] 2044 2045 if r == 160 { 2046 // This is a nonbreaking space that may be inserted with altgr+space that is HORRIBLE. 2047 // Set r to a regular space instead. 2048 r = ' ' 2049 } 2050 2051 // "smart dedent" 2052 if r == '}' || r == ']' || r == ')' { 2053 2054 // Normally, dedent once, but there are exceptions 2055 2056 noContentHereAlready := len(e.TrimmedLine()) == 0 2057 leadingWhitespace := e.LeadingWhitespace() 2058 nextLineContents := e.Line(e.DataY() + 1) 2059 2060 currentX := e.pos.sx 2061 2062 foundCurlyBracketBelow := currentX-1 == strings.Index(nextLineContents, "}") 2063 foundSquareBracketBelow := currentX-1 == strings.Index(nextLineContents, "]") 2064 foundParenthesisBelow := currentX-1 == strings.Index(nextLineContents, ")") 2065 2066 noDedent := foundCurlyBracketBelow || foundSquareBracketBelow || foundParenthesisBelow 2067 2068 // Okay, dedent this line by 1 indentation, if possible 2069 if !noDedent && e.pos.sx > 0 && len(leadingWhitespace) > 0 && noContentHereAlready { 2070 newLeadingWhitespace := leadingWhitespace 2071 if strings.HasSuffix(leadingWhitespace, "\t") { 2072 newLeadingWhitespace = leadingWhitespace[:len(leadingWhitespace)-1] 2073 e.pos.sx -= e.indentation.PerTab 2074 } else if strings.HasSuffix(leadingWhitespace, strings.Repeat(" ", e.indentation.PerTab)) { 2075 newLeadingWhitespace = leadingWhitespace[:len(leadingWhitespace)-e.indentation.PerTab] 2076 e.pos.sx -= e.indentation.PerTab 2077 } 2078 e.SetCurrentLine(newLeadingWhitespace) 2079 } 2080 } 2081 2082 wrapped := e.InsertRune(c, r) 2083 e.WriteRune(c) 2084 if !wrapped && len(string(r)) > 0 { 2085 // Move to the next position 2086 e.Next(c) 2087 } 2088 e.redrawCursor = true 2089 } 2090 } 2091 2092 if e.addSpace { 2093 e.InsertString(c, " ") 2094 e.addSpace = false 2095 } 2096 2097 // Clear the key history, if needed 2098 if clearKeyHistory { 2099 kh.Clear() 2100 clearKeyHistory = false 2101 } else { 2102 kh.Push(key) 2103 } 2104 2105 // Display the ctrl-o menu if esc was pressed 4 times 2106 if !e.nanoMode && kh.Repeated("c:27", 4-1) { // esc pressed 4 times (minus the one that was added just now) 2107 status.ClearAll(c) 2108 undo.Snapshot(e) 2109 undoBackup := undo 2110 lastCommandMenuIndex = e.CommandMenu(c, tty, status, bookmark, undo, lastCommandMenuIndex, forceFlag, fileLock) 2111 undo = undoBackup 2112 // Reset the key history next iteration 2113 clearKeyHistory = true 2114 } 2115 2116 // Clear status, if needed 2117 if e.statusMode && e.redrawCursor { 2118 status.ClearAll(c) 2119 } 2120 2121 // Draw and/or redraw everything, with slightly different behavior over ssh 2122 e.RedrawAtEndOfKeyLoop(c, status) 2123 2124 // Also draw the watches, if debug mode is enabled // and a debug session is in progress 2125 if e.debugMode { 2126 repositionCursor := false 2127 e.DrawWatches(c, repositionCursor) 2128 e.DrawRegisters(c, repositionCursor) 2129 e.DrawGDBOutput(c, repositionCursor) 2130 e.DrawInstructions(c, repositionCursor) 2131 repositionCursor = true 2132 e.DrawFlags(c, repositionCursor) 2133 } 2134 2135 } // end of main loop 2136 2137 if canUseLocks { 2138 // Start by loading the lock overview, just in case something has happened in the mean time 2139 fileLock.Load() 2140 2141 // Check if the lock is unchanged 2142 fileLockTimestamp := fileLock.GetTimestamp(absFilename) 2143 lockUnchanged := lockTimestamp == fileLockTimestamp 2144 2145 // TODO: If the stored timestamp is older than uptime, unlock and save the lock overview 2146 2147 // var notime time.Time 2148 2149 if !forceFlag || lockUnchanged { 2150 // If the file has not been locked externally since this instance of the editor was loaded, don't 2151 // Unlock the current file and save the lock overview. Ignore errors because they are not critical. 2152 fileLock.Unlock(absFilename) 2153 fileLock.Save() 2154 } 2155 } 2156 2157 // Save the current location in the location history and write it to file 2158 e.SaveLocation(absFilename, locationHistory) 2159 2160 // Clear all status bar messages 2161 status.ClearAll(c) 2162 2163 // Quit everything that has to do with the terminal 2164 if e.clearOnQuit { 2165 vt100.Clear() 2166 vt100.Close() 2167 } else { 2168 c.Draw() 2169 fmt.Println() 2170 } 2171 2172 // All done 2173 return "", e.stopParentOnQuit, nil 2174 }