github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/editor.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "sort" 10 "strconv" 11 "strings" 12 "time" 13 "unicode" 14 "unicode/utf8" 15 16 "github.com/cyrus-and/gdb" 17 "github.com/xyproto/clip" 18 "github.com/xyproto/env/v2" 19 "github.com/xyproto/files" 20 "github.com/xyproto/mode" 21 "github.com/xyproto/vt100" 22 ) 23 24 // Editor represents the contents and editor settings, but not settings related to the viewport or scrolling 25 type Editor struct { 26 detectedTabs *bool // were tab or space indentations detected when loading the data? 27 breakpoint *Position // for the breakpoint/jump functionality in debug mode 28 gdb *gdb.Gdb // connection to gdb, if debugMode is enabled 29 sameFilePortal *Portal // a portal that points to the same file 30 lines map[int][]rune // the contents of the current document 31 macro *Macro // the contents of the current macro (will be cleared when esc is pressed) 32 filename string // the current filename 33 searchTerm string // the current search term, used when searching 34 stickySearchTerm string // used when going to the next match with ctrl-n, unless esc has been pressed 35 Theme // editor theme, embedded struct 36 pos Position // the current cursor and scroll position 37 indentation mode.TabsSpaces // spaces or tabs, and how many spaces per tab character 38 wrapWidth int // set to ie. 80 or 100 to trigger word wrap when typing to that column 39 mode mode.Mode // a filetype mode, like for git, markdown or various programming languages 40 debugShowRegisters int // show no register box, show changed registers, show all changed registers 41 previousY int // previous cursor position 42 previousX int // previous cursor position 43 lineBeforeSearch LineIndex // save the current line number before jumping between search results 44 playBackMacroCount int // number of times the macro should be played back, right now 45 rainbowParenthesis bool // rainbow parenthesis 46 redraw bool // if the contents should be redrawn in the next loop 47 sshMode bool // is o used over ssh, tmux or screen, in a way that usually requires extra redrawing? 48 debugMode bool // in a mode where ctrl-b toggles breakpoints, ctrl-n steps to the next line and ctrl-space runs the application 49 statusMode bool // display a status line at all times at the bottom of the screen 50 expandTags bool // can be used for XML and HTML 51 syntaxHighlight bool // syntax highlighting 52 stopParentOnQuit bool // send SIGQUIT to the parent PID when quitting 53 clearOnQuit bool // clear the terminal when quitting the editor, or not 54 quit bool // for indicating if the user wants to end the editor session 55 changed bool // has the contents changed, since last save? 56 readOnly bool // is the file read-only when initializing o? 57 debugHideOutput bool // hide the GDB stdout pane when in debug mode? 58 binaryFile bool // is this a binary file, or a text file? 59 wrapWhenTyping bool // wrap text at a certain limit when typing 60 addSpace bool // add a space to the editor, once 61 debugStepInto bool // when stepping to the next instruction, step into instead of over 62 slowLoad bool // was the initial file slow to load? (might be an indication of a slow disk or USB stick) 63 building bool // currently buildig code or exporting to a file? 64 runAfterBuild bool // run the application after building? 65 generatingTokens bool // is code or text being generated right now? 66 redrawCursor bool // if the cursor should be moved to the location it is supposed to be 67 fixAsYouType bool // fix each line as you type it in, using AI? 68 monitorAndReadOnly bool // monitor the file for changes and open it as read-only 69 primaryClipboard bool // use the primary or the secondary clipboard on UNIX? 70 jumpToLetterMode bool // jump directly to a highlighted letter 71 nanoMode bool // emulate GNU Nano 72 spellCheckMode bool // spell check mode? 73 createDirectoriesIfMissing bool // when saving a file, should directories be created if they are missing? 74 displayQuickHelp bool // display the quick help box? 75 drawProgress bool // used for drawing the progress character on the right side 76 } 77 78 // CopyLines will create a new map[int][]rune struct that is the copy of all the lines in the editor 79 func (e *Editor) CopyLines() map[int][]rune { 80 lines2 := make(map[int][]rune) 81 for key, runes := range e.lines { 82 runes2 := make([]rune, len(runes)) 83 copy(runes2, runes) 84 lines2[key] = runes2 85 } 86 return lines2 87 } 88 89 // Set will store a rune in the editor data, at the given data coordinates 90 func (e *Editor) Set(x int, index LineIndex, r rune) { 91 y := int(index) 92 if e.lines == nil { 93 e.lines = make(map[int][]rune) 94 } 95 _, ok := e.lines[y] 96 if !ok { 97 e.lines[y] = make([]rune, 0, x+1) 98 } 99 l := len(e.lines[y]) 100 if x < l { 101 e.lines[y][x] = r 102 e.changed = true 103 return 104 } 105 // If the line is too short, fill it up with spaces 106 if l <= x { 107 n := (x + 1) - l 108 e.lines[y] = append(e.lines[y], []rune(strings.Repeat(" ", n))...) 109 } 110 111 // Set the rune 112 e.lines[y][x] = r 113 e.changed = true 114 } 115 116 // Get will retrieve a rune from the editor data, at the given coordinates 117 func (e *Editor) Get(x int, y LineIndex) rune { 118 if e.lines == nil { 119 return ' ' 120 } 121 runes, ok := e.lines[int(y)] 122 if !ok { 123 return ' ' 124 } 125 if x >= len(runes) { 126 return ' ' 127 } 128 return runes[x] 129 } 130 131 // Changed will return true if the contents were changed since last time this function was called 132 func (e *Editor) Changed() bool { 133 return e.changed 134 } 135 136 // Line returns the contents of line number N, counting from 0 137 func (e *Editor) Line(n LineIndex) string { 138 line, ok := e.lines[int(n)] 139 if !ok { 140 return "" 141 } 142 return string(line) 143 } 144 145 // ScreenLine returns the screen contents of line number N, counting from 0. 146 // The tabs are expanded. 147 func (e *Editor) ScreenLine(n int) string { 148 line, ok := e.lines[n] 149 if ok { 150 var sb strings.Builder 151 skipX := e.pos.offsetX 152 for _, r := range line { 153 if skipX > 0 { 154 skipX-- 155 continue 156 } 157 sb.WriteRune(r) 158 } 159 tabSpace := strings.Repeat("\t", e.indentation.PerTab) 160 return strings.ReplaceAll(sb.String(), "\t", tabSpace) 161 } 162 return "" 163 } 164 165 // LastDataPosition returns the last X index for this line, for the data (does not expand tabs) 166 // Can be negative, if the line is empty. 167 func (e *Editor) LastDataPosition(n LineIndex) int { 168 return utf8.RuneCountInString(e.Line(n)) - 1 169 } 170 171 // LastScreenPosition returns the last X index for this line, for the screen (expands tabs) 172 // Can be negative, if the line is empty. 173 func (e *Editor) LastScreenPosition(n LineIndex) int { 174 extraSpaceBecauseOfTabs := int(e.CountRune('\t', n) * (e.indentation.PerTab - 1)) 175 return (e.LastDataPosition(n) + extraSpaceBecauseOfTabs) 176 } 177 178 // LastTextPosition returns the last X index for this line, regardless of horizontal scrolling. 179 // Can be negative if the line is empty. Tabs are expanded. 180 func (e *Editor) LastTextPosition(n LineIndex) int { 181 extraSpaceBecauseOfTabs := int(e.CountRune('\t', n) * (e.indentation.PerTab - 1)) 182 return (e.LastDataPosition(n) + extraSpaceBecauseOfTabs) 183 } 184 185 // FirstScreenPosition returns the first X index for this line, that is not '\t' or ' '. 186 // Does not deal with the X offset. 187 func (e *Editor) FirstScreenPosition(n LineIndex) uint { 188 var ( 189 counter uint 190 spacesPerTab = uint(e.indentation.PerTab) 191 ) 192 for _, r := range e.Line(n) { 193 if r == '\t' { 194 counter += spacesPerTab 195 } else if r == ' ' { 196 counter++ 197 } else { 198 break 199 } 200 } 201 return counter 202 } 203 204 // FirstDataPosition returns the first X index for this line, that is not whitespace. 205 func (e *Editor) FirstDataPosition(n LineIndex) int { 206 counter := 0 207 for _, r := range e.Line(n) { 208 if !unicode.IsSpace(r) { 209 break 210 } 211 counter++ 212 } 213 return counter 214 } 215 216 // CountRune will count the number of instances of the rune r in the line n 217 func (e *Editor) CountRune(r rune, n LineIndex) int { 218 var counter int 219 line, ok := e.lines[int(n)] 220 if ok { 221 for _, l := range line { 222 if l == r { 223 counter++ 224 } 225 } 226 } 227 return counter 228 } 229 230 // Len returns the number of lines 231 func (e *Editor) Len() int { 232 return len(e.lines) 233 } 234 235 // String returns the contents of the editor 236 func (e *Editor) String() string { 237 var sb strings.Builder 238 l := e.Len() 239 for i := 0; i < l; i++ { 240 sb.WriteString(e.Line(LineIndex(i)) + "\n") 241 } 242 return sb.String() 243 } 244 245 // ContentsAndReverseSearchPrefix returns the contents of the editor, 246 // and also the LineNumber of the given string, searching for the prefix backwards from the current position. 247 // Also returns true if the given string was found. Used for the "iferr" feature in keyloop.go. 248 func (e *Editor) ContentsAndReverseSearchPrefix(prefix string) (string, LineIndex, bool) { 249 currentLineIndex := e.LineIndex() 250 foundLineIndex := currentLineIndex 251 foundIt := false 252 var sb strings.Builder 253 l := e.Len() 254 for i := 0; i < l; i++ { 255 lineIndex := LineIndex(i) 256 line := e.Line(lineIndex) 257 if lineIndex <= currentLineIndex && strings.HasPrefix(strings.TrimSpace(line), prefix) { 258 // Found it, and it's above the currentLineIndex 259 foundLineIndex = lineIndex 260 foundIt = true 261 } 262 sb.WriteString(line + "\n") 263 } 264 return sb.String(), foundLineIndex, foundIt 265 } 266 267 // Clear removes all data from the editor 268 func (e *Editor) Clear() { 269 e.lines = make(map[int][]rune) 270 e.changed = true 271 } 272 273 // Load will try to load a file. The file is assumed to be checked to already exist. 274 // Returns a warning message (possibly empty) and an error type 275 func (e *Editor) Load(c *vt100.Canvas, tty *vt100.TTY, fnord FilenameOrData) (string, error) { 276 var ( 277 message string 278 err error 279 ) 280 281 // Start a spinner, in a short while 282 quitChan := Spinner(c, tty, fmt.Sprintf("Reading %s... ", fnord.filename), fmt.Sprintf("reading %s: stopped by user", fnord.filename), 200*time.Millisecond, e.ItalicsColor) 283 284 // Stop the spinner at the end of the function 285 defer func() { 286 quitChan <- true 287 }() 288 289 start := time.Now() 290 291 // Check if the file extension is ".class" and if "jad" is installed 292 if filepath.Ext(fnord.filename) == ".class" && files.Which("jad") != "" && fnord.Empty() { 293 if fnord.data, err = e.LoadClass(fnord.filename); err != nil { 294 return "Could not run jad", err 295 } 296 // Load the data (and make opinionated replacements if it's a text file + set e.binaryFile if it's binary) 297 e.LoadBytes(fnord.data) 298 } else if fnord.stdin { 299 // Load the data that has already been read from stdin 300 // (and make opinionated replacements if it's a text file + set e.binaryFile if it's binary) 301 e.LoadBytes(fnord.data) 302 } else if fnord.Empty() { 303 // Load the file (and make opinionated replacements if it's a text file + set e.binaryFile if it's binary) 304 if err := e.ReadFileAndProcessLines(fnord.filename); err != nil { 305 return message, err 306 } 307 } 308 309 if e.binaryFile { 310 e.mode = mode.Blank 311 } 312 313 // If enough time passed so that the spinner was shown by now, enter "slow disk mode" where fewer disk-related I/O operations will be performed 314 e.slowLoad = time.Since(start) > 400*time.Millisecond 315 316 // Mark the data as "not changed", since this happens when starting the editor 317 e.changed = false 318 319 return message, nil 320 } 321 322 // IndexByteLine represents a single line of text, as bytes and with a line index 323 type IndexByteLine struct { 324 byteLine []byte 325 index int 326 } 327 328 // PrepareEmpty prepares an empty textual representation of a given filename. 329 // If it's an image, there will be text placeholders for pixels. 330 // If it's anything else, it will just be blank. 331 // Returns an editor mode and an error type. 332 func (e *Editor) PrepareEmpty() (mode.Mode, error) { 333 var ( 334 m mode.Mode = mode.Blank 335 data []byte 336 err error 337 ) 338 339 // Check if the data could be prepared 340 if err != nil { 341 return m, err 342 } 343 344 lines := strings.Split(string(data), "\n") 345 e.Clear() 346 for y, line := range lines { 347 counter := 0 348 for _, letter := range line { 349 e.Set(counter, LineIndex(y), letter) 350 counter++ 351 } 352 } 353 // Mark the data as "not changed" 354 e.changed = false 355 356 return m, nil 357 } 358 359 // Save will try to save the current editor contents to file. 360 // It needs a canvas in case trailing spaces are stripped and the cursor needs to move to the end. 361 func (e *Editor) Save(c *vt100.Canvas, tty *vt100.TTY) error { 362 if e.monitorAndReadOnly { 363 return errors.New("file is read-only") 364 } 365 366 var ( 367 bookmark = e.pos.Copy() // Save the current position 368 changed bool 369 shebang bool 370 data []byte 371 ) 372 373 quitMut.Lock() 374 defer quitMut.Unlock() 375 376 if e.createDirectoriesIfMissing { 377 if err := os.MkdirAll(filepath.Dir(e.filename), os.ModePerm); err != nil { 378 return err 379 } 380 } 381 382 if e.binaryFile { 383 data = []byte(e.String()) 384 } else { 385 // Strip trailing spaces on all lines 386 l := e.Len() 387 for i := 0; i < l; i++ { 388 if e.TrimRight(LineIndex(i)) { 389 changed = true 390 } 391 } 392 393 // Trim away trailing whitespace 394 s := trimRightSpace(e.String()) 395 396 // Make additional replacements, and add a final newline 397 s = opinionatedStringReplacer.Replace(s) + "\n" 398 399 // TODO: Auto-detect tabs/spaces instead of per-language assumptions 400 if e.mode.Spaces() { 401 // NOTE: This is a hack, that can only replace 10 levels deep. 402 for level := 10; level > 0; level-- { 403 fromString := "\n" + strings.Repeat("\t", level) 404 toString := "\n" + strings.Repeat(" ", level*e.indentation.PerTab) 405 s = strings.ReplaceAll(s, fromString, toString) 406 } 407 } else if e.mode == mode.Make || e.mode == mode.Just { 408 // NOTE: This is a hack, that can only replace 10 levels deep. 409 for level := 10; level > 0; level-- { 410 fromString := "\n" + strings.Repeat(" ", level*e.indentation.PerTab) 411 toString := "\n" + strings.Repeat("\t", level) 412 s = strings.ReplaceAll(s, fromString, toString) 413 } 414 } 415 416 // Should the file be saved with the executable bit enabled? 417 // (Does it either start with a shebang or reside in a common bin directory like /usr/bin?) 418 shebang = files.BinDirectory(e.filename) || strings.HasPrefix(s, "#!") 419 420 data = []byte(s) 421 } 422 423 // Mark the data as "not changed" if it's not a binary file 424 if !e.binaryFile { 425 e.changed = false 426 } 427 428 // Default file mode (0644 for regular files, 0755 for executable files) 429 var fileMode os.FileMode = 0o644 430 431 // Shell scripts that contains the word "source" typically needs to be sourced and should not be "chmod +x"-ed, nor "chmod -x" ed 432 containsTheWordSource := bytes.Contains(data, []byte("source ")) 433 434 // Checking the syntax highlighting makes it easy to press `ctrl-t` before saving a script, 435 // to toggle the executable bit on or off. This is only for files that start with "#!". 436 // Also, if the file is in one of the common bin directories, like "/usr/bin", then assume that it 437 // is supposed to be executable. 438 if shebang && e.syntaxHighlight && !containsTheWordSource { 439 // This is a script file, syntax highlighting is enabled and it does not contain the word "source " 440 // (typical for shell files that should be sourced and not executed) 441 fileMode = 0o755 442 } 443 444 // If it's not a binary file OR the file has changed: save the data 445 if !e.binaryFile || e.changed { 446 447 // Check if the user appears to be a quick developer 448 if time.Since(editorLaunchTime) < 30*time.Second && e.mode != mode.Text && e.mode != mode.Blank { 449 // Disable the quick help at start 450 DisableQuickHelpScreen(nil) 451 } 452 453 // Start a spinner, in a short while 454 quitChan := Spinner(c, tty, fmt.Sprintf("Saving %s... ", e.filename), fmt.Sprintf("saving %s: stopped by user", e.filename), 200*time.Millisecond, e.ItalicsColor) 455 456 // Prepare gzipped data 457 if strings.HasSuffix(e.filename, ".gz") { 458 var err error 459 data, err = gZipData(data) 460 if err != nil { 461 quitChan <- true 462 return err 463 } 464 } 465 466 // Save the file and return any errors 467 if err := os.WriteFile(e.filename, data, fileMode); err != nil { 468 // Stop the spinner and return 469 quitChan <- true 470 return err 471 } 472 473 // This file should not be considered read-only, since saving went fine 474 e.readOnly = false 475 476 // TODO: Consider the previous fileMode of the file when doing chmod +x instead of just setting 0755 or 0644 477 478 // "chmod +x" or "chmod -x". This is needed after saving the file, in order to toggle the executable bit. 479 // rust source may start with something like "#![feature(core_intrinsics)]", so avoid that. 480 if !containsTheWordSource { 481 if shebang && e.mode != mode.Rust && e.mode != mode.Python && e.mode != mode.Mojo && !e.readOnly { 482 // Call Chmod, but ignore errors (since this is just a bonus and not critical) 483 os.Chmod(e.filename, fileMode) 484 e.syntaxHighlight = true 485 } else if e.mode == mode.ASCIIDoc || e.mode == mode.Just || e.mode == mode.Make || e.mode == mode.Markdown || e.mode == mode.ReStructured || e.mode == mode.SCDoc { 486 fileMode = 0o644 487 os.Chmod(e.filename, fileMode) 488 } else if baseFilename := filepath.Base(e.filename); baseFilename == "PKGBUILD" || baseFilename == "APKGBUILD" { 489 fileMode = 0o644 490 os.Chmod(e.filename, fileMode) 491 } 492 } 493 494 // Stop the spinner 495 quitChan <- true 496 497 } 498 499 e.redrawCursor = true 500 501 // Trailing spaces may be trimmed, so move to the end, if needed 502 if changed { 503 e.GoToPosition(c, nil, *bookmark) 504 if e.AfterEndOfLine() { 505 e.EndNoTrim(c) 506 } 507 // Do the redraw manually before showing the status message 508 respectOffset := true 509 redrawCanvas := false 510 e.DrawLines(c, respectOffset, redrawCanvas) 511 e.redraw = false 512 } 513 514 // All done 515 return nil 516 } 517 518 // TrimRight will remove whitespace from the end of the given line number 519 // Returns true if the line was trimmed 520 func (e *Editor) TrimRight(index LineIndex) bool { 521 n := int(index) 522 line, ok := e.lines[n] 523 if !ok { 524 return false 525 } 526 trimmedLine := []rune(trimRightSpace(string(line))) 527 if len(trimmedLine) != len(line) { 528 e.lines[n] = trimmedLine 529 return true 530 } 531 return false 532 } 533 534 // TrimLeft will remove whitespace from the start of the given line number 535 // Returns true if the line was trimmed 536 func (e *Editor) TrimLeft(index LineIndex) bool { 537 changed := false 538 n := int(index) 539 if line, ok := e.lines[n]; ok { 540 newRunes := []rune(strings.TrimLeftFunc(string(line), unicode.IsSpace)) 541 // TODO: Just compare lengths instead of contents? 542 if string(newRunes) != string(line) { 543 e.lines[n] = newRunes 544 changed = true 545 } 546 } 547 return changed 548 } 549 550 // StripSingleLineComment will strip away trailing single-line comments. 551 // TODO: Also strip trailing /* ... */ comments 552 func (e *Editor) StripSingleLineComment(line string) string { 553 commentMarker := e.SingleLineCommentMarker() 554 if strings.Count(line, commentMarker) == 1 { 555 p := strings.Index(line, commentMarker) 556 return strings.TrimSpace(line[:p]) 557 } 558 return line 559 } 560 561 // DeleteRestOfLine will delete the rest of the line, from the given position 562 func (e *Editor) DeleteRestOfLine() { 563 x, err := e.DataX() 564 if err != nil { 565 // position is after the data, do nothing 566 return 567 } 568 y := int(e.DataY()) 569 if e.lines == nil { 570 e.lines = make(map[int][]rune) 571 } 572 v, ok := e.lines[y] 573 if !ok { 574 return 575 } 576 if v == nil { 577 e.lines[y] = make([]rune, 0) 578 } 579 if x > len(e.lines[y]) { 580 return 581 } 582 e.lines[y] = e.lines[y][:x] 583 e.changed = true 584 585 // Make sure no lines are nil 586 e.MakeConsistent() 587 } 588 589 // DeleteLine will delete the given line index 590 func (e *Editor) DeleteLine(n LineIndex) { 591 if n < 0 { 592 // This should never happen 593 return 594 } 595 lastLineIndex := LineIndex(e.Len() - 1) 596 endOfDocument := n >= lastLineIndex 597 if endOfDocument { 598 // Just delete this line 599 delete(e.lines, int(n)) 600 return 601 } 602 // TODO: Rely on the length of the hash map for finding the index instead of 603 // searching through each line number key. 604 var maxIndex LineIndex 605 found := false 606 for k := range e.lines { 607 if LineIndex(k) > maxIndex { 608 maxIndex = LineIndex(k) 609 found = true 610 } 611 } 612 if !found { 613 // This should never happen 614 return 615 } 616 if _, ok := e.lines[int(maxIndex)]; !ok { 617 // The line numbers and the length of e.lines does not match 618 return 619 } 620 // Shift all lines after y: 621 // shift all lines after n one step closer to n, overwriting e.lines[n] 622 for index := n; index <= (maxIndex - 1); index++ { 623 i := int(index) 624 e.lines[i] = e.lines[i+1] 625 } 626 // Then delete the final item 627 delete(e.lines, int(maxIndex)) 628 629 // This changes the document 630 e.changed = true 631 632 // Make sure no lines are nil 633 e.MakeConsistent() 634 } 635 636 // DeleteLineMoveBookmark will delete the given line index and also move the bookmark if it's after n 637 func (e *Editor) DeleteLineMoveBookmark(n LineIndex, bookmark *Position) { 638 if bookmark != nil && bookmark.LineIndex() > n { 639 bookmark.DecY() 640 } 641 e.DeleteLine(n) 642 } 643 644 // DeleteCurrentLineMoveBookmark will delete the current line and also move the bookmark one up 645 // if it's after the current line. 646 func (e *Editor) DeleteCurrentLineMoveBookmark(bookmark *Position) { 647 e.DeleteLineMoveBookmark(e.DataY(), bookmark) 648 } 649 650 // Delete will delete a character at the given position 651 func (e *Editor) Delete() { 652 y := int(e.DataY()) 653 lineLen := len(e.lines[y]) 654 if _, ok := e.lines[y]; !ok || lineLen == 0 || (lineLen == 1 && unicode.IsSpace(e.lines[y][0])) { 655 // All keys in the map that are > y should be shifted -1. 656 // This also overwrites e.lines[y]. 657 e.DeleteLine(LineIndex(y)) 658 e.changed = true 659 return 660 } 661 x, err := e.DataX() 662 if err != nil || x > len(e.lines[y])-1 { 663 // on the last index, just use every element but x 664 e.lines[y] = e.lines[y][:x] 665 // check if the next line exists 666 if _, ok := e.lines[y+1]; ok { 667 // then add the contents of the next line, if available 668 nextLine, ok := e.lines[y+1] 669 if ok && len(nextLine) > 0 { 670 e.lines[y] = append(e.lines[y], nextLine...) 671 // then delete the next line 672 e.DeleteLine(LineIndex(y + 1)) 673 } 674 } 675 e.changed = true 676 return 677 } 678 // Delete just this character 679 e.lines[y] = append(e.lines[y][:x], e.lines[y][x+1:]...) 680 e.changed = true 681 682 // Make sure no lines are nil 683 e.MakeConsistent() 684 } 685 686 // Empty will check if the current editor contents are empty or not. 687 // If there's only one line left and it is only whitespace, that will be considered empty as well. 688 func (e *Editor) Empty() bool { 689 l := len(e.lines) 690 if l == 0 { 691 return true 692 } 693 if l == 1 { 694 // Regardless of line number key, check the contents of the one remaining trimmed line 695 for _, line := range e.lines { 696 return len(strings.TrimSpace(string(line))) == 0 697 } 698 } 699 // > 1 lines 700 return false 701 } 702 703 // MakeConsistent creates an empty slice of runes for any empty lines, 704 // to make sure that no line number below e.Len() points to a nil map. 705 func (e *Editor) MakeConsistent() { 706 // Check if the keys in the map are consistent 707 for i := 0; i < len(e.lines); i++ { 708 if _, found := e.lines[i]; !found { 709 e.lines[i] = make([]rune, 0) 710 e.changed = true 711 } 712 } 713 } 714 715 // WithinLimit will check if a line is within the word wrap limit, 716 // given a Y position. 717 func (e *Editor) WithinLimit(y LineIndex) bool { 718 return len(e.lines[int(y)]) < e.wrapWidth 719 } 720 721 // LastWord will return the last word of a line, 722 // given a Y position. Returns an empty string if there is no last word. 723 func (e *Editor) LastWord(y int) string { 724 // TODO: Use a faster method 725 words := strings.Fields(strings.TrimSpace(string(e.lines[y]))) 726 if len(words) > 0 { 727 return words[len(words)-1] 728 } 729 return "" 730 } 731 732 // SplitOvershoot will split the line into a first part that is within the 733 // word wrap length and a second part that is the overshooting part. 734 // y is the line index (y position, counting from 0). 735 // isSpace is true if a space has just been inserted on purpose at the current position. 736 // returns true if there was a space at the split point. 737 func (e *Editor) SplitOvershoot(index LineIndex, isSpace bool) ([]rune, []rune, bool) { 738 hasSpace := false 739 740 y := int(index) 741 742 // Maximum word length to not keep as one word 743 maxDistance := e.wrapWidth / 2 744 if e.WithinLimit(index) { 745 return e.lines[y], make([]rune, 0), false 746 } 747 splitPosition := e.wrapWidth 748 if isSpace { 749 splitPosition, _ = e.DataX() 750 } else { 751 // Starting at the split position, move left until a space is reached (or the start of the line). 752 // If a space is reached, check if it is too far away from n to be used as a split position, or not. 753 spacePosition := -1 754 for i := splitPosition; i >= 0; i-- { 755 if i < len(e.lines[y]) && unicode.IsSpace(e.lines[y][i]) { 756 // Found a space at position i 757 spacePosition = i 758 break 759 } 760 } 761 // Found a better position to split, at a nearby space? 762 if spacePosition != -1 { 763 hasSpace = true 764 if (splitPosition - spacePosition) <= maxDistance { 765 // Okay, we found a better split point. 766 splitPosition = spacePosition 767 } 768 } 769 } 770 771 // Split the line into two parts 772 773 n := splitPosition 774 // Make space for the two parts 775 first := make([]rune, len(e.lines[y][:n])) 776 second := make([]rune, len(e.lines[y][n:])) 777 // Copy the line into first and second 778 copy(first, e.lines[y][:n]) 779 copy(second, e.lines[y][n:]) 780 781 // If the second part starts with a space, remove it 782 if len(second) > 0 && unicode.IsSpace(second[0]) { 783 second = second[1:] 784 hasSpace = true 785 } 786 787 return first, second, hasSpace 788 } 789 790 // WrapAllLines will word wrap all lines that are longer than e.wrapWidth 791 func (e *Editor) WrapAllLines() bool { 792 wrapped := false 793 insertedLines := 0 794 795 y := e.DataY() 796 797 for i := 0; i < e.Len(); i++ { 798 if e.WithinLimit(LineIndex(i)) { 799 continue 800 } 801 wrapped = true 802 803 first, second, spaceBetween := e.SplitOvershoot(LineIndex(i), false) 804 805 if len(first) > 0 && len(second) > 0 { 806 807 e.lines[i] = first 808 if spaceBetween { 809 second = append(second, ' ') 810 } 811 e.lines[i+1] = append(second, e.lines[i+1]...) 812 e.InsertLineBelowAt(LineIndex(i + 1)) 813 814 // This isn't perfect, but it helps move the cursor somewhere in 815 // the vicinity of where the line was before word wrapping. 816 // TODO: Make the cursor placement exact. 817 if LineIndex(i) < y { 818 insertedLines++ 819 } 820 821 e.changed = true 822 } 823 } 824 825 // Move the cursor as well, after wrapping 826 if insertedLines > 0 { 827 e.pos.sy += insertedLines 828 if e.pos.sy < 0 { 829 e.pos.sy = 0 830 } else if e.pos.sy >= len(e.lines) { 831 e.pos.sy = len(e.lines) - 1 832 } 833 e.redraw = true 834 e.redrawCursor = true 835 } 836 837 // This appears to be needed as well 838 e.MakeConsistent() 839 840 return wrapped 841 } 842 843 // WrapNow is a helper function for changing the word wrap width, 844 // while also wrapping all lines 845 func (e *Editor) WrapNow(wrapWith int) { 846 e.wrapWidth = wrapWith 847 if e.WrapAllLines() { 848 e.redraw = true 849 e.redrawCursor = true 850 } 851 } 852 853 // InsertLineAbove will attempt to insert a new line above the current position 854 func (e *Editor) InsertLineAbove() { 855 lineIndex := e.DataY() 856 857 if e.sameFilePortal != nil { 858 e.sameFilePortal.NewLineInserted(lineIndex) 859 } 860 861 y := int(lineIndex) 862 863 // Create new set of lines 864 lines2 := make(map[int][]rune) 865 866 // If at the first line, just add a line at the top 867 if y == 0 { 868 869 // Insert a blank line 870 lines2[0] = make([]rune, 0) 871 // Then insert all the other lines, shifted by 1 872 for k, v := range e.lines { 873 lines2[k+1] = v 874 } 875 y++ 876 877 } else { 878 // For each line in the old map, if at (y-1), insert a blank line 879 // (insert a blank line above) 880 for k, v := range e.lines { 881 if k < (y - 1) { 882 lines2[k] = v 883 } else if k == (y - 1) { 884 lines2[k] = v 885 lines2[k+1] = make([]rune, 0) 886 } else if k > (y - 1) { 887 lines2[k+1] = v 888 } 889 } 890 } 891 892 // Use the new set of lines 893 e.lines = lines2 894 895 // Make sure no lines are nil 896 e.MakeConsistent() 897 898 // Skip trailing newlines after this line 899 for i := len(e.lines); i > y; i-- { 900 if len(e.lines[i]) == 0 { 901 delete(e.lines, i) 902 } else { 903 break 904 } 905 } 906 e.changed = true 907 } 908 909 // InsertLineBelow will attempt to insert a new line below the current position 910 func (e *Editor) InsertLineBelow() { 911 lineIndex := e.DataY() 912 if e.sameFilePortal != nil { 913 e.sameFilePortal.NewLineInserted(lineIndex) 914 } 915 e.InsertLineBelowAt(lineIndex) 916 } 917 918 // InsertLineBelowAt will attempt to insert a new line below the given y position 919 func (e *Editor) InsertLineBelowAt(index LineIndex) { 920 y := int(index) 921 922 // Make sure no lines are nil 923 e.MakeConsistent() 924 925 // If we are the the last line, add an empty line at the end and return 926 if y == (len(e.lines) - 1) { 927 e.lines[int(y)+1] = make([]rune, 0) 928 e.changed = true 929 return 930 } 931 932 // Create new set of lines, with room for one more 933 lines2 := make(map[int][]rune, len(e.lines)+1) 934 935 // For each line in the old map, if at y, insert a blank line 936 // (insert a blank line below) 937 for k, v := range e.lines { 938 if k < y { 939 lines2[k] = v 940 } else if k == y { 941 lines2[k] = v 942 lines2[k+1] = make([]rune, 0) 943 } else if k > y { 944 lines2[k+1] = v 945 } 946 } 947 // Use the new set of lines 948 e.lines = lines2 949 950 // Skip trailing newlines after this line 951 for i := len(e.lines); i > y; i-- { 952 if len(e.lines[i]) == 0 { 953 delete(e.lines, i) 954 } else { 955 break 956 } 957 } 958 959 e.changed = true 960 } 961 962 // Insert will insert a rune at the given position, with no word wrap, 963 // but MakeConsisten will be called. 964 func (e *Editor) Insert(r rune) { 965 // Ignore it if the current position is out of bounds 966 x, _ := e.DataX() 967 968 y := int(e.DataY()) 969 970 // If there are no lines, initialize and set the 0th rune to the given one 971 if e.lines == nil { 972 e.lines = make(map[int][]rune) 973 e.lines[0] = []rune{r} 974 return 975 } 976 977 // If the current line is empty, initialize it with a line that is just the given rune 978 _, ok := e.lines[y] 979 if !ok { 980 e.lines[y] = []rune{r} 981 return 982 } 983 if len(e.lines[y]) < x { 984 // Can only insert in the existing block of text 985 return 986 } 987 newlineLength := len(e.lines[y]) + 1 988 newline := make([]rune, newlineLength) 989 for i := 0; i < x; i++ { 990 newline[i] = e.lines[y][i] 991 } 992 newline[x] = r 993 for i := x + 1; i < newlineLength; i++ { 994 newline[i] = e.lines[y][i-1] 995 } 996 e.lines[y] = newline 997 998 e.changed = true 999 1000 // Make sure no lines are nil 1001 e.MakeConsistent() 1002 } 1003 1004 // CreateLineIfMissing will create a line at the given Y index, if it's missing 1005 func (e *Editor) CreateLineIfMissing(n LineIndex) { 1006 if e.lines == nil { 1007 e.lines = make(map[int][]rune) 1008 } 1009 _, ok := e.lines[int(n)] 1010 if !ok { 1011 e.lines[int(n)] = make([]rune, 0) 1012 e.changed = true 1013 } 1014 } 1015 1016 // WordCount returns the number of spaces in the text + 1 1017 func (e *Editor) WordCount() int { 1018 return len(strings.Fields(e.String())) 1019 } 1020 1021 // ToggleSyntaxHighlight toggles syntax highlighting 1022 func (e *Editor) ToggleSyntaxHighlight() { 1023 e.syntaxHighlight = !e.syntaxHighlight 1024 } 1025 1026 // ToggleRainbow toggles rainbow parenthesis 1027 func (e *Editor) ToggleRainbow() { 1028 e.rainbowParenthesis = !e.rainbowParenthesis 1029 } 1030 1031 // SetRainbow enables or disables rainbow parenthesis 1032 func (e *Editor) SetRainbow(rainbowParenthesis bool) { 1033 e.rainbowParenthesis = rainbowParenthesis 1034 } 1035 1036 // SetLine will fill the given line index with the given string. 1037 // Any previous contents of that line is removed. 1038 func (e *Editor) SetLine(n LineIndex, s string) { 1039 e.CreateLineIfMissing(n) 1040 e.lines[int(n)] = make([]rune, 0) 1041 counter := 0 1042 // It's important not to use the index value when looping over a string, 1043 // unless the byte index is what one's after, as opposed to the rune index. 1044 for _, letter := range s { 1045 e.Set(counter, n, letter) 1046 counter++ 1047 } 1048 } 1049 1050 // SetCurrentLine will replace the current line with the given string 1051 func (e *Editor) SetCurrentLine(s string) { 1052 e.SetLine(e.DataY(), s) 1053 } 1054 1055 // SplitLine will, at the given position, split the line in two. 1056 // The right side of the contents is moved to a new line below. 1057 func (e *Editor) SplitLine() bool { 1058 x, err := e.DataX() 1059 if err != nil { 1060 // After contents, this should not happen, do nothing 1061 return false 1062 } 1063 1064 y := e.DataY() 1065 1066 // Get the contents of this line 1067 runeLine := e.lines[int(y)] 1068 if len(runeLine) < 2 { 1069 // Did not split 1070 return false 1071 } 1072 leftContents := trimRightSpace(string(runeLine[:x])) 1073 rightContents := string(runeLine[x:]) 1074 // Insert a new line above this one 1075 e.InsertLineAbove() 1076 // Replace this line with the left contents 1077 e.SetLine(y, leftContents) 1078 e.SetLine(y+1, rightContents) 1079 // Splitted 1080 return true 1081 } 1082 1083 // DataX will return the X position in the data (as opposed to the X position in the viewport) 1084 func (e *Editor) DataX() (int, error) { 1085 // the y position in the data is the lines scrolled + current screen cursor Y position 1086 var dataY int 1087 e.pos.mut.RLock() 1088 dataY = e.pos.offsetY + e.pos.sy 1089 e.pos.mut.RUnlock() 1090 // get the current line of text 1091 screenCounter := 0 // counter for the characters on the screen 1092 // loop, while also keeping track of tab expansion 1093 // add a space to allow to jump to the position after the line and get a valid data position 1094 found := false 1095 dataX := 0 1096 runeCounter := 0 1097 for _, r := range e.lines[dataY] { 1098 e.pos.mut.RLock() 1099 // When we reached the correct screen position, use i as the data position 1100 if screenCounter == (e.pos.sx + e.pos.offsetX) { 1101 e.pos.mut.RUnlock() 1102 dataX = runeCounter 1103 found = true 1104 break 1105 } 1106 e.pos.mut.RUnlock() 1107 // Increase the counter, based on the current rune 1108 if r == '\t' { 1109 screenCounter += e.indentation.PerTab 1110 } else { 1111 screenCounter++ 1112 } 1113 runeCounter++ 1114 } 1115 if !found { 1116 return runeCounter, errors.New("position is after data") 1117 } 1118 // Return the data cursor 1119 return dataX, nil 1120 } 1121 1122 // DataY will return the Y position in the data (as opposed to the Y position in the viewport) 1123 func (e *Editor) DataY() LineIndex { 1124 e.pos.mut.RLock() 1125 defer e.pos.mut.RUnlock() 1126 return LineIndex(e.pos.offsetY + e.pos.sy) 1127 } 1128 1129 // SetRune will set a rune at the current data position 1130 func (e *Editor) SetRune(r rune) { 1131 // Only set a rune if x is within the current line contents 1132 if x, err := e.DataX(); err == nil { 1133 e.Set(x, e.DataY(), r) 1134 } 1135 } 1136 1137 // InsertBelow will insert the given rune at the start of the line below, 1138 // starting a new line if required. 1139 func (e *Editor) InsertBelow(y int, r rune) { 1140 if _, ok := e.lines[y+1]; !ok { 1141 // If the next line does not exist, create one containing just "r" 1142 e.lines[y+1] = []rune{r} 1143 } else if len(e.lines[y+1]) > 0 { 1144 // If the next line is non-empty, insert "r" at the start 1145 e.lines[y+1] = append([]rune{r}, e.lines[y+1][:]...) 1146 } else { 1147 // The next line exists, but is of length 0, should not happen, just replace it 1148 e.lines[y+1] = []rune{r} 1149 } 1150 } 1151 1152 // InsertStringBelow will insert the given string at the start of the line below, 1153 // starting a new line if required. 1154 func (e *Editor) InsertStringBelow(y int, s string) { 1155 if _, ok := e.lines[y+1]; !ok { 1156 // If the next line does not exist, create one containing the string 1157 e.lines[y+1] = []rune(s) 1158 } else if len(e.lines[y+1]) > 0 { 1159 // If the next line is non-empty, insert the string at the start 1160 e.lines[y+1] = append([]rune(s), e.lines[y+1][:]...) 1161 } else { 1162 // The next line exists, but is of length 0, should not happen, just replace it 1163 e.lines[y+1] = []rune(s) 1164 } 1165 } 1166 1167 // InsertStringAndMove will insert a string at the current data position 1168 // and possibly move down. This will also call e.WriteRune, e.Down and e.Next, as needed. 1169 func (e *Editor) InsertStringAndMove(c *vt100.Canvas, s string) { 1170 for _, r := range s { 1171 if r == '\n' { 1172 e.InsertLineBelow() 1173 e.Down(c, nil) 1174 continue 1175 } 1176 e.InsertRune(c, r) 1177 e.WriteRune(c) 1178 e.Next(c) 1179 } 1180 } 1181 1182 // InsertString will insert a string without newlines at the current data position. 1183 // his will also call e.WriteRune and e.Next, as needed. 1184 func (e *Editor) InsertString(c *vt100.Canvas, s string) { 1185 for _, r := range s { 1186 e.InsertRune(c, r) 1187 e.WriteRune(c) 1188 e.Next(c) 1189 } 1190 } 1191 1192 // Rune will get the rune at the current data position 1193 func (e *Editor) Rune() rune { 1194 x, err := e.DataX() 1195 if err != nil { 1196 // after line contents, return a zero rune 1197 return rune(0) 1198 } 1199 return e.Get(x, e.DataY()) 1200 } 1201 1202 // LeftRune will get the rune to the left of the current data position 1203 func (e *Editor) LeftRune() rune { 1204 y := e.DataY() 1205 x, err := e.DataX() 1206 if err != nil { 1207 // This is after the line contents, return the last rune 1208 runes, ok := e.lines[int(y)] 1209 if !ok || len(runes) == 0 { 1210 return rune(0) 1211 } 1212 // Return the last rune 1213 return runes[len(runes)-1] 1214 } 1215 if x <= 0 { 1216 // Nothing to the left of this 1217 return rune(0) 1218 } 1219 // Return the rune to the left 1220 return e.Get(x-1, e.DataY()) 1221 } 1222 1223 // CurrentLine will get the current data line, as a string 1224 func (e *Editor) CurrentLine() string { 1225 return e.Line(e.DataY()) 1226 } 1227 1228 // PreviousLine will get the previous data line, as a string 1229 func (e *Editor) PreviousLine() string { 1230 y := e.DataY() - 1 1231 if y < 0 { 1232 return "" 1233 } 1234 return e.Line(y) 1235 } 1236 1237 // NextLine will get the previous data line, as a string 1238 func (e *Editor) NextLine() string { 1239 y := e.DataY() + 1 1240 if y < 0 || int(y) >= e.Len() { 1241 return "" 1242 } 1243 return e.Line(y) 1244 } 1245 1246 // Home will move the cursor the the start of the line (x = 0) 1247 // And also scroll all the way to the left. 1248 func (e *Editor) Home() { 1249 e.pos.sx = 0 1250 e.pos.offsetX = 0 1251 e.redraw = true 1252 } 1253 1254 // End will move the cursor to the position right after the end of the current line contents, 1255 // and also trim away whitespace from the right side. 1256 func (e *Editor) End(c *vt100.Canvas) { 1257 y := e.DataY() 1258 e.TrimRight(y) 1259 x := e.LastTextPosition(y) + 1 1260 e.pos.SetX(c, x) 1261 e.redraw = true 1262 } 1263 1264 // EndNoTrim will move the cursor to the position right after the end of the current line contents 1265 func (e *Editor) EndNoTrim(c *vt100.Canvas) { 1266 x := e.LastTextPosition(e.DataY()) + 1 1267 e.pos.SetX(c, x) 1268 e.redraw = true 1269 } 1270 1271 // AtEndOfLine returns true if the cursor is at exactly the last character of the line, not the one after 1272 func (e *Editor) AtEndOfLine() bool { 1273 return e.pos.sx+e.pos.offsetX == e.LastTextPosition(e.DataY()) 1274 } 1275 1276 // DownEnd will move down and then choose a "smart" X position 1277 func (e *Editor) DownEnd(c *vt100.Canvas) error { 1278 tmpx := e.pos.sx 1279 err := e.pos.Down(c) 1280 if err != nil { 1281 return err 1282 } 1283 line := e.CurrentLine() 1284 if len(strings.TrimSpace(line)) == 1 { 1285 e.TrimRight(e.DataY()) 1286 e.End(c) 1287 } else if e.AfterLineScreenContentsPlusOne() && tmpx > 1 { 1288 e.End(c) 1289 if e.pos.sx != tmpx && e.pos.sx > e.pos.savedX { 1290 e.pos.savedX = tmpx 1291 } 1292 } else { 1293 e.pos.sx = e.pos.savedX 1294 1295 if e.pos.sx < 0 { 1296 e.pos.sx = 0 1297 } 1298 if e.AfterLineScreenContentsPlusOne() { 1299 e.End(c) 1300 } 1301 1302 // Also checking if e.Rune() is ' ' is nice for code, but horrible for regular text files 1303 if e.Rune() == '\t' { 1304 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1305 } 1306 1307 // Expand the line, then check if e.pos.sx falls on a tab character ("\t" is expanded to several tabs ie. "\t\t\t\t") 1308 expandedRunes := []rune(strings.ReplaceAll(line, "\t", strings.Repeat("\t", e.indentation.PerTab))) 1309 if e.pos.sx < len(expandedRunes) && expandedRunes[e.pos.sx] == '\t' { 1310 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1311 } 1312 } 1313 return nil 1314 } 1315 1316 // UpEnd will move up and then choose a "smart" X position 1317 func (e *Editor) UpEnd(c *vt100.Canvas) error { 1318 tmpx := e.pos.sx 1319 err := e.pos.Up() 1320 if err != nil { 1321 return err 1322 } 1323 if e.AfterLineScreenContentsPlusOne() && tmpx > 1 { 1324 e.End(c) 1325 if e.pos.sx != tmpx && e.pos.sx > e.pos.savedX { 1326 e.pos.savedX = tmpx 1327 } 1328 } else { 1329 e.pos.sx = e.pos.savedX 1330 1331 if e.pos.sx < 0 { 1332 e.pos.sx = 0 1333 } 1334 if e.AfterLineScreenContentsPlusOne() { 1335 e.End(c) 1336 } 1337 1338 // Also checking if e.Rune() is ' ' is nice for code, but horrible for regular text files 1339 if e.Rune() == '\t' { 1340 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1341 } 1342 1343 // Expand the line, then check if e.pos.sx falls on a tab character ("\t" is expanded to several tabs ie. "\t\t\t\t") 1344 expandedRunes := []rune(strings.ReplaceAll(e.CurrentLine(), "\t", strings.Repeat("\t", e.indentation.PerTab))) 1345 if e.pos.sx < len(expandedRunes) && expandedRunes[e.pos.sx] == '\t' { 1346 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1347 } 1348 } 1349 return nil 1350 } 1351 1352 // Next will move the cursor to the next position in the contents 1353 func (e *Editor) Next(c *vt100.Canvas) error { 1354 // Ignore it if the position is out of bounds 1355 atTab := e.Rune() == '\t' 1356 if atTab { 1357 e.pos.sx += e.indentation.PerTab 1358 } else { 1359 e.pos.sx++ 1360 } 1361 // Did we move too far on this line? 1362 if e.AfterLineScreenContentsPlusOne() { 1363 // Undo the move 1364 if atTab { 1365 e.pos.sx -= e.indentation.PerTab 1366 } else { 1367 e.pos.sx-- 1368 } 1369 // Move down 1370 err := e.pos.Down(c) 1371 if err != nil { 1372 return err 1373 } 1374 // Move to the start of the line 1375 e.pos.sx = 0 1376 } 1377 return nil 1378 } 1379 1380 // LeftRune2 returns the rune to the left of the current position, or an error 1381 func (e *Editor) LeftRune2() (rune, error) { 1382 x, err := e.DataX() 1383 if err != nil { 1384 return rune(0), err 1385 } 1386 x-- 1387 if x <= 0 { 1388 return rune(0), errors.New("no runes to the left") 1389 } 1390 return e.Get(x, e.DataY()), nil 1391 } 1392 1393 // TabToTheLeft returns true if there is a '\t' to the left of the current position 1394 func (e *Editor) TabToTheLeft() bool { 1395 r, err := e.LeftRune2() 1396 if err != nil { 1397 return false 1398 } 1399 return r == '\t' 1400 } 1401 1402 // Prev will move the cursor to the previous position in the contents 1403 func (e *Editor) Prev(c *vt100.Canvas) error { 1404 atTab := e.TabToTheLeft() || (e.pos.sx <= e.indentation.PerTab && e.Get(0, e.DataY()) == '\t') 1405 if e.pos.sx == 0 && e.pos.offsetX > 0 { 1406 // at left edge, but can scroll to the left 1407 e.pos.offsetX-- 1408 e.redraw = true 1409 } else { 1410 // If at a tab character, move a few more positions 1411 if atTab { 1412 e.pos.sx -= e.indentation.PerTab 1413 } else { 1414 e.pos.sx-- 1415 } 1416 } 1417 if e.pos.sx < 0 { // Did we move too far and there is no X offset? 1418 // Undo the move 1419 if atTab { 1420 e.pos.sx += e.indentation.PerTab 1421 } else { 1422 e.pos.sx++ 1423 } 1424 // Move up, and to the end of the line above, if in EOL mode 1425 err := e.pos.Up() 1426 if err != nil { 1427 return err 1428 } 1429 e.End(c) 1430 } 1431 return nil 1432 } 1433 1434 // SaveX will save the current X position, if it's within reason 1435 func (e *Editor) SaveX(regardless bool) { 1436 if regardless || (!e.AfterLineScreenContentsPlusOne() && e.pos.sx > 1) { 1437 e.pos.savedX = e.pos.sx 1438 } 1439 } 1440 1441 // ScrollDown will scroll down the given amount of lines given in scrollSpeed 1442 func (e *Editor) ScrollDown(c *vt100.Canvas, status *StatusBar, scrollSpeed int) bool { 1443 // Find out if we can scroll scrollSpeed, or less 1444 canScroll := scrollSpeed 1445 1446 // Last y position in the canvas 1447 canvasLastY := int(c.H() - 1) 1448 1449 // Retrieve the current editor scroll offset offset 1450 mut.RLock() 1451 offset := e.pos.offsetY 1452 mut.RUnlock() 1453 1454 // Number of lines in the document 1455 l := e.Len() 1456 1457 if offset >= l-canvasLastY { 1458 c.Draw() 1459 // Don't redraw 1460 return false 1461 } 1462 if status != nil { 1463 status.Clear(c) 1464 } 1465 if (offset + canScroll) >= (l - canvasLastY) { 1466 // Almost at the bottom, we can scroll the remaining lines 1467 canScroll = (l - canvasLastY) - offset 1468 } 1469 1470 // Move the scroll offset 1471 mut.Lock() 1472 e.pos.offsetX = 0 1473 e.pos.offsetY += canScroll 1474 mut.Unlock() 1475 1476 // Prepare to redraw 1477 return true 1478 } 1479 1480 // ScrollUp will scroll down the given amount of lines given in scrollSpeed 1481 func (e *Editor) ScrollUp(c *vt100.Canvas, status *StatusBar, scrollSpeed int) bool { 1482 // Find out if we can scroll scrollSpeed, or less 1483 canScroll := scrollSpeed 1484 1485 // Retrieve the current editor scroll offset offset 1486 mut.RLock() 1487 offset := e.pos.offsetY 1488 mut.RUnlock() 1489 1490 if offset == 0 { 1491 // Can't scroll further up 1492 // Status message 1493 // status.SetMessage("Start of text") 1494 // status.Show(c, p) 1495 // c.Draw() 1496 // Redraw 1497 return true 1498 } 1499 if status != nil { 1500 status.Clear(c) 1501 } 1502 if offset-canScroll < 0 { 1503 // Almost at the top, we can scroll the remaining lines 1504 canScroll = offset 1505 } 1506 // Move the scroll offset 1507 mut.Lock() 1508 e.pos.offsetX = 0 1509 e.pos.offsetY -= canScroll 1510 mut.Unlock() 1511 // Prepare to redraw 1512 return true 1513 } 1514 1515 // AtFirstLineOfDocument is true if we're at the first line of the document 1516 func (e *Editor) AtFirstLineOfDocument() bool { 1517 return e.DataY() == LineIndex(0) 1518 } 1519 1520 // AtLastLineOfDocument is true if we're at the last line of the document 1521 func (e *Editor) AtLastLineOfDocument() bool { 1522 return e.DataY() == LineIndex(e.Len()-1) 1523 } 1524 1525 // AfterLastLineOfDocument is true if we're after the last line of the document 1526 func (e *Editor) AfterLastLineOfDocument() bool { 1527 return e.DataY() > LineIndex(e.Len()-1) 1528 } 1529 1530 // AtOrAfterLastLineOfDocument is true if we're at or after the last line of the document 1531 func (e *Editor) AtOrAfterLastLineOfDocument() bool { 1532 return e.DataY() >= LineIndex(e.Len()-1) 1533 } 1534 1535 // AtOrAfterEndOfDocument is true if the cursor is at or after the end of the last line of the document 1536 func (e *Editor) AtOrAfterEndOfDocument() bool { 1537 return (e.AtLastLineOfDocument() && e.AtOrAfterEndOfLine()) || e.AfterLastLineOfDocument() 1538 } 1539 1540 // AfterEndOfDocument is true if the cursor is after the end of the last line of the document 1541 func (e *Editor) AfterEndOfDocument() bool { 1542 return e.AfterLastLineOfDocument() // && e.AtOrAfterEndOfLine() 1543 } 1544 1545 // AtEndOfDocument is true if the cursor is at the end of the last line of the document 1546 func (e *Editor) AtEndOfDocument() bool { 1547 return e.AtLastLineOfDocument() && e.AtEndOfLine() 1548 } 1549 1550 // AtStartOfDocument is true if we're at the first line of the document 1551 func (e *Editor) AtStartOfDocument() bool { 1552 return e.pos.sy == 0 //&& e.pos.offsetY == 0 1553 } 1554 1555 // AtStartOfScreenLine is true if the cursor is a the start of the screen line. 1556 // The line may be scrolled all the way to the end, and the cursor moved to the left of the screen, for instance. 1557 func (e *Editor) AtStartOfScreenLine() bool { 1558 return e.pos.AtStartOfScreenLine() 1559 } 1560 1561 // AtStartOfTheLine is true if the cursor is a the start of the screen line, and the line is not scrolled. 1562 func (e *Editor) AtStartOfTheLine() bool { 1563 return e.pos.AtStartOfTheLine() 1564 } 1565 1566 // AtLeftEdgeOfDocument is true if we're at the first column at the document. Same as AtStarOfTheLine. 1567 func (e *Editor) AtLeftEdgeOfDocument() bool { 1568 return e.pos.sx == 0 && e.pos.offsetX == 0 1569 } 1570 1571 // AtOrAfterEndOfLine returns true if the cursor is at or after the contents of this line 1572 func (e *Editor) AtOrAfterEndOfLine() bool { 1573 if e.EmptyLine() { 1574 return true 1575 } 1576 x, err := e.DataX() 1577 if err != nil { 1578 // After end of data 1579 return true 1580 } 1581 return x >= e.LastDataPosition(e.DataY()) 1582 } 1583 1584 // AfterEndOfLine returns true if the cursor is after the contents of this line 1585 func (e *Editor) AfterEndOfLine() bool { 1586 if e.EmptyLine() { 1587 return true 1588 } 1589 x, err := e.DataX() 1590 if err != nil { 1591 // After end of data 1592 return true 1593 } 1594 return x > e.LastDataPosition(e.DataY()) 1595 } 1596 1597 // AfterLineScreenContents will check if the cursor is after the current line contents 1598 func (e *Editor) AfterLineScreenContents() bool { 1599 return e.pos.sx > e.LastScreenPosition(e.DataY()) 1600 } 1601 1602 // AfterScreenWidth checks if the current cursor position has moved after the terminal/canvas width 1603 func (e *Editor) AfterScreenWidth(c *vt100.Canvas) bool { 1604 w := 80 // default width 1605 if c != nil { 1606 w = int(c.W()) 1607 } 1608 return e.pos.sx >= w 1609 } 1610 1611 // AfterLineScreenContentsPlusOne will check if the cursor is after the current line contents, with a margin of 1 1612 func (e *Editor) AfterLineScreenContentsPlusOne() bool { 1613 return e.pos.sx > (e.LastScreenPosition(e.DataY()) + 1) 1614 } 1615 1616 // WriteRune writes the current rune to the given canvas 1617 func (e *Editor) WriteRune(c *vt100.Canvas) { 1618 if c != nil { 1619 c.WriteRune(uint(e.pos.sx+e.pos.offsetX), uint(e.pos.sy), e.Foreground, e.Background, e.Rune()) 1620 } 1621 } 1622 1623 // WriteTab writes spaces when there is a tab character, to the canvas 1624 func (e *Editor) WriteTab(c *vt100.Canvas) { 1625 spacesPerTab := e.indentation.PerTab 1626 for x := e.pos.sx; x < e.pos.sx+spacesPerTab; x++ { 1627 c.WriteRune(uint(x+e.pos.offsetX), uint(e.pos.sy), e.Foreground, e.Background, ' ') 1628 } 1629 } 1630 1631 // EmptyRightTrimmedLine checks if the current line is empty (and whitespace doesn't count) 1632 func (e *Editor) EmptyRightTrimmedLine() bool { 1633 return len(trimRightSpace(e.CurrentLine())) == 0 1634 } 1635 1636 // LineAbove returns the line above, if possible 1637 func (e *Editor) LineAbove() string { 1638 y := e.DataY() - 1 1639 if y >= 0 { 1640 return e.Line(y) 1641 } 1642 return "" 1643 1644 } 1645 1646 // LineBelow returns the line below, if possible 1647 func (e *Editor) LineBelow() string { 1648 y := e.DataY() + 1 1649 if int(y) < e.Len() { 1650 return e.Line(y) 1651 } 1652 return "" 1653 } 1654 1655 // EmptyRightTrimmedLineBelow checks if the next line is empty (and whitespace doesn't count) 1656 func (e *Editor) EmptyRightTrimmedLineBelow() bool { 1657 return len(trimRightSpace(e.Line(e.DataY()+1))) == 0 1658 } 1659 1660 // EmptyLine returns true if the current line is completely empty, no whitespace or anything 1661 func (e *Editor) EmptyLine() bool { 1662 return len(e.CurrentLine()) == 0 1663 } 1664 1665 // EmptyTrimmedLine returns true if the current line (trimmed) is completely empty 1666 func (e *Editor) EmptyTrimmedLine() bool { 1667 return len(e.TrimmedLine()) == 0 1668 } 1669 1670 // AtStartOfTextScreenLine returns true if the position is at the start of the text for this screen line 1671 func (e *Editor) AtStartOfTextScreenLine() bool { 1672 return uint(e.pos.sx) == e.FirstScreenPosition(e.DataY()) 1673 } 1674 1675 // BeforeStartOfTextScreenLine returns true if the position is before the start of the text for this screen line 1676 func (e *Editor) BeforeStartOfTextScreenLine() bool { 1677 return uint(e.pos.sx) < e.FirstScreenPosition(e.DataY()) 1678 } 1679 1680 // AtOrBeforeStartOfTextScreenLine returns true if the position is before or at the start of the text for this screen line 1681 func (e *Editor) AtOrBeforeStartOfTextScreenLine() bool { 1682 return uint(e.pos.sx) <= e.FirstScreenPosition(e.DataY()) 1683 } 1684 1685 // Up tried to move the cursor up, and also scroll 1686 func (e *Editor) Up(c *vt100.Canvas, status *StatusBar) { 1687 e.GoTo(e.DataY()-1, c, status) 1688 } 1689 1690 // Down tries to move the cursor down, and also scroll 1691 // status is used for clearing status bar messages and can be nil 1692 // returns true if the end is reached 1693 func (e *Editor) Down(c *vt100.Canvas, status *StatusBar) bool { 1694 _, reachedTheEnd := e.GoTo(e.DataY()+1, c, status) 1695 return reachedTheEnd 1696 } 1697 1698 // LeadingWhitespace returns the leading whitespace for this line 1699 func (e *Editor) LeadingWhitespace() string { 1700 return e.CurrentLine()[:e.FirstDataPosition(e.DataY())] 1701 } 1702 1703 // WhitespaceAboveAndBelow returns the longest indentation whitespace for the line above or below 1704 func (e *Editor) WhitespaceAboveAndBelow() string { 1705 prev := getLeadingWhitespace(e.LineAbove()) 1706 next := getLeadingWhitespace(e.LineBelow()) 1707 if len(next) > len(prev) { 1708 return next 1709 } 1710 return prev 1711 } 1712 1713 // WhitespaceOnAboveAndBelow returns the longest indentation whitespace for the current line, the line above or the line below 1714 func (e *Editor) WhitespaceOnAboveAndBelow() string { 1715 prv := getLeadingWhitespace(e.LineAbove()) 1716 cur := getLeadingWhitespace(e.CurrentLine()) 1717 nxt := getLeadingWhitespace(e.LineBelow()) 1718 if len(nxt) > len(prv) && len(nxt) > len(cur) { 1719 return nxt 1720 } 1721 if len(prv) > len(nxt) && len(prv) > len(cur) { 1722 return prv 1723 } 1724 return cur 1725 } 1726 1727 // LeadingWhitespaceAt returns the leading whitespace for a given line index 1728 func (e *Editor) LeadingWhitespaceAt(y LineIndex) string { 1729 return e.Line(y)[:e.FirstDataPosition(y)] 1730 } 1731 1732 // LineNumber will return the current line number (data y index + 1) 1733 func (e *Editor) LineNumber() LineNumber { 1734 return LineNumber(e.DataY() + 1) 1735 } 1736 1737 // LineIndex will return the current line index (data y index) 1738 func (e *Editor) LineIndex() LineIndex { 1739 return e.DataY() 1740 } 1741 1742 // ColNumber will return the current column number (data x index + 1) 1743 func (e *Editor) ColNumber() ColNumber { 1744 x, _ := e.DataX() 1745 return ColNumber(x + 1) 1746 } 1747 1748 // ColIndex will return the current column index (data x index) 1749 func (e *Editor) ColIndex() ColIndex { 1750 x, _ := e.DataX() 1751 return ColIndex(x) 1752 } 1753 1754 // PositionAndModeInfo returns a status message, intended for being displayed at the bottom, containing: 1755 // * the current line number (counting from 1) 1756 // * the current column number (counting from 1) 1757 // * the current rune unicode value 1758 // * the current word count 1759 // * the currently detected file mode 1760 // * the current indentation mode (tabs or spaces) 1761 func (e *Editor) PositionAndModeInfo() string { 1762 indentation := "spaces" 1763 if !e.indentation.Spaces { 1764 indentation = "tabs" 1765 } 1766 return fmt.Sprintf("line %d col %d rune %U words %d, [%s] %s", e.LineNumber(), e.ColNumber(), e.Rune(), e.WordCount(), e.mode, indentation) 1767 } 1768 1769 // PositionPercentageAndModeInfo returns a status message, intended for being displayed at the bottom, containing: 1770 // * the current line number (counting from 1) 1771 // * the current number of lines 1772 // * the current line percentage 1773 // * the current column number (counting from 1) 1774 // * the current rune unicode value 1775 // * the current word count 1776 // * the currently detected file mode 1777 // * the current indentation mode (tabs or spaces) 1778 func (e *Editor) PositionPercentageAndModeInfo() string { 1779 indentation := "spaces" 1780 if !e.indentation.Spaces { 1781 indentation = "tabs" 1782 } 1783 lineNumber := e.LineNumber() 1784 allLines := e.Len() 1785 percentage := 0 1786 if allLines > 0 { 1787 percentage = int(100.0 * (float64(lineNumber) / float64(allLines))) 1788 } 1789 1790 return fmt.Sprintf("line %d/%d (%d%%) col %d rune %U words %d, [%s] %s", lineNumber, allLines, percentage, e.ColNumber(), e.Rune(), e.WordCount(), e.mode, indentation) 1791 } 1792 1793 // GoToPosition can go to the given position struct and use it as the new position 1794 func (e *Editor) GoToPosition(c *vt100.Canvas, status *StatusBar, pos Position) { 1795 e.pos = pos 1796 e.redraw, _ = e.GoTo(e.DataY(), c, status) 1797 e.redrawCursor = true 1798 } 1799 1800 // GoToStartOfTextLine will go to the start of the non-whitespace text, for this line 1801 func (e *Editor) GoToStartOfTextLine(c *vt100.Canvas) { 1802 e.pos.SetX(c, int(e.FirstScreenPosition(e.DataY()))) 1803 e.redraw = true 1804 } 1805 1806 // GoToNextParagraph will jump to the next line that has a blank line above it, if possible 1807 // Returns true if the editor should be redrawn, and true if the end has been reached 1808 func (e *Editor) GoToNextParagraph(c *vt100.Canvas, status *StatusBar) (bool, bool) { 1809 var lastFoundBlankLine LineIndex = -1 1810 l := e.Len() 1811 for i := e.DataY() + 1; i < LineIndex(l); i++ { 1812 // Check if this is a blank line 1813 if len(strings.TrimSpace(e.Line(i))) == 0 { 1814 lastFoundBlankLine = i 1815 } else { 1816 // This is a non-blank line, check if the line above is blank (or before the first line) 1817 if lastFoundBlankLine == (i - 1) { 1818 // Yes, this is the line we wish to jump to 1819 return e.GoTo(i, c, status) 1820 } 1821 } 1822 } 1823 return false, false 1824 } 1825 1826 // GoToPrevParagraph will jump to the previous line that has a blank line below it, if possible 1827 // Returns true if the editor should be redrawn, and true if the end has been reached 1828 func (e *Editor) GoToPrevParagraph(c *vt100.Canvas, status *StatusBar) (bool, bool) { 1829 lastFoundBlankLine := LineIndex(e.Len()) 1830 for i := e.DataY() - 1; i >= 0; i-- { 1831 // Check if this is a blank line 1832 if len(strings.TrimSpace(e.Line(i))) == 0 { 1833 lastFoundBlankLine = i 1834 } else { 1835 // This is a non-blank line, check if the line below is blank (or after the last line) 1836 if lastFoundBlankLine == (i + 1) { 1837 // Yes, this is the line we wish to jump to 1838 return e.GoTo(i, c, status) 1839 } 1840 } 1841 } 1842 return false, false 1843 } 1844 1845 // Center will scroll the contents so that the line with the cursor ends up in the center of the screen 1846 func (e *Editor) Center(c *vt100.Canvas) { 1847 // Find the terminal height 1848 h := 25 1849 if c != nil { 1850 h = int(c.Height()) 1851 } 1852 1853 // General information about how the positions and offsets relate: 1854 // 1855 // offset + screen y = data y 1856 // 1857 // offset = e.pos.offset 1858 // screen y = e.pos.sy 1859 // data y = e.DataY() 1860 // 1861 // offset = data y - screen y 1862 1863 // Plan: 1864 // 1. offset = data y - (h / 2) 1865 // 2. screen y = data y - offset 1866 1867 // Find the center line 1868 centerY := h / 2 1869 y := int(e.DataY()) 1870 if y < centerY { 1871 // Not enough room to adjust 1872 return 1873 } 1874 1875 // Find the new offset and y position 1876 newOffset := y - centerY 1877 newScreenY := y - newOffset 1878 1879 // Assign the new values to the editor 1880 e.pos.offsetY = newOffset 1881 e.pos.sy = newScreenY 1882 } 1883 1884 // CommentOn will insert a comment marker (like # or //) in front of a line 1885 func (e *Editor) CommentOn(commentMarker string) { 1886 space := " " 1887 if e.mode == mode.Config { // For config files, assume things will be toggled in and out, without a space 1888 space = "" 1889 } 1890 e.SetCurrentLine(commentMarker + space + e.CurrentLine()) 1891 } 1892 1893 // CommentOff will remove "//" or "// " from the front of the line if "//" is given 1894 func (e *Editor) CommentOff(commentMarker string) { 1895 var ( 1896 changed bool 1897 newContents string 1898 contents = e.CurrentLine() 1899 trimContents = strings.TrimSpace(contents) 1900 ) 1901 commentMarkerPlusSpace := commentMarker + " " 1902 if strings.HasPrefix(trimContents, commentMarkerPlusSpace) { 1903 // toggle off comment 1904 newContents = strings.Replace(contents, commentMarkerPlusSpace, "", 1) 1905 changed = true 1906 } else if strings.HasPrefix(trimContents, commentMarker) { 1907 // toggle off comment 1908 newContents = strings.Replace(contents, commentMarker, "", 1) 1909 changed = true 1910 } 1911 if changed { 1912 e.SetCurrentLine(newContents) 1913 // If the line was shortened and the cursor ended up after the line, move it 1914 if e.AfterEndOfLine() { 1915 e.End(nil) 1916 } 1917 } 1918 } 1919 1920 // CurrentLineCommented checks if the current trimmed line starts with "//", if "//" is given 1921 func (e *Editor) CurrentLineCommented(commentMarker string) bool { 1922 return strings.HasPrefix(e.TrimmedLine(), commentMarker) 1923 } 1924 1925 // ForEachLineInBlock will move the cursor and run the given function for 1926 // each line in the current block of text (until newline or end of document) 1927 // Also takes a string that will be passed on to the function. 1928 func (e *Editor) ForEachLineInBlock(c *vt100.Canvas, f func(string), commentMarker string) { 1929 downCounter := 0 1930 for !e.EmptyRightTrimmedLine() { 1931 f(commentMarker) 1932 if e.AtOrAfterEndOfDocument() { 1933 break 1934 } 1935 if e.Down(c, nil) { // reached the end 1936 break 1937 } 1938 downCounter++ 1939 if downCounter > 10 { // safeguard 1940 break 1941 } 1942 } 1943 // Go up again 1944 for i := downCounter; i > 0; i-- { 1945 e.Up(c, nil) 1946 } 1947 } 1948 1949 // Block will return the text from the given line until 1950 // either a newline or the end of the document. 1951 func (e *Editor) Block(n LineIndex) string { 1952 var ( 1953 bb, lb strings.Builder // block string builder and line string builder 1954 line []rune 1955 ok bool 1956 s string 1957 ) 1958 for { 1959 line, ok = e.lines[int(n)] 1960 n++ 1961 if !ok || len(line) == 0 { 1962 // End of document, empty line or invalid line: end of block 1963 return bb.String() 1964 } 1965 lb.Reset() 1966 for _, r := range line { 1967 lb.WriteRune(r) 1968 } 1969 s = lb.String() 1970 if len(strings.TrimSpace(s)) == 0 { 1971 // Empty trimmed line, end of block 1972 return bb.String() 1973 } 1974 // Save this line to bb 1975 bb.WriteString(s) 1976 // And add a newline 1977 bb.Write([]byte{'\n'}) 1978 } 1979 } 1980 1981 // ToggleCommentBlock will toggle comments until a blank line or the end of the document is reached 1982 // The amount of existing commented lines is considered before deciding to comment the block in or out 1983 func (e *Editor) ToggleCommentBlock(c *vt100.Canvas) { 1984 // If most of the lines in the block are comments, comment it out 1985 // If most of the lines in the block are not comments, comment it in 1986 1987 var ( 1988 downCounter = 0 1989 commentCounter = 0 1990 commentMarker = e.SingleLineCommentMarker() 1991 ) 1992 1993 // Count the commented lines in this block while going down 1994 for !e.EmptyRightTrimmedLine() { 1995 if e.CurrentLineCommented(commentMarker) { 1996 commentCounter++ 1997 } 1998 if e.AtOrAfterEndOfDocument() { 1999 break 2000 } 2001 if e.Down(c, nil) { // reached the end 2002 break 2003 } 2004 // TODO: Remove the safeguard 2005 downCounter++ 2006 if downCounter > 10 { // safeguard at the end of the document 2007 break 2008 } 2009 } 2010 // Go up again 2011 for i := downCounter; i > 0; i-- { 2012 e.Up(c, nil) 2013 } 2014 2015 // Check if most lines are commented out 2016 mostLinesAreComments := commentCounter >= (downCounter / 2) 2017 2018 // Handle the single-line case differently 2019 if downCounter == 1 && commentCounter == 0 { 2020 e.CommentOn(commentMarker) 2021 } else if downCounter == 1 && commentCounter == 1 { 2022 e.CommentOff(commentMarker) 2023 } else if mostLinesAreComments { 2024 e.ForEachLineInBlock(c, e.CommentOff, commentMarker) 2025 } else { 2026 e.ForEachLineInBlock(c, e.CommentOn, commentMarker) 2027 } 2028 } 2029 2030 // NewLine inserts a new line below and moves down one step 2031 func (e *Editor) NewLine(c *vt100.Canvas, status *StatusBar) { 2032 e.InsertLineBelow() 2033 e.Down(c, status) 2034 } 2035 2036 // ChopLine takes a string where the tabs have been expanded 2037 // and scrolls it + chops it up for display in the current viewport. 2038 // e.pos.offsetX and the given viewportWidth are respected. 2039 func (e *Editor) ChopLine(line string, viewportWidth int) string { 2040 var screenLine string 2041 // Shorten the screen line to account for the X offset 2042 if utf8.RuneCountInString(line) > e.pos.offsetX { 2043 screenLine = line[e.pos.offsetX:] 2044 } 2045 // Shorten the screen line to account for the terminal width 2046 if len(string(screenLine)) >= viewportWidth { 2047 screenLine = screenLine[:viewportWidth] 2048 } 2049 return screenLine 2050 } 2051 2052 // HorizontalScrollIfNeeded will scroll along the X axis, if needed 2053 func (e *Editor) HorizontalScrollIfNeeded(c *vt100.Canvas) { 2054 x := e.pos.sx 2055 w := 80 2056 if c != nil { 2057 w = int(c.W()) 2058 } 2059 if x < w { 2060 e.pos.offsetX = 0 2061 } else { 2062 e.pos.offsetX = (x - w) + 1 2063 e.pos.sx -= e.pos.offsetX 2064 } 2065 e.redraw = true 2066 e.redrawCursor = true 2067 } 2068 2069 // VerticalScrollIfNeeded will scroll along the X axis, if needed 2070 func (e *Editor) VerticalScrollIfNeeded(c *vt100.Canvas) { 2071 y := e.pos.sy 2072 h := 25 2073 if c != nil { 2074 h = int(c.H()) 2075 } 2076 if y < h { 2077 e.pos.offsetY = 0 2078 } else { 2079 e.pos.offsetY = (y - h) + 1 2080 e.pos.sy -= e.pos.offsetY 2081 } 2082 e.redraw = true 2083 e.redrawCursor = true 2084 } 2085 2086 // InsertFile inserts the contents of a file at the current location 2087 func (e *Editor) InsertFile(c *vt100.Canvas, filename string) error { 2088 data, err := os.ReadFile(filename) 2089 if err != nil { 2090 return err 2091 } 2092 s := opinionatedStringReplacer.Replace(trimRightSpace(string(data))) 2093 e.InsertStringAndMove(c, s) 2094 return nil 2095 } 2096 2097 // AbsFilename returns the absolute filename for this editor, 2098 // cleaned with filepath.Clean. 2099 func (e *Editor) AbsFilename() (string, error) { 2100 absFilename, err := filepath.Abs(e.filename) 2101 if err != nil { 2102 return "", err 2103 } 2104 return filepath.Clean(absFilename), nil 2105 } 2106 2107 // Switch replaces the current editor with a new Editor that opens the given file. 2108 // The undo stack is also swapped. 2109 // Only works for switching to one file, and then back again. 2110 func (e *Editor) Switch(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, lk *LockKeeper, filenameToOpen string) error { 2111 absFilename, err := e.AbsFilename() 2112 if err != nil { 2113 return err 2114 } 2115 2116 // About to switch from absFilename to filenameToOpen 2117 2118 if lk != nil { 2119 // Unlock and save the lock file 2120 lk.Unlock(absFilename) 2121 lk.Save() 2122 } 2123 2124 // Now open the header filename instead of the current file. Save the current file first. 2125 e.Save(c, tty) 2126 // Save the current location in the location history and write it to file 2127 e.SaveLocation(absFilename, locationHistory) 2128 2129 var ( 2130 e2 *Editor 2131 statusMessage string 2132 displayedImage bool 2133 ) 2134 2135 if switchBuffer.Len() == 1 { 2136 // Load the Editor from the switchBuffer if switchBuffer has length 1, then use that editor. 2137 switchBuffer.Restore(e) 2138 undo, switchUndoBackup = switchUndoBackup, undo 2139 } else { 2140 fnord := FilenameOrData{filenameToOpen, []byte{}, 0, false} 2141 e2, statusMessage, displayedImage, err = NewEditor(tty, c, fnord, LineNumber(0), ColNumber(0), e.Theme, e.syntaxHighlight, false, e.monitorAndReadOnly, e.nanoMode, e.createDirectoriesIfMissing, e.displayQuickHelp) 2142 if err == nil { // no issue 2143 // Save the current Editor to the switchBuffer if switchBuffer if empty, then use the new editor. 2144 switchBuffer.Snapshot(e) 2145 2146 // Now use e2 as the current editor 2147 *e = *e2 2148 (*e).lines = (*e2).lines 2149 (*e).pos = (*e2).pos 2150 } else if displayedImage { 2151 // internal error 2152 panic("displayed an image while switching from one Editor struct to another") 2153 } else { 2154 // internal error when switching from absFilename to filenameToOpen 2155 panic(err) 2156 } 2157 fnord.SetTitle() 2158 undo, switchUndoBackup = switchUndoBackup, undo 2159 } 2160 2161 if statusMessage != "" { 2162 status.SetMessageAfterRedraw(statusMessage) 2163 } 2164 2165 e.redraw = true 2166 e.redrawCursor = true 2167 2168 return err 2169 } 2170 2171 // Reload tries to load the current file again 2172 func (e *Editor) Reload(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, lk *LockKeeper) error { 2173 return e.Switch(c, tty, status, lk, e.filename) 2174 } 2175 2176 // TrimmedLine returns the current line, trimmed in both ends 2177 func (e *Editor) TrimmedLine() string { 2178 return strings.TrimSpace(e.CurrentLine()) 2179 } 2180 2181 // PreviousTrimmedLine returns the line above, trimmed in both ends 2182 func (e *Editor) PreviousTrimmedLine() string { 2183 return strings.TrimSpace(e.PreviousLine()) 2184 } 2185 2186 // NextTrimmedLine returns the line below, trimmed in both ends 2187 func (e *Editor) NextTrimmedLine() string { 2188 return strings.TrimSpace(e.NextLine()) 2189 } 2190 2191 // TrimmedLineAt returns the current line, trimmed in both ends 2192 func (e *Editor) TrimmedLineAt(lineIndex LineIndex) string { 2193 return strings.TrimSpace(e.Line(lineIndex)) 2194 } 2195 2196 // LineContentsFromCursorPosition returns the rest of the line, 2197 // from the current cursor position, trimmed. 2198 func (e *Editor) LineContentsFromCursorPosition() string { 2199 x, err := e.DataX() 2200 if err != nil { 2201 return "" 2202 } 2203 return strings.TrimSpace(e.CurrentLine()[x:]) 2204 } 2205 2206 // CurrentWord returns the current word under the cursor, or an empty string. 2207 // The word may contain numbers or dashes, but not spaces or special characters. 2208 // "." is included. 2209 func (e *Editor) CurrentWord() string { 2210 y := int(e.DataY()) 2211 runes, ok := e.lines[y] 2212 if !ok { 2213 // This should never happen 2214 return "" 2215 } 2216 2217 // Check if there are letters on the current line 2218 if len(runes) == 0 { 2219 return "" 2220 } 2221 2222 // Either find x or use the last index of the line 2223 x, err := e.DataX() 2224 if err != nil { 2225 x = len(runes) 2226 } 2227 2228 qualifies := func(r rune) bool { 2229 return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == '.' 2230 } 2231 2232 if x >= len(runes) { 2233 return "" 2234 } 2235 2236 // Check if the cursor is at a word 2237 if !qualifies(runes[x]) { 2238 return "" 2239 } 2240 2241 // Find the first letter of the word (or the start of the line) 2242 firstLetterIndex := 0 2243 for i := x; i >= 0; i-- { 2244 r := runes[i] 2245 if !qualifies(r) { 2246 break 2247 } 2248 firstLetterIndex = i 2249 } 2250 2251 // Loop from the first letter of the word, to the first non-letter. 2252 // Gather the letters. 2253 var word []rune 2254 for i := firstLetterIndex; i < len(runes); i++ { 2255 r := runes[i] 2256 if !qualifies(r) { 2257 break 2258 } 2259 // Gather the letters 2260 word = append(word, r) 2261 } 2262 2263 // Return the word 2264 return string(word) 2265 } 2266 2267 // LettersBeforeCursor returns the current word up until the cursor (for autocompletion) 2268 func (e *Editor) LettersBeforeCursor() string { 2269 y := int(e.DataY()) 2270 runes, ok := e.lines[y] 2271 if !ok { 2272 // This should never happen 2273 return "" 2274 } 2275 // Either find x or use the last index of the line 2276 x, err := e.DataX() 2277 if err != nil { 2278 x = len(runes) 2279 } 2280 2281 qualifies := func(r rune) bool { 2282 return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' 2283 } 2284 2285 // Loop from the position before the current one and then leftwards on the current line. 2286 // Gather the letters. 2287 var word []rune 2288 for i := x - 1; i >= 0; i-- { 2289 r := runes[i] 2290 if !qualifies(r) { 2291 break 2292 } 2293 // Gather the letters in reverse 2294 word = append([]rune{r}, word...) 2295 } 2296 2297 // Return the letters as a string 2298 return string(word) 2299 } 2300 2301 // LettersOrDotBeforeCursor returns the current word up until the cursor (for autocompletion). 2302 // Will also include ".". 2303 func (e *Editor) LettersOrDotBeforeCursor() string { 2304 y := int(e.DataY()) 2305 runes, ok := e.lines[y] 2306 if !ok { 2307 // This should never happen 2308 return "" 2309 } 2310 // Either find x or use the last index of the line 2311 x, err := e.DataX() 2312 if err != nil { 2313 x = len(runes) 2314 } 2315 2316 qualifies := func(r rune) bool { 2317 return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == '.' 2318 } 2319 2320 // Loop from the position before the current one and then leftwards on the current line. 2321 // Gather the letters. 2322 var word []rune 2323 for i := x - 1; i >= 0; i-- { 2324 r := runes[i] 2325 if !qualifies(r) { 2326 break 2327 } 2328 // Gather the letters in reverse 2329 word = append([]rune{r}, word...) 2330 } 2331 return string(word) 2332 } 2333 2334 // LastLineNumber returns the last line number (not line index) of the current file 2335 func (e *Editor) LastLineNumber() LineNumber { 2336 // The last line (by line number, not by index, e.Len() returns an index which is why there is no -1) 2337 return LineNumber(e.Len()) 2338 } 2339 2340 // UserInput asks the user to enter text, then collects the letters. No history. 2341 func (e *Editor) UserInput(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, title, defaultValue string, quickList []string, arrowsAreCountedAsLetters bool, tabInsertText string) (string, bool) { 2342 status.ClearAll(c) 2343 if defaultValue != "" { 2344 status.SetMessage(title + ": " + defaultValue) 2345 } else { 2346 status.SetMessage(title + ":") 2347 } 2348 status.ShowNoTimeout(c, e) 2349 cancel := false 2350 entered := defaultValue 2351 doneCollectingLetters := false 2352 for !doneCollectingLetters { 2353 if e.debugMode { 2354 e.DrawWatches(c, false) // don't reposition cursor 2355 e.DrawRegisters(c, false) // don't reposition cursor 2356 e.DrawInstructions(c, false) // don't reposition cursor 2357 e.DrawFlags(c, false) // don't reposition cursor 2358 e.DrawGDBOutput(c, false) // don't reposition cursor 2359 } 2360 pressed := tty.String() 2361 switch pressed { 2362 case "c:8", "c:127": // ctrl-h or backspace 2363 if len(entered) > 0 { 2364 entered = entered[:len(entered)-1] 2365 status.SetMessage(title + ": " + entered) 2366 status.ShowNoTimeout(c, e) 2367 } 2368 case "←", "→": // left arrow or right arrow 2369 fallthrough // cancel 2370 case "↑", "↓": // up arrow or down arrow 2371 if arrowsAreCountedAsLetters { 2372 entered += pressed 2373 status.SetMessage(title + ": " + entered) 2374 status.ShowNoTimeout(c, e) 2375 } 2376 // Is this a special quick command, where return is not needed, like "wq"? 2377 if hasS(quickList, entered) { 2378 break 2379 } 2380 fallthrough // cancel 2381 case "c:9": 2382 if tabInsertText != "" { 2383 entered = tabInsertText 2384 status.SetMessage(title + ": " + entered) 2385 status.ShowNoTimeout(c, e) 2386 } 2387 case "c:27", "c:3", "c:17", "c:24": // esc, ctrl-c, ctrl-q or ctrl-x 2388 cancel = true 2389 entered = "" 2390 fallthrough // done 2391 case "c:13": // return 2392 doneCollectingLetters = true 2393 default: 2394 entered += pressed 2395 status.SetMessage(title + ": " + entered) 2396 status.ShowNoTimeout(c, e) 2397 } 2398 // Is this a special quick command, where return is not needed, like "wq"? 2399 if hasS(quickList, entered) { 2400 break 2401 } 2402 } 2403 status.ClearAll(c) 2404 return entered, !cancel 2405 } 2406 2407 // MoveToNumber will try to move to the given line number + column number (given as strings) 2408 func (e *Editor) MoveToNumber(c *vt100.Canvas, status *StatusBar, lineNumber, lineColumn string) error { 2409 // Move to (x, y), line number first and then column number 2410 i, err := strconv.Atoi(lineNumber) 2411 if err != nil { 2412 return err 2413 } 2414 foundY := LineNumber(i) 2415 e.redraw, _ = e.GoTo(foundY.LineIndex(), c, status) 2416 e.redrawCursor = e.redraw 2417 x, err := strconv.Atoi(lineColumn) 2418 if err != nil { 2419 return err 2420 } 2421 foundX := x - 1 2422 tabs := strings.Count(e.Line(foundY.LineIndex()), "\t") 2423 e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1)) 2424 e.Center(c) 2425 return nil 2426 } 2427 2428 // MoveToLineColumnNumber will try to move to the given line number + column number (given as ints) 2429 func (e *Editor) MoveToLineColumnNumber(c *vt100.Canvas, status *StatusBar, lineNumber, lineColumn int, ignoreIndentation bool) error { 2430 // Move to (x, y), line number first and then column number 2431 foundY := LineNumber(lineNumber) 2432 e.redraw, _ = e.GoTo(foundY.LineIndex(), c, status) 2433 e.redrawCursor = e.redraw 2434 x := lineColumn 2435 foundX := x - 1 2436 tabs := strings.Count(e.Line(foundY.LineIndex()), "\t") 2437 e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1)) 2438 if ignoreIndentation { 2439 e.pos.sx += len(e.LeadingWhitespace()) 2440 } 2441 e.Center(c) 2442 return nil 2443 } 2444 2445 // MoveToIndex will try to move to the given line index + column index (given as strings) 2446 func (e *Editor) MoveToIndex(c *vt100.Canvas, status *StatusBar, lineIndex, lineColumnIndex string, subtractOne bool) error { 2447 // Move to (x, y), line number first and then column number 2448 i, err := strconv.Atoi(lineIndex) 2449 if err != nil { 2450 return err 2451 } 2452 if subtractOne { 2453 i-- 2454 } 2455 foundY := LineIndex(i) 2456 e.redraw, _ = e.GoTo(foundY, c, status) 2457 e.redrawCursor = e.redraw 2458 x, err := strconv.Atoi(lineColumnIndex) 2459 if err != nil { 2460 return err 2461 } 2462 if subtractOne { 2463 x-- 2464 } 2465 foundX := x - 1 2466 tabs := strings.Count(e.Line(foundY), "\t") 2467 e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1)) 2468 e.Center(c) 2469 return nil 2470 } 2471 2472 // GoToTop jumps and scrolls to the top of the file 2473 func (e *Editor) GoToTop(c *vt100.Canvas, status *StatusBar) { 2474 e.redraw = e.GoToLineNumber(1, c, status, true) 2475 } 2476 2477 // GoToMiddle jumps and scrolls to the middle of the file 2478 func (e *Editor) GoToMiddle(c *vt100.Canvas, status *StatusBar) { 2479 e.GoToLineNumber(LineNumber(e.Len()/2), c, status, true) 2480 } 2481 2482 // GoToEnd jumps and scrolls to the end of the file 2483 func (e *Editor) GoToEnd(c *vt100.Canvas, status *StatusBar) { 2484 // Go to the last line (by line number, not by index, e.Len() returns an index which is why there is no -1) 2485 e.redraw = e.GoToLineNumber(LineNumber(e.Len()), c, status, true) 2486 } 2487 2488 // SortBlock sorts the a block of lines, at the current position 2489 func (e *Editor) SortBlock(c *vt100.Canvas, status *StatusBar, bookmark *Position) { 2490 if e.CurrentLine() == "" { 2491 status.SetErrorMessage("no text block at the current position") 2492 return 2493 } 2494 y := e.LineIndex() 2495 e.Home() 2496 s := e.Block(y) 2497 var lines sort.StringSlice 2498 lines = strings.Split(s, "\n") 2499 if len(lines) == 0 { 2500 status.SetErrorMessage("no text block to sort") 2501 return 2502 } 2503 // Remove the last empty line, if it's there 2504 addEmptyLine := false 2505 if lines[len(lines)-1] == "" { 2506 lines = lines[:len(lines)-1] 2507 addEmptyLine = true 2508 } 2509 lines.Sort() 2510 e.GoTo(y, c, status) 2511 e.DeleteBlock(bookmark) 2512 e.GoTo(y, c, status) 2513 e.InsertBlock(c, lines, addEmptyLine) 2514 e.GoTo(y, c, status) 2515 } 2516 2517 // SmartSplitLineOnBlanks splits the current line on space, as separate lines, but not if spaces are within brackets, parentheses or curly brackets 2518 func (e *Editor) SmartSplitLineOnBlanks(c *vt100.Canvas, status *StatusBar, bookmark *Position) { 2519 if e.CurrentLine() == "" { 2520 status.SetErrorMessage("nothing to split on blanks") 2521 return 2522 } 2523 y := e.LineIndex() 2524 2525 // Split the current (trimmed) line on space 2526 lines := smartSplit(e.TrimmedLine()) 2527 2528 // Remove empty elements from the slice 2529 var trimmedLines []string 2530 for _, line := range lines { 2531 trimmedLine := strings.TrimSpace(line) 2532 if trimmedLine == "" { 2533 continue 2534 } 2535 trimmedLines = append(trimmedLines, trimmedLine) 2536 } 2537 2538 // Remove the current line and insert the new lines 2539 e.DeleteCurrentLineMoveBookmark(bookmark) 2540 const addEmptyLine = false 2541 e.InsertBlock(c, trimmedLines, addEmptyLine) 2542 e.GoTo(y, c, status) 2543 } 2544 2545 // ReplaceBlock replaces the current block with the given string, if possible 2546 func (e *Editor) ReplaceBlock(c *vt100.Canvas, status *StatusBar, bookmark *Position, s string) { 2547 if e.CurrentLine() == "" { 2548 status.SetErrorMessage("no text block at the current position") 2549 return 2550 } 2551 y := e.LineIndex() 2552 lines := strings.Split(s, "\n") 2553 if len(lines) == 0 { 2554 status.SetErrorMessage("no text block to replace") 2555 return 2556 } 2557 // Remove the last empty line, if it's there 2558 addEmptyLine := false 2559 if lines[len(lines)-1] == "" { 2560 lines = lines[:len(lines)-1] 2561 addEmptyLine = true 2562 } 2563 e.GoTo(y, c, status) 2564 e.DeleteBlock(bookmark) 2565 e.GoTo(y, c, status) 2566 e.InsertBlock(c, lines, addEmptyLine) 2567 e.GoTo(y, c, status) 2568 } 2569 2570 // DeleteBlock will deletes a block of lines at the current position 2571 func (e *Editor) DeleteBlock(bookmark *Position) { 2572 s := e.Block(e.LineIndex()) 2573 lines := strings.Split(s, "\n") 2574 if len(lines) == 0 { 2575 // Need at least 1 line to be able to cut "the rest" after the first line has been cut 2576 return 2577 } 2578 for range lines { 2579 e.DeleteLineMoveBookmark(e.LineIndex(), bookmark) 2580 } 2581 } 2582 2583 // InsertBlock will insert multiple lines at the current position, without trimming 2584 // If addEmptyLine is true, an empty line will be added at the end 2585 func (e *Editor) InsertBlock(c *vt100.Canvas, addLines []string, addEmptyLine bool) { 2586 e.InsertLineAbove() 2587 // copyLines contains the lines to be pasted, and they are > 1 2588 // the first line is skipped since that was already pasted when ctrl-v was pressed the first time 2589 lastIndex := len(addLines[1:]) - 1 2590 // If the first line has been pasted, and return has been pressed, paste the rest of the lines differently 2591 skipFirstLineInsert := e.EmptyRightTrimmedLine() 2592 // Insert the lines 2593 for i, line := range addLines { 2594 if i == lastIndex && len(strings.TrimSpace(line)) == 0 { 2595 // If the last line is blank, skip it 2596 break 2597 } 2598 if skipFirstLineInsert { 2599 skipFirstLineInsert = false 2600 } else { 2601 e.InsertLineBelow() 2602 e.Down(c, nil) // no status message if the end of document is reached, there should always be a new line 2603 } 2604 e.InsertStringAndMove(c, line) 2605 } 2606 if addEmptyLine { 2607 e.InsertLineBelow() 2608 e.Down(c, nil) // no status message if the end of document is reached, there should always be a new line 2609 } 2610 } 2611 2612 // LineIsBlank checks if the given line index is blank 2613 func (e *Editor) LineIsBlank(y LineIndex) bool { 2614 return strings.TrimSpace(e.Line(y)) == "" 2615 } 2616 2617 // NextLineIsBlank checks if the next line is blank 2618 func (e *Editor) NextLineIsBlank() bool { 2619 return e.LineIsBlank(e.DataY() + 1) 2620 } 2621 2622 // OnParenOrBracket checks if we are currently on a parenthesis or bracket 2623 func (e *Editor) OnParenOrBracket() bool { 2624 switch e.Rune() { 2625 case '(', ')', '{', '}', '[', ']': 2626 return true 2627 default: 2628 return false 2629 } 2630 } 2631 2632 // JumpToMatching can jump to a to matching parenthesis or bracket ([{. 2633 // Return true if a jump was possible and happened. 2634 func (e *Editor) JumpToMatching(c *vt100.Canvas) bool { 2635 const maxSearchLength = 256000 2636 2637 var r = e.Rune() 2638 // Find which opening and closing parenthesis/curly brackets to look for 2639 opening, closing := rune(0), rune(0) 2640 onparen := true 2641 switch r { 2642 case '(', ')': 2643 opening = '(' 2644 closing = ')' 2645 case '{', '}': 2646 opening = '{' 2647 closing = '}' 2648 case '[', ']': 2649 opening = '[' 2650 closing = ']' 2651 default: 2652 onparen = false 2653 } 2654 2655 if onparen { 2656 // Search either forwards or backwards to find a matching rune 2657 switch r { 2658 case '(', '{', '[': 2659 parcount := 0 2660 counter := 0 2661 found := false 2662 for !e.AtOrAfterEndOfDocument() && counter < maxSearchLength { 2663 counter++ 2664 if r := e.Rune(); r == closing { 2665 if parcount == 1 { 2666 found = true 2667 // FOUND, STOP 2668 break 2669 } 2670 parcount-- 2671 } else if r == opening { 2672 parcount++ 2673 } 2674 e.Next(c) 2675 } 2676 if !found { 2677 return false 2678 } 2679 case ')', '}', ']': 2680 parcount := 0 2681 counter := 0 2682 found := false 2683 for !e.AtStartOfDocument() && counter < maxSearchLength { 2684 counter++ 2685 if r := e.Rune(); r == opening { 2686 if parcount == 1 { 2687 found = true 2688 // FOUND, STOP 2689 break 2690 } 2691 parcount-- 2692 } else if r == closing { 2693 parcount++ 2694 } 2695 e.Prev(c) 2696 } 2697 if !found { 2698 return false 2699 } 2700 } 2701 e.redrawCursor = true 2702 e.redraw = true 2703 return true 2704 } 2705 return false 2706 } 2707 2708 // DeleteToEndOfLine (ctrl-k) 2709 func (e *Editor) DeleteToEndOfLine(c *vt100.Canvas, status *StatusBar, bookmark *Position, lastCopyY, lastPasteY, lastCutY *LineIndex) { 2710 if e.Empty() { 2711 status.SetMessage("Empty file") 2712 status.Show(c, e) 2713 return 2714 } 2715 // Reset the cut/copy/paste double-keypress detection 2716 *lastCopyY = -1 2717 *lastPasteY = -1 2718 *lastCutY = -1 2719 if e.EmptyLine() { 2720 e.DeleteCurrentLineMoveBookmark(bookmark) 2721 e.redraw = true 2722 e.redrawCursor = true 2723 return 2724 } 2725 e.DeleteRestOfLine() 2726 if e.EmptyRightTrimmedLine() { 2727 // Deleting the rest of the line cleared this line, 2728 // so just remove it. 2729 e.DeleteCurrentLineMoveBookmark(bookmark) 2730 // Then go to the end of the line, if needed 2731 if e.AfterEndOfLine() { 2732 e.End(c) 2733 } 2734 } 2735 e.redraw = true 2736 e.redrawCursor = true 2737 } 2738 2739 // CutSingleLine can be called when ctrl-x is pressed (or ctrl-k in nano mode) 2740 // returns true if a multi-line cut should happen instead 2741 func (e *Editor) CutSingleLine(status *StatusBar, bookmark *Position, lastCutY, lastCopyY, lastPasteY *LineIndex, copyLines *[]string, firstCopyAction *bool) (y LineIndex, multiLineCut bool) { 2742 y = e.DataY() 2743 line := e.Line(y) 2744 // Now check if there is anything to cut 2745 if len(strings.TrimSpace(line)) == 0 { 2746 // Nothing to cut, just remove the current line 2747 e.Home() 2748 e.DeleteCurrentLineMoveBookmark(bookmark) 2749 e.redraw = true 2750 e.redrawCursor = true 2751 // Check if ctrl-x was pressed once or twice, for this line 2752 return y, false 2753 } 2754 if *lastCutY != y { // Single line cut 2755 // Also close the portal, if any 2756 e.ClosePortal() 2757 *lastCutY = y 2758 *lastCopyY = -1 2759 *lastPasteY = -1 2760 // Copy the line internally 2761 *copyLines = []string{line} 2762 var err error 2763 if isDarwin() { 2764 // Copy the line to the clipboard 2765 err = pbcopy(line) 2766 } else { 2767 // Copy the line to the non-primary clipboard 2768 err = clip.WriteAll(line, e.primaryClipboard) 2769 } 2770 if err != nil && *firstCopyAction { 2771 if env.Has("WAYLAND_DISPLAY") && files.Which("wl-copy") == "" { // Wayland 2772 status.SetErrorMessage("The wl-copy utility (from wl-clipboard) is missing!") 2773 } else if env.Has("DISPLAY") && files.Which("xclip") == "" { 2774 status.SetErrorMessage("The xclip utility is missing!") 2775 } else if isDarwin() && files.Which("pbcopy") == "" { // pbcopy is missing, on macOS 2776 status.SetErrorMessage("The pbcopy utility is missing!") 2777 } 2778 } 2779 // Delete the line 2780 e.DeleteLineMoveBookmark(y, bookmark) 2781 // No status message is needed for the cut operation, because it's visible that lines are cut 2782 e.redrawCursor = true 2783 e.redraw = true 2784 return y, false 2785 } 2786 return y, true 2787 } 2788 2789 // CursorForward moves the cursor forward 1 step 2790 func (e *Editor) CursorForward(c *vt100.Canvas, status *StatusBar) { 2791 // If on the last line or before, go to the next character 2792 if e.DataY() <= LineIndex(e.Len()) { 2793 e.Next(c) 2794 } 2795 if e.AfterScreenWidth(c) { 2796 e.pos.SetOffsetX(e.pos.OffsetX() + 1) 2797 e.redraw = true 2798 e.pos.sx-- 2799 if e.pos.sx < 0 { 2800 e.pos.sx = 0 2801 } 2802 if e.AfterEndOfLine() { 2803 e.Down(c, status) 2804 } 2805 } else if e.AfterEndOfLine() { 2806 e.End(c) 2807 } 2808 e.SaveX(true) 2809 e.redrawCursor = true 2810 } 2811 2812 // CursorBackward moves the cursor backward 1 step 2813 func (e *Editor) CursorBackward(c *vt100.Canvas, status *StatusBar) { 2814 // movement if there is horizontal scrolling 2815 if e.pos.OffsetX() > 0 { 2816 if e.pos.sx > 0 { 2817 // Move one step left 2818 if e.TabToTheLeft() { 2819 e.pos.sx -= e.indentation.PerTab 2820 } else { 2821 e.pos.sx-- 2822 } 2823 } else { 2824 // Scroll one step left 2825 e.pos.SetOffsetX(e.pos.OffsetX() - 1) 2826 e.redraw = true 2827 } 2828 e.SaveX(true) 2829 } else if e.pos.sx > 0 { 2830 // no horizontal scrolling going on 2831 // Move one step left 2832 if e.TabToTheLeft() { 2833 e.pos.sx -= e.indentation.PerTab 2834 } else { 2835 e.pos.sx-- 2836 } 2837 e.SaveX(true) 2838 } else if e.DataY() > 0 { 2839 // no scrolling or movement to the left going on 2840 e.Up(c, status) 2841 e.End(c) 2842 // e.redraw = true 2843 } // else at the start of the document 2844 e.redrawCursor = true 2845 // Workaround for Konsole 2846 if e.pos.sx <= 2 { 2847 // Konsole prints "2H" here, but 2848 // no other terminal emulator does that 2849 e.redraw = true 2850 } 2851 } 2852 2853 // JoinLineWithNext tries to join the current line with the next. 2854 // If the next line is empty, the next line is removed. 2855 // Returns true if this and the next line had text and they were joined to this line. 2856 func (e *Editor) JoinLineWithNext(c *vt100.Canvas, bookmark *Position) bool { 2857 nextLineIndex := e.DataY() + 1 2858 e.redraw = true 2859 e.redrawCursor = true 2860 if e.EmptyRightTrimmedLineBelow() { 2861 // Just delete the line below if it's empty 2862 e.DeleteLineMoveBookmark(nextLineIndex, bookmark) 2863 return false 2864 } 2865 // Join the line below with this line. Also add a space in between. 2866 e.TrimLeft(nextLineIndex) // this is unproblematic, even at the end of the document 2867 e.End(c) 2868 e.InsertRune(c, ' ') 2869 e.WriteRune(c) 2870 e.Next(c) 2871 e.Delete() 2872 return true 2873 }