1package main 2 3import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "log" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "time" 12 "unicode" 13 14 "github.com/atotto/clipboard" 15 "github.com/cyrus-and/gdb" 16 "github.com/xyproto/env" 17 "github.com/xyproto/mode" 18 "github.com/xyproto/syntax" 19 "github.com/xyproto/vt100" 20) 21 22// Create a LockKeeper for keeping track of which files are being edited 23var fileLock = NewLockKeeper(defaultLockFile) 24 25// Loop will set up and run the main loop of the editor 26// a *vt100.TTY struct 27// a filename to open 28// a LineNumber (may be 0 or -1) 29// a forceFlag for if the file should be force opened 30// If an error and "true" is returned, it is a quit message to the user, and not an error. 31// If an error and "false" is returned, it is an error. 32func Loop(tty *vt100.TTY, filename string, lineNumber LineNumber, colNumber ColNumber, forceFlag bool, theme Theme, syntaxHighlight bool) (userMessage string, err error) { 33 34 // Create a Canvas for drawing onto the terminal 35 vt100.Init() 36 c := vt100.NewCanvas() 37 c.ShowCursor() 38 39 var ( 40 statusDuration = 2700 * time.Millisecond 41 42 copyLines []string // for the cut/copy/paste functionality 43 previousCopyLines []string // for checking if a paste is the same as last time 44 bookmark *Position // for the bookmark/jump functionality 45 breakpoint *Position // for the breakpoint/jump functionality in debug mode 46 statusMode bool // if information should be shown at the bottom 47 48 firstPasteAction = true 49 firstCopyAction = true 50 51 lastCopyY LineIndex = -1 // used for keeping track if ctrl-c is pressed twice on the same line 52 lastPasteY LineIndex = -1 // used for keeping track if ctrl-v is pressed twice on the same line 53 lastCutY LineIndex = -1 // used for keeping track if ctrl-x is pressed twice on the same line 54 55 previousKey string // keep track of the previous key press 56 57 lastCommandMenuIndex int // for the command menu 58 59 key string // for the main loop 60 61 jsonFormatToggle bool // for toggling indentation or not when pressing ctrl-w for JSON 62 ) 63 64 // New editor struct. Scroll 10 lines at a time, no word wrap. 65 e, statusMessage, err := NewEditor(tty, c, filename, lineNumber, colNumber, theme, syntaxHighlight) 66 if err != nil { 67 return "", err 68 } 69 70 // Find the absolute path to this filename 71 absFilename, err := e.AbsFilename() 72 if err != nil { 73 // This should never happen, just use the given filename 74 absFilename = e.filename 75 } 76 77 // Minor adjustments to some modes 78 switch e.mode { 79 case mode.Git: 80 e.StatusForeground = vt100.LightBlue 81 e.StatusBackground = vt100.BackgroundDefault 82 case mode.ManPage: 83 e.readOnly = true 84 } 85 86 // Prepare a status bar 87 status := NewStatusBar(e.StatusForeground, e.StatusBackground, e.StatusErrorForeground, e.StatusErrorBackground, e, statusDuration) 88 89 e.SetTheme(e.Theme) 90 91 // Terminal resize handler 92 e.SetUpResizeHandler(c, tty, status) 93 94 // ctrl-c handler 95 e.SetUpSignalHandlers(c, tty, status, absFilename) 96 97 e.previousX = 1 98 e.previousY = 1 99 100 tty.SetTimeout(2 * time.Millisecond) 101 102 var ( 103 canUseLocks = true 104 lockTimestamp time.Time 105 ) 106 107 // If the lock keeper does not have an overview already, that's fine. Ignore errors from lk.Load(). 108 if err := fileLock.Load(); err != nil { 109 // Could not load an existing lock overview, this might be the first run? Try saving. 110 if err := fileLock.Save(); err != nil { 111 // Could not save a lock overview. Can not use locks. 112 canUseLocks = false 113 } 114 } 115 116 if canUseLocks { 117 // Check if the lock should be forced (also force when running git commit, becase it is likely that o was killed in that case) 118 if forceFlag || filepath.Base(absFilename) == "COMMIT_EDITMSG" || env.Bool("O_FORCE") { 119 // Lock and save, regardless of what the previous status is 120 fileLock.Lock(absFilename) 121 // TODO: If the file was already marked as locked, this is not strictly needed? The timestamp might be modified, though. 122 fileLock.Save() 123 } else { 124 // Lock the current file, if it's not already locked 125 if err := fileLock.Lock(absFilename); err != nil { 126 return fmt.Sprintf("Locked by another (possibly dead) instance of this editor.\nTry: o -f %s", filepath.Base(absFilename)), errors.New(absFilename + " is locked") 127 } 128 // Immediately save the lock file as a signal to other instances of the editor 129 fileLock.Save() 130 } 131 lockTimestamp = fileLock.GetTimestamp(absFilename) 132 133 // Set up a catch for panics, so that the current file can be unlocked 134 defer func() { 135 if x := recover(); x != nil { 136 // Unlock and save the lock file 137 fileLock.Unlock(absFilename) 138 fileLock.Save() 139 140 // Save the current file. The assumption is that it's better than not saving, if something crashes. 141 // TODO: Save to a crash file, then let the editor discover this when it starts. 142 e.Save(c, tty) 143 144 // Output the error message 145 quitMessageWithStack(tty, fmt.Sprintf("%v", x)) 146 } 147 }() 148 } 149 150 // Draw everything once, with slightly different behavior if used over ssh 151 e.InitialRedraw(c, status, &statusMessage) 152 153 // This is the main loop for the editor 154 for !e.quit { 155 156 // Read the next key 157 key = tty.String() 158 159 switch key { 160 case "c:17": // ctrl-q, quit 161 e.quit = true 162 case "c:23": // ctrl-w, format (or if in git mode, cycle interactive rebase keywords) 163 164 undo.Snapshot(e) 165 166 // Clear the search term 167 e.ClearSearchTerm() 168 169 // Cycle git rebase keywords 170 if line := e.CurrentLine(); e.mode == mode.Git && hasAnyPrefixWord(line, gitRebasePrefixes) { 171 newLine := nextGitRebaseKeyword(line) 172 e.SetCurrentLine(newLine) 173 e.redraw = true 174 e.redrawCursor = true 175 break 176 } 177 178 if e.Empty() { 179 // Empty file, nothing to format, insert a program template, if available 180 if err := e.InsertTemplateProgram(c); err != nil { 181 status.ClearAll(c) 182 status.SetMessage("nothing to format and no template available") 183 status.Show(c, e) 184 } else { 185 e.redraw = true 186 e.redrawCursor = true 187 } 188 break 189 } 190 191 if e.mode == mode.Markdown { 192 e.ToggleCheckboxCurrentLine() 193 break 194 } 195 196 e.formatCode(c, tty, status, &jsonFormatToggle) 197 198 // Move the cursor if after the end of the line 199 if e.AtOrAfterEndOfLine() { 200 e.End(c) 201 } 202 case "c:6": // ctrl-f, search for a string 203 e.SearchMode(c, status, tty, true, &statusMessage, undo) 204 case "c:0": // ctrl-space, build source code to executable, or export, depending on the mode 205 206 if e.Empty() { 207 // Empty file, nothing to build 208 status.ClearAll(c) 209 status.SetErrorMessage("Nothing to build") 210 status.Show(c, e) 211 break 212 } 213 214 // Save the current file, but only if it has changed 215 if e.changed { 216 if err := e.Save(c, tty); err != nil { 217 status.ClearAll(c) 218 status.SetErrorMessage(err.Error()) 219 status.Show(c, e) 220 break 221 } 222 } 223 224 // Clear the current search term 225 e.ClearSearchTerm() 226 227 // ctrl-space was pressed while in debug mode 228 if e.debugMode && e.gdb != nil { 229 status.ClearAll(c) 230 status.SetMessage("step") 231 status.Show(c, e) 232 // Go to the next step in the program 233 e.gdb.Send("step") 234 // continue is also available 235 // see: https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html 236 break 237 } 238 239 // Press ctrl-space twice the first time the Markdown file should be exported to PDF 240 // to avvoid the first accidental ctrl-space key press. 241 242 // Build or export the current file 243 var ( 244 statusMessage string 245 performedAction bool 246 compiled bool 247 outputExecutable string 248 ) 249 250 // The last argument is if the command should run in the background or not 251 statusMessage, performedAction, compiled, outputExecutable = e.BuildOrExport(c, tty, status, e.filename, e.mode == mode.Markdown) 252 253 //logf("status message %s performed action %v compiled %v filename %s\n", statusMessage, performedAction, compiled, e.filename) 254 255 // Could an action be performed for this file extension? 256 if !performedAction { 257 status.ClearAll(c) 258 // Building this file extension is not implemented yet. 259 // Just display the current time and word count. 260 // TODO: status.ClearAll() should have cleared the status bar first, but this is not always true, 261 // which is why the message is hackily surrounded by spaces. Fix. 262 statusMessage := fmt.Sprintf(" %d words, %s ", e.WordCount(), time.Now().Format("15:04")) // HH:MM 263 status.SetMessage(statusMessage) 264 status.Show(c, e) 265 } else if performedAction && !compiled { 266 status.ClearAll(c) 267 // Performed an action, but it did not work out 268 if statusMessage != "" { 269 status.SetErrorMessage(statusMessage) 270 } else { 271 // This should never happen, failed compilations should return a message 272 status.SetErrorMessage("Compilation failed") 273 } 274 status.ShowNoTimeout(c, e) 275 } else if performedAction && compiled { 276 // Everything worked out 277 if statusMessage != "" { 278 // Got a status message (this may not be the case for build/export processes running in the background) 279 // NOTE: Do not clear the status message first here! 280 status.SetMessage(statusMessage) 281 status.ShowNoTimeout(c, e) 282 } 283 if e.debugMode { 284 // Try to start a new gdb session 285 if e.gdb == nil { 286 e.gdb, err = gdb.New(func(notification map[string]interface{}) { 287 notificationText, err := json.Marshal(notification) 288 if err != nil { 289 log.Fatal(err) 290 } 291 logf("%s\n", notificationText) 292 }) 293 if err != nil { 294 status.ClearAll(c) 295 status.SetErrorMessage(err.Error()) 296 status.Show(c, e) 297 return 298 } 299 if e.gdb == nil { 300 status.ClearAll(c) 301 status.SetErrorMessage("could not start gdb") 302 status.Show(c, e) 303 return 304 } 305 defer e.gdb.Exit() 306 // Load the executable file 307 e.gdb.Send("file-exec-file", outputExecutable) 308 // Set the breakpoint, if it has been set with ctrl-b 309 if breakpoint != nil { 310 e.gdb.Send("break-insert", fmt.Sprintf("%s:%d", absFilename, breakpoint.LineNumber())) 311 } 312 // Start from the top 313 e.gdb.Send("exec-run", "--start") 314 // Ready 315 break 316 } 317 } 318 } 319 case "c:20": // ctrl-t, jump to code 320 // If in a C++ header file, switch to the corresponding 321 // C++ source file, and the other way around. 322 323 // Save the current file, but only if it has changed 324 if e.changed { 325 if err := e.Save(c, tty); err != nil { 326 status.ClearAll(c) 327 status.SetErrorMessage(err.Error()) 328 status.Show(c, e) 329 break 330 } 331 } 332 333 e.redrawCursor = true 334 335 // If this is a C++ source file, try finding and opening the corresponding header file 336 if hasS([]string{".cpp", ".cc", ".c", ".cxx"}, filepath.Ext(e.filename)) { 337 // Check if there is a corresponding header file 338 if absFilename, err := e.AbsFilename(); err == nil { // no error 339 headerExtensions := []string{".h", ".hpp"} 340 if headerFilename, err := ExtFileSearch(absFilename, headerExtensions, fileSearchMaxTime); err == nil && headerFilename != "" { // no error 341 // Switch to another file (without forcing it) 342 e.Switch(c, tty, status, fileLock, headerFilename, false) 343 } 344 } 345 break 346 } 347 348 // If this is a header file, present a menu option for open the corresponding source file 349 if hasS([]string{".h", ".hpp"}, filepath.Ext(e.filename)) { 350 // Check if there is a corresponding header file 351 if absFilename, err := e.AbsFilename(); err == nil { // no error 352 sourceExtensions := []string{".c", ".cpp", ".cxx", ".cc"} 353 if headerFilename, err := ExtFileSearch(absFilename, sourceExtensions, fileSearchMaxTime); err == nil && headerFilename != "" { // no error 354 // Switch to another file (without forcing it) 355 e.Switch(c, tty, status, fileLock, headerFilename, false) 356 } 357 } 358 break 359 } 360 361 status.ClearAll(c) 362 status.SetErrorMessage("Nothing to jump to") 363 status.Show(c, e) 364 365 case "c:28": // ctrl-\, toggle comment for this block 366 undo.Snapshot(e) 367 e.ToggleCommentBlock(c) 368 e.redraw = true 369 e.redrawCursor = true 370 case "c:15": // ctrl-o, launch the command menu 371 status.ClearAll(c) 372 undo.Snapshot(e) 373 undoBackup := undo 374 var addSpace bool 375 lastCommandMenuIndex, addSpace = e.CommandMenu(c, tty, status, undo, lastCommandMenuIndex, forceFlag, fileLock) 376 undo = undoBackup 377 if e.AfterEndOfLine() { 378 e.End(c) 379 } 380 if addSpace { 381 e.InsertString(c, " ") 382 } 383 case "c:7": // ctrl-g, status mode 384 e.statusMode = !e.statusMode 385 if e.statusMode { 386 status.ShowLineColWordCount(c, e, e.filename) 387 } else { 388 status.ClearAll(c) 389 } 390 case "←": // left arrow 391 // movement if there is horizontal scrolling 392 if e.pos.offsetX > 0 { 393 if e.pos.sx > 0 { 394 // Move one step left 395 if e.TabToTheLeft() { 396 e.pos.sx -= e.tabsSpaces.PerTab 397 } else { 398 e.pos.sx-- 399 } 400 } else { 401 // Scroll one step left 402 e.pos.offsetX-- 403 e.redraw = true 404 } 405 e.SaveX(true) 406 } else if e.pos.sx > 0 { 407 // no horizontal scrolling going on 408 // Move one step left 409 if e.TabToTheLeft() { 410 e.pos.sx -= e.tabsSpaces.PerTab 411 } else { 412 e.pos.sx-- 413 } 414 e.SaveX(true) 415 } else if e.DataY() > 0 { 416 // no scrolling or movement to the left going on 417 e.Up(c, status) 418 e.End(c) 419 //e.redraw = true 420 } // else at the start of the document 421 e.redrawCursor = true 422 // Workaround for Konsole 423 if e.pos.sx <= 2 { 424 // Konsole prints "2H" here, but 425 // no other terminal emulator does that 426 e.redraw = true 427 } 428 case "→": // right arrow 429 // If on the last line or before, go to the next character 430 if e.DataY() <= LineIndex(e.Len()) { 431 e.Next(c) 432 } 433 if e.AfterScreenWidth(c) { 434 e.pos.offsetX++ 435 e.redraw = true 436 e.pos.sx-- 437 if e.pos.sx < 0 { 438 e.pos.sx = 0 439 } 440 if e.AfterEndOfLine() { 441 e.Down(c, status) 442 } 443 } else if e.AfterEndOfLine() { 444 e.End(c) 445 } 446 e.SaveX(true) 447 e.redrawCursor = true 448 case "↑": // up arrow 449 // Move the screen cursor 450 451 // TODO: Stay at the same X offset when moving up in the document? 452 if e.pos.offsetX > 0 { 453 e.pos.offsetX = 0 454 } 455 456 if e.DataY() > 0 { 457 // Move the position up in the current screen 458 if e.UpEnd(c) != nil { 459 // If below the top, scroll the contents up 460 if e.DataY() > 0 { 461 e.redraw = e.ScrollUp(c, status, 1) 462 e.pos.Down(c) 463 e.UpEnd(c) 464 } 465 } 466 // If the cursor is after the length of the current line, move it to the end of the current line 467 if e.AfterLineScreenContents() { 468 e.End(c) 469 } 470 } 471 // If the cursor is after the length of the current line, move it to the end of the current line 472 if e.AfterLineScreenContents() { 473 e.End(c) 474 475 // Then, if the rune to the left is '}', move one step to the left 476 if r := e.LeftRune(); r == '}' { 477 e.Prev(c) 478 } 479 } 480 e.redrawCursor = true 481 case "↓": // down arrow 482 483 // TODO: Stay at the same X offset when moving down in the document? 484 if e.pos.offsetX > 0 { 485 e.pos.offsetX = 0 486 } 487 488 if e.DataY() < LineIndex(e.Len()) { 489 // Move the position down in the current screen 490 if e.DownEnd(c) != nil { 491 // If at the bottom, don't move down, but scroll the contents 492 // Output a helpful message 493 if !e.AfterEndOfDocument() { 494 e.redraw = e.ScrollDown(c, status, 1) 495 e.pos.Up() 496 e.DownEnd(c) 497 } 498 } 499 // If the cursor is after the length of the current line, move it to the end of the current line 500 if e.AfterLineScreenContents() { 501 e.End(c) 502 503 // Then, if the rune to the left is '}', move one step to the left 504 if r := e.LeftRune(); r == '}' { 505 e.Prev(c) 506 } 507 } 508 } 509 // If the cursor is after the length of the current line, move it to the end of the current line 510 if e.AfterLineScreenContents() { 511 e.End(c) 512 } 513 e.redrawCursor = true 514 case "c:14": // ctrl-n, scroll down or jump to next match, using the sticky search term 515 e.UseStickySearchTerm() 516 if e.SearchTerm() != "" { 517 // Go to next match 518 wrap := true 519 forward := true 520 if err := e.GoToNextMatch(c, status, wrap, forward); err == errNoSearchMatch { 521 status.Clear(c) 522 if wrap { 523 status.SetMessage(e.SearchTerm() + " not found") 524 } else { 525 status.SetMessage(e.SearchTerm() + " not found from here") 526 } 527 status.Show(c, e) 528 } 529 } else { 530 531 // Scroll down 532 e.redraw = e.ScrollDown(c, status, e.pos.scrollSpeed) 533 // If e.redraw is false, the end of file is reached 534 if !e.redraw { 535 status.Clear(c) 536 status.SetMessage("EOF") 537 status.Show(c, e) 538 } 539 e.redrawCursor = true 540 if e.AfterLineScreenContents() { 541 e.End(c) 542 } 543 544 } 545 case "c:16": // ctrl-p, scroll up or jump to the previous match, using the sticky search term 546 e.UseStickySearchTerm() 547 if e.SearchTerm() != "" { 548 // Go to previous match 549 wrap := true 550 forward := false 551 if err := e.GoToNextMatch(c, status, wrap, forward); err == errNoSearchMatch { 552 status.Clear(c) 553 if wrap { 554 status.SetMessage(e.SearchTerm() + " not found") 555 } else { 556 status.SetMessage(e.SearchTerm() + " not found from here") 557 } 558 status.Show(c, e) 559 } 560 } else { 561 e.redraw = e.ScrollUp(c, status, e.pos.scrollSpeed) 562 e.redrawCursor = true 563 if e.AfterLineScreenContents() { 564 e.End(c) 565 } 566 567 } 568 // Additional way to clear the sticky search term, like with Esc 569 case "c:27": // esc, clear search term (but not the sticky search term), reset, clean and redraw 570 // Reset the cut/copy/paste double-keypress detection 571 lastCopyY = -1 572 lastPasteY = -1 573 lastCutY = -1 574 // Do a full clear and redraw + clear search term + jump 575 drawLines := true 576 resized := false 577 e.FullResetRedraw(c, status, drawLines, resized) 578 case " ": // space 579 580 // Scroll down if a man page is being viewed, or if the editor is read-only 581 if e.readOnly { 582 // Scroll down at double scroll speed 583 e.redraw = e.ScrollDown(c, status, e.pos.scrollSpeed*2) 584 // If e.redraw is false, the end of file is reached 585 if !e.redraw { 586 status.Clear(c) 587 status.SetMessage("EOF") 588 status.Show(c, e) 589 } 590 e.redrawCursor = true 591 if e.AfterLineScreenContents() { 592 e.End(c) 593 } 594 break 595 } 596 597 // Regular behavior, take an undo snapshot and insert a space 598 undo.Snapshot(e) 599 // Place a space 600 wrapped := e.InsertRune(c, ' ') 601 if !wrapped { 602 e.WriteRune(c) 603 // Move to the next position 604 e.Next(c) 605 } 606 e.redraw = true 607 case "c:13": // return 608 609 // Scroll down if a man page is being viewed, or if the editor is read-only 610 if e.readOnly { 611 // Scroll down at double scroll speed 612 e.redraw = e.ScrollDown(c, status, e.pos.scrollSpeed*2) 613 // If e.redraw is false, the end of file is reached 614 if !e.redraw { 615 status.Clear(c) 616 status.SetMessage("EOF") 617 status.Show(c, e) 618 } 619 e.redrawCursor = true 620 if e.AfterLineScreenContents() { 621 e.End(c) 622 } 623 break 624 } 625 626 // Regular behavior 627 628 // Modify the paste double-keypress detection to allow for a manual return before pasting the rest 629 if lastPasteY != -1 && previousKey != "c:13" { 630 lastPasteY++ 631 } 632 633 undo.Snapshot(e) 634 635 var ( 636 lineContents = e.CurrentLine() 637 trimmedLine = strings.TrimSpace(lineContents) 638 currentLeadingWhitespace = e.LeadingWhitespace() 639 640 // Grab the leading whitespace from the current line, and indent depending on the end of trimmedLine 641 leadingWhitespace = e.smartIndentation(currentLeadingWhitespace, trimmedLine, false) // the last parameter is "also dedent" 642 643 noHome = false 644 indent = true 645 ) 646 647 // TODO: add and use something like "e.shouldAutoIndent" for these file types 648 if e.mode == mode.Markdown || e.mode == mode.Text || e.mode == mode.Blank { 649 indent = false 650 } 651 652 if trimmedLine == "private:" || trimmedLine == "protected:" || trimmedLine == "public:" { 653 // De-indent the current line before moving on to the next 654 e.SetCurrentLine(trimmedLine) 655 leadingWhitespace = currentLeadingWhitespace 656 } else if e.mode == mode.C || e.mode == mode.Cpp || e.mode == mode.Zig || e.mode == mode.Rust || e.mode == mode.Java || e.mode == mode.JavaScript || e.mode == mode.Kotlin || e.mode == mode.TypeScript || e.mode == mode.D { 657 // Add missing parenthesis for "if ... {", "} else if", "} elif", "for", "while" and "when" for C-like languages 658 for _, kw := range []string{"for", "foreach", "foreach_reverse", "if", "switch", "when", "while", "while let", "} else if", "} elif"} { 659 if strings.HasPrefix(trimmedLine, kw+" ") && !strings.HasPrefix(trimmedLine, kw+" (") { 660 if strings.HasSuffix(trimmedLine, " {") { 661 // Add ( and ), keep the final "{" 662 e.SetCurrentLine(currentLeadingWhitespace + kw + " (" + trimmedLine[len(kw)+1:len(trimmedLine)-2] + ") {") 663 e.pos.sx += 2 664 } else if !strings.HasSuffix(trimmedLine, ")") { 665 // Add ( and ), there is no final "{" 666 e.SetCurrentLine(currentLeadingWhitespace + kw + " (" + trimmedLine[len(kw)+1:] + ")") 667 e.pos.sx += 2 668 indent = true 669 leadingWhitespace = e.tabsSpaces.String() + currentLeadingWhitespace 670 } 671 } 672 } 673 } 674 675 //onlyOneLine := e.AtFirstLineOfDocument() && e.AtOrAfterLastLineOfDocument() 676 //middleOfText := !e.AtOrBeforeStartOfTextLine() && !e.AtOrAfterEndOfLine() 677 678 scrollBack := false 679 680 // TODO: Collect the criteria that trigger the same behavior 681 682 switch { 683 case e.AtOrAfterLastLineOfDocument() && (e.AtStartOfTheLine() || e.AtOrBeforeStartOfTextScreenLine()): 684 e.InsertLineAbove() 685 noHome = true 686 case e.AtOrAfterEndOfDocument() && !e.AtStartOfTheLine() && !e.AtOrAfterEndOfLine(): 687 e.InsertStringAndMove(c, "") 688 e.InsertLineBelow() 689 scrollBack = true 690 case e.AfterEndOfLine(): 691 e.InsertLineBelow() 692 scrollBack = true 693 case !e.AtFirstLineOfDocument() && e.AtOrAfterLastLineOfDocument() && (e.AtStartOfTheLine() || e.AtOrAfterEndOfLine()): 694 e.InsertStringAndMove(c, "") 695 scrollBack = true 696 case e.AtStartOfTheLine(): 697 e.InsertLineAbove() 698 noHome = true 699 default: 700 // Split the current line in two 701 if !e.SplitLine() { 702 e.InsertLineBelow() 703 } 704 scrollBack = true 705 // Indent the next line if at the end, not else 706 if !e.AfterEndOfLine() { 707 indent = false 708 } 709 } 710 e.MakeConsistent() 711 712 h := int(c.Height()) 713 if e.pos.sy > (h - 1) { 714 e.pos.Down(c) 715 e.redraw = e.ScrollDown(c, status, 1) 716 e.redrawCursor = true 717 } else if e.pos.sy == (h - 1) { 718 e.redraw = e.ScrollDown(c, status, 1) 719 e.redrawCursor = true 720 } else { 721 e.pos.Down(c) 722 } 723 724 if !noHome { 725 e.pos.sx = 0 726 //e.Home() 727 if scrollBack { 728 e.pos.SetX(c, 0) 729 } 730 } 731 732 if indent && len(leadingWhitespace) > 0 { 733 // If the leading whitespace starts with a tab and ends with a space, remove the final space 734 if strings.HasPrefix(leadingWhitespace, "\t") && strings.HasSuffix(leadingWhitespace, " ") { 735 leadingWhitespace = leadingWhitespace[:len(leadingWhitespace)-1] 736 //logf("cleaned leading whitespace: %v\n", []rune(leadingWhitespace)) 737 } 738 if !noHome { 739 // Insert the same leading whitespace for the new line 740 e.SetCurrentLine(leadingWhitespace + e.LineContentsFromCursorPosition()) 741 // Then move to the start of the text 742 e.GoToStartOfTextLine(c) 743 } 744 } 745 746 e.SaveX(true) 747 e.redraw = true 748 e.redrawCursor = true 749 case "c:8", "c:127": // ctrl-h or backspace 750 751 // Scroll up if a man page is being viewed, or if the editor is read-only 752 if e.readOnly { 753 // Scroll up at double speed 754 e.redraw = e.ScrollUp(c, status, e.pos.scrollSpeed*2) 755 e.redrawCursor = true 756 if e.AfterLineScreenContents() { 757 e.End(c) 758 } 759 break 760 } 761 762 // Just clear the search term, if there is an active search 763 if len(e.SearchTerm()) > 0 { 764 e.ClearSearchTerm() 765 e.redraw = true 766 e.redrawCursor = true 767 // Don't break, continue to delete to the left after clearing the search 768 //break 769 } 770 undo.Snapshot(e) 771 // Delete the character to the left 772 if e.EmptyLine() { 773 e.DeleteCurrentLineMoveBookmark(bookmark) 774 e.pos.Up() 775 e.TrimRight(e.DataY()) 776 e.End(c) 777 } else if e.AtStartOfTheLine() { // at the start of the screen line, the line may be scrolled 778 // remove the rest of the current line and move to the last letter of the line above 779 // before deleting it 780 if e.DataY() > 0 { 781 e.pos.Up() 782 e.TrimRight(e.DataY()) 783 e.End(c) 784 e.Delete() 785 } 786 } else if e.tabsSpaces.Spaces && (e.EmptyLine() || e.AtStartOfTheLine()) && e.tabsSpaces.WSLen(e.LeadingWhitespace()) >= e.tabsSpaces.PerTab { 787 // Delete several spaces 788 for i := 0; i < e.tabsSpaces.PerTab; i++ { 789 // Move back 790 e.Prev(c) 791 // Type a blank 792 e.SetRune(' ') 793 e.WriteRune(c) 794 e.Delete() 795 } 796 } else { 797 // Move back 798 e.Prev(c) 799 // Type a blank 800 e.SetRune(' ') 801 e.WriteRune(c) 802 if !e.AtOrAfterEndOfLine() { 803 // Delete the blank 804 e.Delete() 805 } 806 } 807 e.redrawCursor = true 808 e.redraw = true 809 case "c:9": // tab 810 y := int(e.DataY()) 811 r := e.Rune() 812 leftRune := e.LeftRune() 813 ext := filepath.Ext(e.filename) 814 815 // Tab completion of words for Go 816 if word := e.LettersBeforeCursor(); e.mode != mode.Blank && e.mode != mode.GoAssembly && e.mode != mode.Assembly && leftRune != '.' && !unicode.IsLetter(r) && len(word) > 0 { 817 found := false 818 expandedWord := "" 819 for kw := range syntax.Keywords { 820 if len(kw) < 3 { 821 // skip too short suggestions 822 continue 823 } 824 if strings.HasPrefix(kw, word) { 825 if !found || (len(kw) < len(expandedWord)) && (len(expandedWord) > 0) { 826 expandedWord = kw 827 found = true 828 } 829 } 830 } 831 832 // Found a suitable keyword to expand to? Insert the rest of the string. 833 if found { 834 toInsert := strings.TrimPrefix(expandedWord, word) 835 undo.Snapshot(e) 836 e.redrawCursor = true 837 e.redraw = true 838 // Insert the part of expandedWord that comes after the current word 839 e.InsertStringAndMove(c, toInsert) 840 break 841 } 842 843 // Tab completion after a '.' 844 } else if word := e.LettersOrDotBeforeCursor(); e.mode != mode.Blank && e.mode != mode.GoAssembly && e.mode != mode.Assembly && leftRune == '.' && !unicode.IsLetter(r) && len(word) > 0 { 845 // Now the preceding word before the "." has been found 846 847 // Trim the trailing ".", if needed 848 word = strings.TrimSuffix(strings.TrimSpace(word), ".") 849 850 // Grep all files in this directory with the same extension as the currently edited file 851 // for what could follow the word and a "." 852 suggestions := corpus(word, "*"+ext) 853 854 // Choose a suggestion (tab cycles to the next suggestion) 855 chosen := e.SuggestMode(c, status, tty, suggestions) 856 e.redrawCursor = true 857 e.redraw = true 858 859 if chosen != "" { 860 undo.Snapshot(e) 861 // Insert the chosen word 862 e.InsertStringAndMove(c, chosen) 863 break 864 } 865 866 } 867 868 // Enable auto indent if the extension is not "" and either: 869 // * The mode is set to Go and the position is not at the very start of the line (empty or not) 870 // * Syntax highlighting is enabled and the cursor is not at the start of the line (or before) 871 trimmedLine := e.TrimmedLine() 872 //emptyLine := len(trimmedLine) == 0 873 //almostEmptyLine := len(trimmedLine) <= 1 874 875 // Check if a line that is more than just a '{', '(', '[' or ':' ends with one of those 876 endsWithSpecial := len(trimmedLine) > 1 && r == '{' || r == '(' || r == '[' || r == ':' 877 878 // Smart indent if: 879 // * the rune to the left is not a blank character or the line ends with {, (, [ or : 880 // * and also if it the cursor is not to the very left 881 // * and also if this is not a text file or a blank file 882 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 883 if (!unicode.IsSpace(leftRune) || endsWithSpecial) && e.pos.sx > 0 && !noSmartIndentation { 884 lineAbove := 1 885 if strings.TrimSpace(e.Line(LineIndex(y-lineAbove))) == "" { 886 // The line above is empty, use the indentation before the line above that 887 lineAbove-- 888 } 889 indexAbove := LineIndex(y - lineAbove) 890 // If we have a line (one or two lines above) as a reference point for the indentation 891 if strings.TrimSpace(e.Line(indexAbove)) != "" { 892 893 // Move the current indentation to the same as the line above 894 undo.Snapshot(e) 895 896 var ( 897 spaceAbove = e.LeadingWhitespaceAt(indexAbove) 898 strippedLineAbove = e.StripSingleLineComment(strings.TrimSpace(e.Line(indexAbove))) 899 newLeadingSpace string 900 ) 901 902 oneIndentation := e.tabsSpaces.String() 903 904 // Smart-ish indentation 905 if !strings.HasPrefix(strippedLineAbove, "switch ") && (strings.HasPrefix(strippedLineAbove, "case ")) || 906 strings.HasSuffix(strippedLineAbove, "{") || strings.HasSuffix(strippedLineAbove, "[") || 907 strings.HasSuffix(strippedLineAbove, "(") || strings.HasSuffix(strippedLineAbove, ":") || 908 strings.HasSuffix(strippedLineAbove, " \\") || 909 strings.HasPrefix(strippedLineAbove, "if ") { 910 // Use one more indentation than the line above 911 newLeadingSpace = spaceAbove + oneIndentation 912 } else if ((len(spaceAbove) - len(oneIndentation)) > 0) && strings.HasSuffix(trimmedLine, "}") { 913 // Use one less indentation than the line above 914 newLeadingSpace = spaceAbove[:len(spaceAbove)-len(oneIndentation)] 915 } else { 916 // Use the same indentation as the line above 917 newLeadingSpace = spaceAbove 918 } 919 920 e.SetCurrentLine(newLeadingSpace + trimmedLine) 921 if e.AtOrAfterEndOfLine() { 922 e.End(c) 923 } 924 e.redrawCursor = true 925 e.redraw = true 926 927 // job done 928 break 929 930 } 931 } 932 933 undo.Snapshot(e) 934 if e.tabsSpaces.Spaces { 935 for i := 0; i < e.tabsSpaces.PerTab; i++ { 936 e.InsertRune(c, ' ') 937 // Write the spaces that represent the tab to the canvas 938 e.WriteTab(c) 939 // Move to the next position 940 e.Next(c) 941 } 942 } else { 943 // Insert a tab character to the file 944 e.InsertRune(c, '\t') 945 // Write the spaces that represent the tab to the canvas 946 e.WriteTab(c) 947 // Move to the next position 948 e.Next(c) 949 } 950 951 // Prepare to redraw 952 e.redrawCursor = true 953 e.redraw = true 954 case "c:1", "c:25": // ctrl-a, home (or ctrl-y for scrolling up in the st terminal) 955 956 // Do not reset cut/copy/paste status 957 958 // First check if we just moved to this line with the arrow keys 959 justMovedUpOrDown := previousKey == "↓" || previousKey == "↑" 960 // If at an empty line, go up one line 961 if !justMovedUpOrDown && e.EmptyRightTrimmedLine() && e.SearchTerm() == "" { 962 e.Up(c, status) 963 //e.GoToStartOfTextLine() 964 e.End(c) 965 } else if x, err := e.DataX(); err == nil && x == 0 && !justMovedUpOrDown && e.SearchTerm() == "" { 966 // If at the start of the line, 967 // go to the end of the previous line 968 e.Up(c, status) 969 e.End(c) 970 } else if e.AtStartOfTextScreenLine() { 971 // If at the start of the text for this scroll position, go to the start of the line 972 e.Home() 973 } else { 974 // If none of the above, go to the start of the text 975 e.GoToStartOfTextLine(c) 976 } 977 978 e.redrawCursor = true 979 e.SaveX(true) 980 case "c:5": // ctrl-e, end 981 982 // Do not reset cut/copy/paste status 983 984 // First check if we just moved to this line with the arrow keys, or just cut a line with ctrl-x 985 justMovedUpOrDown := previousKey == "↓" || previousKey == "↑" || previousKey == "c:24" 986 if e.AtEndOfDocument() { 987 e.End(c) 988 break 989 } 990 // If we didn't just move here, and are at the end of the line, 991 // move down one line and to the end, if not, 992 // just move to the end. 993 if !justMovedUpOrDown && e.AfterEndOfLine() && e.SearchTerm() == "" { 994 e.Down(c, status) 995 e.Home() 996 } else { 997 e.End(c) 998 } 999 1000 e.redrawCursor = true 1001 e.SaveX(true) 1002 case "c:4": // ctrl-d, delete 1003 undo.Snapshot(e) 1004 if e.Empty() { 1005 status.SetMessage("Empty") 1006 status.Show(c, e) 1007 } else { 1008 e.Delete() 1009 e.redraw = true 1010 } 1011 e.redrawCursor = true 1012 case "c:30": // ctrl-~, jump to matching parenthesis or curly bracket 1013 r := e.Rune() 1014 1015 if e.AfterEndOfLine() { 1016 e.Prev(c) 1017 r = e.Rune() 1018 } 1019 1020 // Find which opening and closing parenthesis/curly brackets to look for 1021 opening, closing := rune(0), rune(0) 1022 switch r { 1023 case '(', ')': 1024 opening = '(' 1025 closing = ')' 1026 case '{', '}': 1027 opening = '{' 1028 closing = '}' 1029 case '[', ']': 1030 opening = '[' 1031 closing = ']' 1032 } 1033 1034 if opening == rune(0) { 1035 status.Clear(c) 1036 status.SetMessage("No matching (, ), [, ], { or }") 1037 status.Show(c, e) 1038 break 1039 } 1040 1041 // Search either forwards or backwards to find a matching rune 1042 switch r { 1043 case '(', '{', '[': 1044 parcount := 0 1045 for !e.AtOrAfterEndOfDocument() { 1046 if r := e.Rune(); r == closing { 1047 if parcount == 1 { 1048 // FOUND, STOP 1049 break 1050 } else { 1051 parcount-- 1052 } 1053 } else if r == opening { 1054 parcount++ 1055 } 1056 e.Next(c) 1057 } 1058 case ')', '}', ']': 1059 parcount := 0 1060 for !e.AtStartOfDocument() { 1061 if r := e.Rune(); r == opening { 1062 if parcount == 1 { 1063 // FOUND, STOP 1064 break 1065 } else { 1066 parcount-- 1067 } 1068 } else if r == closing { 1069 parcount++ 1070 } 1071 e.Prev(c) 1072 } 1073 } 1074 1075 e.redrawCursor = true 1076 e.redraw = true 1077 case "c:19": // ctrl-s, save 1078 e.UserSave(c, tty, status) 1079 case "c:21", "c:26": // ctrl-u or ctrl-z, undo (ctrl-z may background the application) 1080 // Forget the cut, copy and paste line state 1081 lastCutY = -1 1082 lastPasteY = -1 1083 lastCopyY = -1 1084 1085 // Try to restore the previous editor state in the undo buffer 1086 if err := undo.Restore(e); err == nil { 1087 //c.Draw() 1088 x := e.pos.ScreenX() 1089 y := e.pos.ScreenY() 1090 vt100.SetXY(uint(x), uint(y)) 1091 e.redrawCursor = true 1092 e.redraw = true 1093 } else { 1094 status.SetMessage("Nothing more to undo") 1095 status.Show(c, e) 1096 } 1097 case "c:12": // ctrl-l, go to line number 1098 status.ClearAll(c) 1099 status.SetMessage("Go to line number:") 1100 status.ShowNoTimeout(c, e) 1101 lns := "" 1102 cancel := false 1103 doneCollectingDigits := false 1104 goToEnd := false 1105 goToTop := false 1106 goToCenter := false 1107 for !doneCollectingDigits { 1108 numkey := tty.String() 1109 switch numkey { 1110 case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": // 0 .. 9 1111 lns += numkey // string('0' + (numkey - 48)) 1112 status.SetMessage("Go to line number: " + lns) 1113 status.ShowNoTimeout(c, e) 1114 case "c:8", "c:127": // ctrl-h or backspace 1115 if len(lns) > 0 { 1116 lns = lns[:len(lns)-1] 1117 status.SetMessage("Go to line number: " + lns) 1118 status.ShowNoTimeout(c, e) 1119 } 1120 case "b", "t": // top of file 1121 doneCollectingDigits = true 1122 goToTop = true 1123 case "e": // end of file 1124 doneCollectingDigits = true 1125 goToEnd = true 1126 case "c", "m": // center of file 1127 doneCollectingDigits = true 1128 goToCenter = true 1129 case "↑", "↓": // up arrow or down arrow 1130 fallthrough // cancel 1131 case "c:27", "c:17": // esc or ctrl-q 1132 cancel = true 1133 lns = "" 1134 fallthrough // done 1135 case "c:13": // return 1136 doneCollectingDigits = true 1137 } 1138 } 1139 if !cancel { 1140 e.ClearSearchTerm() 1141 } 1142 status.ClearAll(c) 1143 if goToTop { 1144 // Go to the first line (by line number, not by index) 1145 e.redraw = e.GoToLineNumber(1, c, status, true) 1146 } else if goToCenter { 1147 // Go to the center line 1148 e.GoToLineNumber(LineNumber(e.Len()/2), c, status, true) 1149 } else if goToEnd { 1150 // Go to the last line (by line number, not by index, e.Len() returns an index which is why there is no -1) 1151 e.redraw = e.GoToLineNumber(LineNumber(e.Len()), c, status, true) 1152 } else if lns == "" && !cancel { 1153 if e.DataY() > 0 { 1154 // If not at the top, go to the first line (by line number, not by index) 1155 e.redraw = e.GoToLineNumber(1, c, status, true) 1156 } else { 1157 // Go to the last line 1158 e.redraw = e.GoToLineNumber(LineNumber(e.Len()), c, status, true) 1159 } 1160 } else { 1161 // Go to the specified line 1162 if ln, err := strconv.Atoi(lns); err == nil { // no error 1163 e.redraw = e.GoToLineNumber(LineNumber(ln), c, status, true) 1164 } 1165 } 1166 e.redrawCursor = true 1167 case "c:24": // ctrl-x, cut line 1168 y := e.DataY() 1169 line := e.Line(y) 1170 // Prepare to cut 1171 undo.Snapshot(e) 1172 // Now check if there is anything to cut 1173 if len(strings.TrimSpace(line)) == 0 { 1174 // Nothing to cut, just remove the current line 1175 e.Home() 1176 e.DeleteCurrentLineMoveBookmark(bookmark) 1177 // Check if ctrl-x was pressed once or twice, for this line 1178 } else if lastCutY != y { // Single line cut 1179 // Also close the portal, if any 1180 ClosePortal(e) 1181 1182 lastCutY = y 1183 lastCopyY = -1 1184 lastPasteY = -1 1185 // Copy the line internally 1186 copyLines = []string{line} 1187 1188 // Copy the line to the clipboard 1189 err = clipboard.WriteAll(line) 1190 if err == nil { 1191 // no issue 1192 } else if firstCopyAction { 1193 missingUtility := false 1194 1195 if env.Has("WAYLAND_DISPLAY") { // Wayland 1196 if which("wl-copy") == "" { 1197 status.SetErrorMessage("The wl-copy utility (from wl-clipboard) is missing!") 1198 missingUtility = true 1199 } 1200 } else { 1201 if which("xclip") == "" { 1202 status.SetErrorMessage("The xclip utility is missing!") 1203 missingUtility = true 1204 } 1205 } 1206 1207 // TODO 1208 _ = missingUtility 1209 } 1210 1211 // Delete the line 1212 e.DeleteLineMoveBookmark(y, bookmark) 1213 } else { // Multi line cut (add to the clipboard, since it's the second press) 1214 lastCutY = y 1215 lastCopyY = -1 1216 lastPasteY = -1 1217 1218 // Also close the portal, if any 1219 ClosePortal(e) 1220 1221 s := e.Block(y) 1222 lines := strings.Split(s, "\n") 1223 if len(lines) < 1 { 1224 // Need at least 1 line to be able to cut "the rest" after the first line has been cut 1225 break 1226 } 1227 copyLines = append(copyLines, lines...) 1228 s = strings.Join(copyLines, "\n") 1229 // Place the block of text in the clipboard 1230 _ = clipboard.WriteAll(s) 1231 // Delete the corresponding number of lines 1232 for range lines { 1233 e.DeleteLineMoveBookmark(y, bookmark) 1234 } 1235 } 1236 // Go to the end of the current line 1237 e.End(c) 1238 // No status message is needed for the cut operation, because it's visible that lines are cut 1239 e.redrawCursor = true 1240 e.redraw = true 1241 case "c:11": // ctrl-k, delete to end of line 1242 if e.Empty() { 1243 status.SetMessage("Empty file") 1244 status.Show(c, e) 1245 break 1246 } 1247 1248 // Reset the cut/copy/paste double-keypress detection 1249 lastCopyY = -1 1250 lastPasteY = -1 1251 lastCutY = -1 1252 1253 undo.Snapshot(e) 1254 1255 e.DeleteRestOfLine() 1256 if e.EmptyRightTrimmedLine() { 1257 // Deleting the rest of the line cleared this line, 1258 // so just remove it. 1259 e.DeleteCurrentLineMoveBookmark(bookmark) 1260 // Then go to the end of the line, if needed 1261 if e.AfterEndOfLine() { 1262 e.End(c) 1263 } 1264 } 1265 1266 // TODO: Is this one needed/useful? 1267 vt100.Do("Erase End of Line") 1268 1269 e.redraw = true 1270 e.redrawCursor = true 1271 case "c:3": // ctrl-c, copy the stripped contents of the current line 1272 y := e.DataY() 1273 1274 // Forget the cut and paste line state 1275 lastCutY = -1 1276 lastPasteY = -1 1277 1278 // check if this operation is done on the same line as last time 1279 singleLineCopy := lastCopyY != y 1280 lastCopyY = y 1281 1282 // close the portal, if any 1283 closedPortal := ClosePortal(e) == nil 1284 1285 if singleLineCopy { // Single line copy 1286 status.Clear(c) 1287 // Pressed for the first time for this line number 1288 trimmed := strings.TrimSpace(e.Line(y)) 1289 if trimmed != "" { 1290 // Copy the line to the internal clipboard 1291 copyLines = []string{trimmed} 1292 // Copy the line to the clipboard 1293 s := "Copied 1 line" 1294 if err := clipboard.WriteAll(strings.Join(copyLines, "\n")); err == nil { // OK 1295 // The copy operation worked out, using the clipboard 1296 s += " from the clipboard" 1297 } 1298 // The portal was closed? 1299 if closedPortal { 1300 s += " and closed the portal" 1301 } 1302 status.SetMessage(s) 1303 status.Show(c, e) 1304 // Go to the end of the line, for easy line duplication with ctrl-c, enter, ctrl-v, 1305 // but only if the copied line is shorter than the terminal width. 1306 if uint(len(trimmed)) < c.Width() { 1307 e.End(c) 1308 } 1309 } 1310 } else { // Multi line copy 1311 // Pressed multiple times for this line number, copy the block of text starting from this line 1312 s := e.Block(y) 1313 if s != "" { 1314 copyLines = strings.Split(s, "\n") 1315 // Prepare a status message 1316 plural := "" 1317 lineCount := strings.Count(s, "\n") 1318 if lineCount > 1 { 1319 plural = "s" 1320 } 1321 // Place the block of text in the clipboard 1322 err := clipboard.WriteAll(s) 1323 if err != nil { 1324 status.SetMessage(fmt.Sprintf("Copied %d line%s", lineCount, plural)) 1325 } else { 1326 status.SetMessage(fmt.Sprintf("Copied %d line%s (clipboard)", lineCount, plural)) 1327 } 1328 status.Show(c, e) 1329 } 1330 } 1331 case "c:22": // ctrl-v, paste 1332 1333 var ( 1334 gotLineFromPortal bool 1335 line string 1336 ) 1337 1338 if portal, err := LoadPortal(); err == nil { // no error 1339 line, err = portal.PopLine(e, false) // pop the line, but don't remove it from the source file 1340 status.Clear(c) 1341 if err != nil { 1342 // status.SetErrorMessage("Could not copy text through the portal.") 1343 status.SetErrorMessage(err.Error()) 1344 ClosePortal(e) 1345 } else { 1346 status.SetMessage(fmt.Sprintf("Using portal at %s\n", portal)) 1347 gotLineFromPortal = true 1348 } 1349 status.Show(c, e) 1350 } 1351 if gotLineFromPortal { 1352 1353 undo.Snapshot(e) 1354 1355 if e.EmptyRightTrimmedLine() { 1356 // If the line is empty, replace with the string from the portal 1357 e.SetCurrentLine(line) 1358 } else { 1359 // If the line is not empty, insert the trimmed string 1360 e.InsertStringAndMove(c, strings.TrimSpace(line)) 1361 } 1362 1363 e.InsertLineBelow() 1364 e.Down(c, nil) // no status message if the end of document is reached, there should always be a new line 1365 1366 e.redraw = true 1367 1368 break 1369 } // errors with loading a portal are ignored 1370 1371 // This may only work for the same user, and not with sudo/su 1372 1373 // Try fetching the lines from the clipboard first 1374 s, err := clipboard.ReadAll() 1375 if err == nil { // no error 1376 1377 // Make the replacements, then split the text into lines and store it in "copyLines" 1378 copyLines = strings.Split(opinionatedStringReplacer.Replace(s), "\n") 1379 1380 // Note that control characters are not replaced, they are just not printed. 1381 } else if firstPasteAction { 1382 missingUtility := false 1383 1384 status.Clear(c) 1385 1386 if env.Has("WAYLAND_DISPLAY") { // Wayland 1387 if which("wl-paste") == "" { 1388 status.SetErrorMessage("The wl-paste utility (from wl-clipboard) is missing!") 1389 missingUtility = true 1390 } 1391 } else { 1392 if which("xclip") == "" { 1393 status.SetErrorMessage("The xclip utility is missing!") 1394 missingUtility = true 1395 } 1396 } 1397 1398 if missingUtility && firstPasteAction { 1399 firstPasteAction = false 1400 status.Show(c, e) 1401 break // Break instead of pasting from the internal buffer, but only the first time 1402 } 1403 } else { 1404 status.Clear(c) 1405 e.redrawCursor = true 1406 } 1407 1408 // Now check if there is anything to paste 1409 if len(copyLines) == 0 { 1410 break 1411 } 1412 1413 // Now save the contents to "previousCopyLines" and check if they are the same first 1414 if !equalStringSlices(copyLines, previousCopyLines) { 1415 // Start with single-line paste if the contents are new 1416 lastPasteY = -1 1417 } 1418 previousCopyLines = copyLines 1419 1420 // Prepare to paste 1421 undo.Snapshot(e) 1422 y := e.DataY() 1423 1424 // Forget the cut and copy line state 1425 lastCutY = -1 1426 lastCopyY = -1 1427 1428 // Redraw after pasting 1429 e.redraw = true 1430 1431 if lastPasteY != y { // Single line paste 1432 lastPasteY = y 1433 // Pressed for the first time for this line number, paste only one line 1434 1435 // copyLines[0] is the line to be pasted, and it exists 1436 1437 if e.EmptyRightTrimmedLine() { 1438 // If the line is empty, use the existing indentation before pasting 1439 e.SetLine(y, e.LeadingWhitespace()+strings.TrimSpace(copyLines[0])) 1440 } else { 1441 // If the line is not empty, insert the trimmed string 1442 e.InsertStringAndMove(c, strings.TrimSpace(copyLines[0])) 1443 } 1444 1445 } else { // Multi line paste (the rest of the lines) 1446 // Pressed the second time for this line number, paste multiple lines without trimming 1447 var ( 1448 // copyLines contains the lines to be pasted, and they are > 1 1449 // the first line is skipped since that was already pasted when ctrl-v was pressed the first time 1450 lastIndex = len(copyLines[1:]) - 1 1451 1452 // If the first line has been pasted, and return has been pressed, paste the rest of the lines differently 1453 skipFirstLineInsert bool 1454 ) 1455 1456 if previousKey != "c:13" { 1457 // Start by pasting (and overwriting) an untrimmed version of this line, 1458 // if the previous key was not return. 1459 e.SetLine(y, copyLines[0]) 1460 } else if e.EmptyRightTrimmedLine() { 1461 skipFirstLineInsert = true 1462 } 1463 1464 // The paste the rest of the lines, also untrimmed 1465 for i, line := range copyLines[1:] { 1466 if i == lastIndex && len(strings.TrimSpace(line)) == 0 { 1467 // If the last line is blank, skip it 1468 break 1469 } 1470 if skipFirstLineInsert { 1471 skipFirstLineInsert = false 1472 } else { 1473 e.InsertLineBelow() 1474 e.Down(c, nil) // no status message if the end of document is reached, there should always be a new line 1475 } 1476 e.InsertStringAndMove(c, line) 1477 } 1478 } 1479 // Prepare to redraw the text 1480 e.redrawCursor = true 1481 e.redraw = true 1482 case "c:18": // ctrl-r, to open or close a portal 1483 1484 // Are we in git mode? 1485 if line := e.CurrentLine(); e.mode == mode.Git && hasAnyPrefixWord(line, gitRebasePrefixes) { 1486 undo.Snapshot(e) 1487 newLine := nextGitRebaseKeyword(line) 1488 e.SetCurrentLine(newLine) 1489 e.redraw = true 1490 e.redrawCursor = true 1491 break 1492 } 1493 1494 // Deal with the portal 1495 status.Clear(c) 1496 if HasPortal() { 1497 status.SetMessage("Closing portal") 1498 ClosePortal(e) 1499 } else { 1500 portal, err := e.NewPortal() 1501 if err != nil { 1502 status.SetErrorMessage(err.Error()) 1503 status.Show(c, e) 1504 break 1505 } 1506 // Portals in the same file is a special case, since lines may move around when pasting 1507 if portal.SameFile(e) { 1508 e.sameFilePortal = portal 1509 } 1510 if err := portal.Save(); err != nil { 1511 status.SetErrorMessage(err.Error()) 1512 status.Show(c, e) 1513 break 1514 } 1515 status.SetMessage("Opening a portal at " + portal.String()) 1516 } 1517 status.Show(c, e) 1518 case "c:2": // ctrl-b, bookmark, unbookmark or jump to bookmark, toggle breakpoint if in debug mode 1519 status.Clear(c) 1520 if e.debugMode { 1521 if breakpoint == nil { 1522 breakpoint = e.pos.Copy() 1523 s := "Placed breakpoint at line " + e.LineNumber().String() 1524 status.SetMessage(" " + s + " ") 1525 } else if breakpoint.LineNumber() == e.LineNumber() { 1526 // setting a breakpoint at the same line twice: remove the breakpoint 1527 s := "Removed breakpoint at line " + breakpoint.LineNumber().String() 1528 status.SetMessage(s) 1529 breakpoint = nil 1530 } else { 1531 undo.Snapshot(e) 1532 // Go to the breakpoint position 1533 e.GoToPosition(c, status, *breakpoint) 1534 // Do the redraw manually before showing the status message 1535 e.DrawLines(c, true, false) 1536 e.redraw = false 1537 // SHow the status message 1538 s := "Jumped to breakpoint at line " + e.LineNumber().String() 1539 status.SetMessage(s) 1540 } 1541 } else { 1542 if bookmark == nil { 1543 // no bookmark, create a bookmark at the current line 1544 bookmark = e.pos.Copy() 1545 // TODO: Modify the statusbar implementation so that extra spaces are not needed here. 1546 s := "Bookmarked line " + e.LineNumber().String() 1547 status.SetMessage(" " + s + " ") 1548 } else if bookmark.LineNumber() == e.LineNumber() { 1549 // bookmarking the same line twice: remove the bookmark 1550 s := "Removed bookmark for line " + bookmark.LineNumber().String() 1551 status.SetMessage(s) 1552 bookmark = nil 1553 } else { 1554 undo.Snapshot(e) 1555 // Go to the saved bookmark position 1556 e.GoToPosition(c, status, *bookmark) 1557 // Do the redraw manually before showing the status message 1558 e.DrawLines(c, true, false) 1559 e.redraw = false 1560 // Show the status message 1561 s := "Jumped to bookmark at line " + e.LineNumber().String() 1562 status.SetMessage(s) 1563 } 1564 } 1565 status.Show(c, e) 1566 e.redrawCursor = true 1567 case "c:10": // ctrl-j, join line 1568 if e.Empty() { 1569 status.SetMessage("Empty") 1570 status.Show(c, e) 1571 } else { 1572 undo.Snapshot(e) 1573 1574 nextLineIndex := e.DataY() + 1 1575 if e.EmptyRightTrimmedLineBelow() { 1576 // Just delete the line below if it's empty 1577 e.DeleteLineMoveBookmark(nextLineIndex, bookmark) 1578 } else { 1579 // Join the line below with this line. Also add a space in between. 1580 e.TrimLeft(nextLineIndex) // this is unproblematic, even at the end of the document 1581 e.End(c) 1582 e.InsertRune(c, ' ') 1583 e.WriteRune(c) 1584 e.Next(c) 1585 e.Delete() 1586 } 1587 1588 e.redraw = true 1589 } 1590 e.redrawCursor = true 1591 default: // any other key 1592 //panic(fmt.Sprintf("PRESSED KEY: %v", []rune(key))) 1593 if len([]rune(key)) > 0 && unicode.IsLetter([]rune(key)[0]) { // letter 1594 1595 undo.Snapshot(e) 1596 1597 // Type the letter that was pressed 1598 if len([]rune(key)) > 0 { 1599 // Insert a letter. This is what normally happens. 1600 wrapped := e.InsertRune(c, []rune(key)[0]) 1601 if !wrapped { 1602 e.WriteRune(c) 1603 e.Next(c) 1604 } 1605 e.redraw = true 1606 } 1607 } else if len([]rune(key)) > 0 && unicode.IsGraphic([]rune(key)[0]) { // any other key that can be drawn 1608 undo.Snapshot(e) 1609 e.redraw = true 1610 1611 // Place *something* 1612 r := []rune(key)[0] 1613 1614 if r == 160 { 1615 // This is a nonbreaking space that may be inserted with altgr+space that is HORRIBLE. 1616 // Set r to a regular space instead. 1617 r = ' ' 1618 } 1619 1620 // "smart dedent" 1621 if r == '}' || r == ']' || r == ')' { 1622 1623 // Normally, dedent once, but there are exceptions 1624 1625 noContentHereAlready := len(e.TrimmedLine()) == 0 1626 leadingWhitespace := e.LeadingWhitespace() 1627 nextLineContents := e.Line(e.DataY() + 1) 1628 1629 currentX := e.pos.sx 1630 1631 foundCurlyBracketBelow := currentX-1 == strings.Index(nextLineContents, "}") 1632 foundSquareBracketBelow := currentX-1 == strings.Index(nextLineContents, "]") 1633 foundParenthesisBelow := currentX-1 == strings.Index(nextLineContents, ")") 1634 1635 noDedent := foundCurlyBracketBelow || foundSquareBracketBelow || foundParenthesisBelow 1636 1637 //noDedent := similarLineBelow 1638 1639 // Okay, dedent this line by 1 indendation, if possible 1640 if !noDedent && e.pos.sx > 0 && len(leadingWhitespace) > 0 && noContentHereAlready { 1641 newLeadingWhitespace := leadingWhitespace 1642 if strings.HasSuffix(leadingWhitespace, "\t") { 1643 newLeadingWhitespace = leadingWhitespace[:len(leadingWhitespace)-1] 1644 e.pos.sx -= e.tabsSpaces.PerTab 1645 } else if strings.HasSuffix(leadingWhitespace, strings.Repeat(" ", e.tabsSpaces.PerTab)) { 1646 newLeadingWhitespace = leadingWhitespace[:len(leadingWhitespace)-e.tabsSpaces.PerTab] 1647 e.pos.sx -= e.tabsSpaces.PerTab 1648 } 1649 e.SetCurrentLine(newLeadingWhitespace) 1650 } 1651 } 1652 1653 wrapped := e.InsertRune(c, r) 1654 e.WriteRune(c) 1655 if !wrapped && len(string(r)) > 0 { 1656 // Move to the next position 1657 e.Next(c) 1658 } 1659 e.redrawCursor = true 1660 } 1661 } 1662 previousKey = key 1663 1664 // Clear status, if needed 1665 if statusMode && e.redrawCursor { 1666 status.ClearAll(c) 1667 } 1668 1669 // Draw and/or redraw everything, with slightly different behavior over ssh 1670 e.RedrawAtEndOfKeyLoop(c, status, &statusMessage) 1671 1672 } // end of main loop 1673 1674 if canUseLocks { 1675 // Start by loading the lock overview, just in case something has happened in the mean time 1676 fileLock.Load() 1677 1678 // Check if the lock is unchanged 1679 fileLockTimestamp := fileLock.GetTimestamp(absFilename) 1680 lockUnchanged := lockTimestamp == fileLockTimestamp 1681 1682 // TODO: If the stored timestamp is older than uptime, unlock and save the lock overview 1683 1684 //var notime time.Time 1685 1686 if !forceFlag || lockUnchanged { 1687 // If the file has not been locked externally since this instance of the editor was loaded, don't 1688 // Unlock the current file and save the lock overview. Ignore errors because they are not critical. 1689 fileLock.Unlock(absFilename) 1690 fileLock.Save() 1691 } 1692 } 1693 1694 // Save the current location in the location history and write it to file 1695 e.SaveLocation(absFilename, e.locationHistory) 1696 1697 // Clear all status bar messages 1698 status.ClearAll(c) 1699 1700 // Quit everything that has to do with the terminal 1701 if e.clearOnQuit { 1702 vt100.Clear() 1703 vt100.Close() 1704 } else { 1705 c.Draw() 1706 fmt.Println() 1707 } 1708 1709 // All done 1710 return "", nil 1711} 1712