1package main 2 3import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "strings" 11 "time" 12 "unicode" 13 14 "github.com/cyrus-and/gdb" 15 "github.com/xyproto/mode" 16 "github.com/xyproto/vt100" 17) 18 19// Editor represents the contents and editor settings, but not settings related to the viewport or scrolling 20type Editor struct { 21 lines map[int][]rune // the contents of the current document 22 changed bool // has the contents changed, since last save? 23 tabsSpaces mode.TabsSpaces // spaces or tabs, and how many spaces per tab character 24 syntaxHighlight bool // syntax highlighting 25 rainbowParenthesis bool // rainbow parenthesis 26 pos Position // the current cursor and scroll position 27 searchTerm string // the current search term, used when searching 28 stickySearchTerm string // for going to the next match with ctrl-n, unless esc has been pressed 29 redraw bool // if the contents should be redrawn in the next loop 30 redrawCursor bool // if the cursor should be moved to the location it is supposed to be 31 lineBeforeSearch LineIndex // save the current line when jumping between search results 32 wrapWidth int // set to 80 or 100 to trigger word wrap when typing to that column 33 mode mode.Mode // a filetype mode, like for git or markdown 34 filename string // the current filename 35 locationHistory map[string]LineNumber // location history, for jumping to the last location when opening a file 36 quit bool // for indicating if the user wants to end the editor session 37 clearOnQuit bool // clear the terminal when quitting the editor, or not 38 wrapWhenTyping bool // wrap text at a certain limit when typing 39 slowLoad bool // was the initial file slow to load? (might be an indication of a slow disk or USB stick) 40 readOnly bool // is the file read-only when initializing o? 41 sameFilePortal *Portal // a portal that points to the same file 42 sshMode bool // is o used over ssh, tmux or screen, in a way that usually requires extra redrawing? 43 debugMode bool // in a mode where ctrl-b toggles breakpoints, ctrl-n steps to the next line and ctrl-space runs the application 44 statusMode bool // display a status line at all times at the bottom of the screen 45 gdb *gdb.Gdb // connection to gdb, if debugMode is enabled 46 previousX int // previous cursor position 47 previousY int // previous cursor position 48 Theme // editor theme, embedded struct 49} 50 51// NewCustomEditor takes: 52// * the number of spaces per tab (typically 2, 4 or 8) 53// * if the text should be syntax highlighted 54// * if rainbow parenthesis should be enabled 55// * if text edit mode is enabled (as opposed to "ASCII draw mode") 56// * the current scroll speed, in lines 57// * the following colors: 58// - text foreground 59// - text background 60// - search highlight 61// - multiline comment 62// * a syntax highlighting scheme 63// * a file mode 64func NewCustomEditor(tabsSpaces mode.TabsSpaces, rainbowParenthesis bool, scrollSpeed int, m mode.Mode, theme Theme, syntaxHighlight bool) *Editor { 65 e := &Editor{} 66 e.SetTheme(theme) 67 e.lines = make(map[int][]rune) 68 e.tabsSpaces = tabsSpaces 69 e.syntaxHighlight = syntaxHighlight 70 e.rainbowParenthesis = rainbowParenthesis 71 p := NewPosition(scrollSpeed) 72 e.pos = *p 73 // If the file is not to be highlighted, set word wrap to 99 (0 to disable) 74 if e.syntaxHighlight { 75 e.wrapWidth = 99 76 e.wrapWhenTyping = false 77 } 78 switch m { 79 case mode.Git: 80 // The subject should ideally be maximum 50 characters long, then the body of the 81 // git commit message can be 72 characters long. Because e-mail standards. 82 e.wrapWidth = 72 83 e.wrapWhenTyping = true 84 case mode.Markdown, mode.Text, mode.Blank: 85 e.wrapWidth = 99 86 e.wrapWhenTyping = false 87 } 88 e.mode = m 89 return e 90} 91 92// NewSimpleEditor return a new simple editor, where the settings are 4 spaces per tab, white text on black background, 93// no syntax highlighting, text edit mode (as opposed to ASCII draw mode), scroll 1 line at a time, color 94// search results magenta, use the default syntax highlighting scheme, don't use git mode and don't use markdown mode, 95// then set the word wrap limit at the given column width. 96func NewSimpleEditor(wordWrapLimit int) *Editor { 97 t := NewDefaultTheme() 98 e := NewCustomEditor(mode.DefaultTabsSpaces, false, 1, mode.Blank, t, false) 99 e.wrapWidth = wordWrapLimit 100 e.wrapWhenTyping = true 101 return e 102} 103 104// CopyLines will create a new map[int][]rune struct that is the copy of all the lines in the editor 105func (e *Editor) CopyLines() map[int][]rune { 106 lines2 := make(map[int][]rune) 107 for key, runes := range e.lines { 108 runes2 := make([]rune, len(runes)) 109 copy(runes2, runes) 110 lines2[key] = runes2 111 } 112 return lines2 113} 114 115// Set will store a rune in the editor data, at the given data coordinates 116func (e *Editor) Set(x int, index LineIndex, r rune) { 117 y := int(index) 118 if e.lines == nil { 119 e.lines = make(map[int][]rune) 120 } 121 _, ok := e.lines[y] 122 if !ok { 123 e.lines[y] = make([]rune, 0, x+1) 124 } 125 l := len(e.lines[y]) 126 if x < l { 127 e.lines[y][x] = r 128 e.changed = true 129 return 130 } 131 // If the line is too short, fill it up with spaces 132 if l <= x { 133 n := (x + 1) - l 134 e.lines[y] = append(e.lines[y], []rune(strings.Repeat(" ", n))...) 135 } 136 137 // Set the rune 138 e.lines[y][x] = r 139 e.changed = true 140} 141 142// Get will retrieve a rune from the editor data, at the given coordinates 143func (e *Editor) Get(x int, y LineIndex) rune { 144 if e.lines == nil { 145 return ' ' 146 } 147 runes, ok := e.lines[int(y)] 148 if !ok { 149 return ' ' 150 } 151 if x >= len(runes) { 152 return ' ' 153 } 154 return runes[x] 155} 156 157// Changed will return true if the contents were changed since last time this function was called 158func (e *Editor) Changed() bool { 159 return e.changed 160} 161 162// Line returns the contents of line number N, counting from 0 163func (e *Editor) Line(n LineIndex) string { 164 line, ok := e.lines[int(n)] 165 if ok { 166 var sb strings.Builder 167 for _, r := range line { 168 sb.WriteRune(r) 169 } 170 return sb.String() 171 } 172 return "" 173} 174 175// ScreenLine returns the screen contents of line number N, counting from 0. 176// The tabs are expanded. 177func (e *Editor) ScreenLine(n int) string { 178 line, ok := e.lines[n] 179 if ok { 180 var sb strings.Builder 181 skipX := e.pos.offsetX 182 for _, r := range line { 183 if skipX > 0 { 184 skipX-- 185 continue 186 } 187 sb.WriteRune(r) 188 } 189 tabSpace := strings.Repeat("\t", e.tabsSpaces.PerTab) 190 return strings.Replace(sb.String(), "\t", tabSpace, -1) 191 } 192 return "" 193} 194 195// LastDataPosition returns the last X index for this line, for the data (does not expand tabs) 196// Can be negative, if the line is empty. 197func (e *Editor) LastDataPosition(n LineIndex) int { 198 return len([]rune(e.Line(n))) - 1 199} 200 201// LastScreenPosition returns the last X index for this line, for the screen (expands tabs) 202// Can be negative, if the line is empty. 203func (e *Editor) LastScreenPosition(n LineIndex) int { 204 extraSpaceBecauseOfTabs := int(e.CountRune('\t', n) * (e.tabsSpaces.PerTab - 1)) 205 return (e.LastDataPosition(n) + extraSpaceBecauseOfTabs) 206} 207 208// LastTextPosition returns the last X index for this line, regardless of horizontal scrolling. 209// Can be negative if the line is empty. Tabs are expanded. 210func (e *Editor) LastTextPosition(n LineIndex) int { 211 extraSpaceBecauseOfTabs := int(e.CountRune('\t', n) * (e.tabsSpaces.PerTab - 1)) 212 return (e.LastDataPosition(n) + extraSpaceBecauseOfTabs) 213} 214 215// FirstScreenPosition returns the first X index for this line, that is not '\t' or ' '. 216// Does not deal with the X offset. 217func (e *Editor) FirstScreenPosition(n LineIndex) uint { 218 var ( 219 counter uint 220 spacesPerTab = uint(e.tabsSpaces.PerTab) 221 ) 222 for _, r := range e.Line(n) { 223 if r == '\t' { 224 counter += spacesPerTab 225 } else if r == ' ' { 226 counter++ 227 } else { 228 break 229 } 230 } 231 return counter 232} 233 234// FirstDataPosition returns the first X index for this line, that is not whitespace. 235func (e *Editor) FirstDataPosition(n LineIndex) int { 236 counter := 0 237 for _, r := range e.Line(n) { 238 if !unicode.IsSpace(r) { 239 break 240 } 241 counter++ 242 } 243 return counter 244} 245 246// CountRune will count the number of instances of the rune r in the line n 247func (e *Editor) CountRune(r rune, n LineIndex) int { 248 var counter int 249 line, ok := e.lines[int(n)] 250 if ok { 251 for _, l := range line { 252 if l == r { 253 counter++ 254 } 255 } 256 } 257 return counter 258} 259 260// Len returns the number of lines 261func (e *Editor) Len() int { 262 maxy := 0 263 for y := range e.lines { 264 if y > maxy { 265 maxy = y 266 } 267 } 268 return maxy + 1 269} 270 271// String returns the contents of the editor 272func (e *Editor) String() string { 273 var sb strings.Builder 274 l := e.Len() 275 for i := 0; i < l; i++ { 276 sb.WriteString(e.Line(LineIndex(i)) + "\n") 277 } 278 return sb.String() 279} 280 281// Clear removes all data from the editor 282func (e *Editor) Clear() { 283 e.lines = make(map[int][]rune) 284 e.changed = true 285} 286 287// Load will try to load a file. The file is assumed to be checked to already exist. 288// Returns a warning message (possibly empty) and an error type 289func (e *Editor) Load(c *vt100.Canvas, tty *vt100.TTY, filename string) (string, error) { 290 var ( 291 message string 292 data []byte 293 err error 294 ) 295 296 // Start a spinner, in a short while 297 quitChan := Spinner(c, tty, fmt.Sprintf("Reading %s... ", filename), fmt.Sprintf("reading %s: stopped by user", filename), 200*time.Millisecond, e.ItalicsColor) 298 299 defer func() { 300 // Stop the spinner 301 quitChan <- true 302 }() 303 304 start := time.Now() 305 306 // Check if the file extension is ".class" and if "jad" is installed 307 if filepath.Ext(filename) == ".class" && which("jad") != "" { 308 if data, err = e.LoadClass(filename); err != nil { 309 return "Could not run jad", err 310 } 311 } else { 312 // Read the file and check if it could be read 313 data, err = ioutil.ReadFile(filename) 314 if err != nil { 315 return message, err 316 } 317 } 318 319 // 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 320 e.slowLoad = time.Since(start) > 400*time.Millisecond 321 322 // Opinonated replacements 323 data = opinionatedByteReplacer(data) 324 325 // Load the data 326 e.LoadBytes(data) 327 328 // Mark the data as "not changed" 329 e.changed = false 330 331 return message, nil 332} 333 334// LoadBytes replaces the current editor contents with the given bytes 335func (e *Editor) LoadBytes(data []byte) { 336 e.Clear() 337 338 byteLines := bytes.Split(data, []byte{'\n'}) 339 340 lb := len(byteLines) 341 342 // If the last line is empty, skip it 343 if len(byteLines) > 0 && len(byteLines[lb-1]) == 0 { 344 byteLines = byteLines[:lb-1] 345 lb-- 346 } 347 348 // One allocation for all the lines 349 e.lines = make(map[int][]rune, lb) 350 351 // Place the lines into the editor 352 for y, byteLine := range byteLines { 353 e.lines[y] = []rune(string(byteLine)) 354 } 355 356 // Mark the editor contents as "changed" 357 e.changed = true 358} 359 360// PrepareEmpty prepares an empty textual representation of a given filename. 361// If it's an image, there will be text placeholders for pixels. 362// If it's anything else, it will just be blank. 363// Returns an editor mode and an error type. 364func (e *Editor) PrepareEmpty(c *vt100.Canvas, tty *vt100.TTY, filename string) (mode.Mode, error) { 365 var ( 366 m mode.Mode = mode.Blank 367 data []byte 368 err error 369 ) 370 371 // Check if the data could be prepared 372 if err != nil { 373 return m, err 374 } 375 376 lines := strings.Split(string(data), "\n") 377 e.Clear() 378 for y, line := range lines { 379 counter := 0 380 for _, letter := range line { 381 e.Set(counter, LineIndex(y), letter) 382 counter++ 383 } 384 } 385 // Mark the data as "not changed" 386 e.changed = false 387 388 return m, nil 389} 390 391// Save will try to save the current editor contents to file. 392// It needs a canvas in case trailing spaces are stripped and the cursor needs to move to the end. 393func (e *Editor) Save(c *vt100.Canvas, tty *vt100.TTY) error { 394 395 // Save the current position 396 bookmark := e.pos.Copy() 397 398 // Strip trailing spaces on all lines 399 l := e.Len() 400 changed := false 401 for i := 0; i < l; i++ { 402 if e.TrimRight(LineIndex(i)) { 403 changed = true 404 } 405 } 406 407 // Trim away trailing whitespace 408 s := strings.TrimRightFunc(e.String(), unicode.IsSpace) 409 410 // Make additional replacements, and add a final newline 411 s = opinionatedStringReplacer.Replace(s) + "\n" 412 413 // TODO: Auto-detect tabs/spaces instead of per-language assumptions 414 if e.mode.Spaces() { 415 // NOTE: This is a hack, that can only replace 10 levels deep. 416 for level := 10; level > 0; level-- { 417 fromString := "\n" + strings.Repeat("\t", level) 418 toString := "\n" + strings.Repeat(" ", level*e.tabsSpaces.PerTab) 419 s = strings.Replace(s, fromString, toString, -1) 420 } 421 } 422 423 // Should the file be saved with the executable bit enabled? 424 // (Does it either start with a shebang or reside in a common bin directory like /usr/bin?) 425 shebang := aBinDirectory(e.filename) || strings.HasPrefix(s, "#!") 426 427 // Mark the data as "not changed" 428 e.changed = false 429 430 // Default file mode (0644 for regular files, 0755 for executable files) 431 var fileMode os.FileMode = 0644 432 433 // Checking the syntax highlighting makes it easy to press `ctrl-t` before saving a script, 434 // to toggle the executable bit on or off. This is only for files that start with "#!". 435 // Also, if the file is in one of the common bin directories, like "/usr/bin", then assume that it 436 // is supposed to be executable. 437 if shebang && e.syntaxHighlight { 438 // This is both a script file and the syntax highlight is enabled. 439 fileMode = 0755 440 } 441 442 // Start a spinner, in a short while 443 quitChan := Spinner(c, tty, fmt.Sprintf("Saving %s... ", e.filename), fmt.Sprintf("saving %s: stopped by user", e.filename), 200*time.Millisecond, e.ItalicsColor) 444 445 // Save the file and return any errors 446 if err := ioutil.WriteFile(e.filename, []byte(s), fileMode); err != nil { 447 // Stop the spinner and return 448 quitChan <- true 449 return err 450 } 451 452 // Apparently, this file isn't read-only, since saving went fine 453 e.readOnly = false 454 455 // "chmod +x" or "chmod -x". This is needed after saving the file, in order to toggle the executable bit. 456 // rust source may start with something like "#![feature(core_intrinsics)]", so avoid that. 457 if shebang && e.mode != mode.Rust && !e.readOnly { 458 // Call Chmod, but ignore errors (since this is just a bonus and not critical) 459 os.Chmod(e.filename, fileMode) 460 e.syntaxHighlight = true 461 } 462 463 // Stop the spinner 464 quitChan <- true 465 466 e.redrawCursor = true 467 468 // Trailing spaces may be trimmed, so move to the end, if needed 469 if changed { 470 e.GoToPosition(c, nil, *bookmark) 471 if e.AfterEndOfLine() { 472 e.EndNoTrim(c) 473 } 474 // Do the redraw manually before showing the status message 475 respectOffset := true 476 redrawCanvas := false 477 e.DrawLines(c, respectOffset, redrawCanvas) 478 e.redraw = false 479 } 480 481 // All done 482 return nil 483} 484 485// TrimRight will remove whitespace from the end of the given line number 486// Returns true if the line was trimmed 487func (e *Editor) TrimRight(index LineIndex) bool { 488 changed := false 489 n := int(index) 490 if line, ok := e.lines[n]; ok { 491 newRunes := []rune(strings.TrimRightFunc(string(line), unicode.IsSpace)) 492 // TODO: Just compare lengths instead of contents? 493 if string(newRunes) != string(line) { 494 e.lines[n] = newRunes 495 changed = true 496 } 497 } 498 return changed 499} 500 501// TrimLeft will remove whitespace from the start of the given line number 502// Returns true if the line was trimmed 503func (e *Editor) TrimLeft(index LineIndex) bool { 504 changed := false 505 n := int(index) 506 if line, ok := e.lines[n]; ok { 507 newRunes := []rune(strings.TrimLeftFunc(string(line), unicode.IsSpace)) 508 // TODO: Just compare lengths instead of contents? 509 if string(newRunes) != string(line) { 510 e.lines[n] = newRunes 511 changed = true 512 } 513 } 514 return changed 515} 516 517// StripSingleLineComment will strip away trailing single-line comments. 518// TODO: Also strip trailing /* ... */ comments 519func (e *Editor) StripSingleLineComment(line string) string { 520 commentMarker := e.SingleLineCommentMarker() 521 if strings.Count(line, commentMarker) == 1 { 522 p := strings.Index(line, commentMarker) 523 return strings.TrimSpace(line[:p]) 524 } 525 return line 526} 527 528// DeleteRestOfLine will delete the rest of the line, from the given position 529func (e *Editor) DeleteRestOfLine() { 530 x, err := e.DataX() 531 if err != nil { 532 // position is after the data, do nothing 533 return 534 } 535 y := int(e.DataY()) 536 if e.lines == nil { 537 e.lines = make(map[int][]rune) 538 } 539 v, ok := e.lines[y] 540 if !ok { 541 return 542 } 543 if v == nil { 544 e.lines[y] = make([]rune, 0) 545 } 546 if x > len([]rune(e.lines[y])) { 547 return 548 } 549 e.lines[y] = e.lines[y][:x] 550 e.changed = true 551} 552 553// DeleteLine will delete the given line index 554func (e *Editor) DeleteLine(n LineIndex) { 555 if n < 0 { 556 // This should never happen 557 return 558 } 559 lastLineIndex := LineIndex(e.Len() - 1) 560 endOfDocument := n >= lastLineIndex 561 if endOfDocument { 562 // Just delete this line 563 delete(e.lines, int(n)) 564 return 565 } 566 // TODO: Rely on the length of the hash map for finding the index instead of 567 // searching through each line number key. 568 var maxIndex LineIndex 569 found := false 570 for k := range e.lines { 571 if LineIndex(k) > maxIndex { 572 maxIndex = LineIndex(k) 573 found = true 574 } 575 } 576 if !found { 577 // This should never happen 578 return 579 } 580 if _, ok := e.lines[int(maxIndex)]; !ok { 581 // The line numbers and the length of e.lines does not match 582 return 583 } 584 // Shift all lines after y: 585 // shift all lines after n one step closer to n, overwriting e.lines[n] 586 for index := n; index <= (maxIndex - 1); index++ { 587 i := int(index) 588 e.lines[i] = e.lines[i+1] 589 } 590 // Then delete the final item 591 delete(e.lines, int(maxIndex)) 592 593 // This changes the document 594 e.changed = true 595 596 // Make sure no lines are nil 597 e.MakeConsistent() 598} 599 600// DeleteLineMoveBookmark will delete the given line index and also move the bookmark if it's after n 601func (e *Editor) DeleteLineMoveBookmark(n LineIndex, bookmark *Position) { 602 if bookmark != nil && bookmark.LineIndex() > n { 603 bookmark.DecY() 604 } 605 e.DeleteLine(n) 606} 607 608// DeleteCurrentLineMoveBookmark will delete the current line and also move the bookmark one up 609// if it's after the current line. 610func (e *Editor) DeleteCurrentLineMoveBookmark(bookmark *Position) { 611 e.DeleteLineMoveBookmark(e.DataY(), bookmark) 612} 613 614// Delete will delete a character at the given position 615func (e *Editor) Delete() { 616 y := int(e.DataY()) 617 lineLen := len([]rune(e.lines[y])) 618 if _, ok := e.lines[y]; !ok || lineLen == 0 || (lineLen == 1 && unicode.IsSpace(e.lines[y][0])) { 619 // All keys in the map that are > y should be shifted -1. 620 // This also overwrites e.lines[y]. 621 e.DeleteLine(LineIndex(y)) 622 e.changed = true 623 return 624 } 625 x, err := e.DataX() 626 if err != nil || x > len([]rune(e.lines[y]))-1 { 627 // on the last index, just use every element but x 628 e.lines[y] = e.lines[y][:x] 629 // check if the next line exists 630 if _, ok := e.lines[y+1]; ok { 631 // then add the contents of the next line, if available 632 nextLine, ok := e.lines[y+1] 633 if ok && len([]rune(nextLine)) > 0 { 634 e.lines[y] = append(e.lines[y], nextLine...) 635 // then delete the next line 636 e.DeleteLine(LineIndex(y + 1)) 637 } 638 } 639 e.changed = true 640 return 641 } 642 // Delete just this character 643 e.lines[y] = append(e.lines[y][:x], e.lines[y][x+1:]...) 644 645 e.changed = true 646 647 // Make sure no lines are nil 648 e.MakeConsistent() 649} 650 651// Empty will check if the current editor contents are empty or not. 652// If there's only one line left and it is only whitespace, that will be considered empty as well. 653func (e *Editor) Empty() bool { 654 l := len(e.lines) 655 if l == 0 { 656 return true 657 } 658 if l == 1 { 659 // Regardless of line number key, check the contents of the one remaining trimmed line 660 for _, line := range e.lines { 661 return len(strings.TrimSpace(string(line))) == 0 662 } 663 } 664 // > 1 lines 665 return false 666} 667 668// MakeConsistent creates an empty slice of runes for any empty lines, 669// to make sure that no line number below e.Len() points to a nil map. 670func (e *Editor) MakeConsistent() { 671 // Check if the keys in the map are consistent 672 for i := 0; i < len(e.lines); i++ { 673 if _, found := e.lines[i]; !found { 674 e.lines[i] = make([]rune, 0) 675 e.changed = true 676 } 677 } 678} 679 680// WithinLimit will check if a line is within the word wrap limit, 681// given a Y position. 682func (e *Editor) WithinLimit(y LineIndex) bool { 683 return len(e.lines[int(y)]) < e.wrapWidth 684} 685 686// LastWord will return the last word of a line, 687// given a Y position. Returns an empty string if there is no last word. 688func (e *Editor) LastWord(y int) string { 689 // TODO: Use a faster method 690 words := strings.Fields(strings.TrimSpace(string(e.lines[y]))) 691 if len(words) > 0 { 692 return words[len(words)-1] 693 } 694 return "" 695} 696 697// SplitOvershoot will split the line into a first part that is within the 698// word wrap length and a second part that is the overshooting part. 699// y is the line index (y position, counting from 0). 700// isSpace is true if a space has just been inserted on purpose at the current position. 701// returns true if there was a space at the split point. 702func (e *Editor) SplitOvershoot(index LineIndex, isSpace bool) ([]rune, []rune, bool) { 703 hasSpace := false 704 705 y := int(index) 706 707 // Maximum word length to not keep as one word 708 maxDistance := e.wrapWidth / 2 709 if e.WithinLimit(index) { 710 return e.lines[y], make([]rune, 0), false 711 } 712 splitPosition := e.wrapWidth 713 if isSpace { 714 splitPosition, _ = e.DataX() 715 } else { 716 // Starting at the split position, move left until a space is reached (or the start of the line). 717 // If a space is reached, check if it is too far away from n to be used as a split position, or not. 718 spacePosition := -1 719 for i := splitPosition; i >= 0; i-- { 720 if i < len(e.lines[y]) && unicode.IsSpace(e.lines[y][i]) { 721 // Found a space at position i 722 spacePosition = i 723 break 724 } 725 } 726 // Found a better position to split, at a nearby space? 727 if spacePosition != -1 { 728 hasSpace = true 729 distance := splitPosition - spacePosition 730 if distance > maxDistance { 731 // To far away, don't use this as a split point, 732 // stick to the hard split. 733 } else { 734 // Okay, we found a better split point. 735 splitPosition = spacePosition 736 } 737 } 738 } 739 740 // Split the line into two parts 741 742 n := splitPosition 743 // Make space for the two parts 744 first := make([]rune, len(e.lines[y][:n])) 745 second := make([]rune, len(e.lines[y][n:])) 746 // Copy the line into first and second 747 copy(first, e.lines[y][:n]) 748 copy(second, e.lines[y][n:]) 749 750 // If the second part starts with a space, remove it 751 if len(second) > 0 && unicode.IsSpace(second[0]) { 752 second = second[1:] 753 hasSpace = true 754 } 755 756 return first, second, hasSpace 757} 758 759// WrapAllLinesAt will word wrap all lines that are longer than n, 760// with a maximum overshoot of too long words (measured in runes) of maxOvershoot. 761// Returns true if any lines were wrapped. 762func (e *Editor) WrapAllLinesAt(n, maxOvershoot int) bool { 763 // This is not even called when the problematic insert behavior occurs 764 765 wrapped := false 766 insertedLines := 0 767 768 y := e.DataY() 769 770 for i := 0; i < e.Len(); i++ { 771 if e.WithinLimit(LineIndex(i)) { 772 continue 773 } 774 wrapped = true 775 776 first, second, spaceBetween := e.SplitOvershoot(LineIndex(i), false) 777 778 if len(first) > 0 && len(second) > 0 { 779 780 e.lines[i] = first 781 if spaceBetween { 782 second = append(second, ' ') 783 } 784 e.lines[i+1] = append(second, e.lines[i+1]...) 785 e.InsertLineBelowAt(LineIndex(i + 1)) 786 787 // This isn't perfect, but it helps move the cursor somewhere in 788 // the vicinity of where the line was before word wrapping. 789 // TODO: Make the cursor placement exact. 790 if LineIndex(i) < y { 791 insertedLines++ 792 } 793 794 e.changed = true 795 } 796 } 797 798 // Move the cursor as well, after wrapping 799 if insertedLines > 0 { 800 e.pos.sy += insertedLines 801 if e.pos.sy < 0 { 802 e.pos.sy = 0 803 } else if e.pos.sy >= len(e.lines) { 804 e.pos.sy = len(e.lines) - 1 805 } 806 e.redraw = true 807 e.redrawCursor = true 808 } 809 810 return wrapped 811} 812 813// InsertLineAbove will attempt to insert a new line above the current position 814func (e *Editor) InsertLineAbove() { 815 lineIndex := e.DataY() 816 817 if e.sameFilePortal != nil { 818 e.sameFilePortal.NewLineInserted(lineIndex) 819 } 820 821 y := int(lineIndex) 822 823 // Create new set of lines 824 lines2 := make(map[int][]rune) 825 826 // If at the first line, just add a line at the top 827 if y == 0 { 828 829 // Insert a blank line 830 lines2[0] = make([]rune, 0) 831 // Then insert all the other lines, shifted by 1 832 for k, v := range e.lines { 833 lines2[k+1] = v 834 } 835 y++ 836 837 } else { 838 839 // For each line in the old map, if at (y-1), insert a blank line 840 // (insert a blank line above) 841 for k, v := range e.lines { 842 if k < (y - 1) { 843 lines2[k] = v 844 } else if k == (y - 1) { 845 lines2[k] = v 846 lines2[k+1] = make([]rune, 0) 847 } else if k > (y - 1) { 848 lines2[k+1] = v 849 } 850 } 851 852 } 853 854 // Use the new set of lines 855 e.lines = lines2 856 857 // Make sure no lines are nil 858 e.MakeConsistent() 859 860 // Skip trailing newlines after this line 861 for i := len(e.lines); i > y; i-- { 862 if len([]rune(e.lines[i])) == 0 { 863 delete(e.lines, i) 864 } else { 865 break 866 } 867 } 868 e.changed = true 869} 870 871// InsertLineBelow will attempt to insert a new line below the current position 872func (e *Editor) InsertLineBelow() { 873 lineIndex := e.DataY() 874 if e.sameFilePortal != nil { 875 e.sameFilePortal.NewLineInserted(lineIndex) 876 } 877 e.InsertLineBelowAt(lineIndex) 878} 879 880// InsertLineBelowAt will attempt to insert a new line below the given y position 881func (e *Editor) InsertLineBelowAt(index LineIndex) { 882 y := int(index) 883 884 // Make sure no lines are nil 885 e.MakeConsistent() 886 887 // If we are the the last line, add an empty line at the end and return 888 if y == (len(e.lines) - 1) { 889 e.lines[int(y)+1] = make([]rune, 0) 890 e.changed = true 891 return 892 } 893 894 // Create new set of lines, with room for one more 895 lines2 := make(map[int][]rune, len(e.lines)+1) 896 897 // For each line in the old map, if at y, insert a blank line 898 // (insert a blank line below) 899 for k, v := range e.lines { 900 if k < y { 901 lines2[k] = v 902 } else if k == y { 903 lines2[k] = v 904 lines2[k+1] = make([]rune, 0) 905 } else if k > y { 906 lines2[k+1] = v 907 } 908 } 909 // Use the new set of lines 910 e.lines = lines2 911 912 // Skip trailing newlines after this line 913 for i := len(e.lines); i > y; i-- { 914 if len([]rune(e.lines[i])) == 0 { 915 delete(e.lines, i) 916 } else { 917 break 918 } 919 } 920 921 e.changed = true 922} 923 924// Insert will insert a rune at the given position, with no word wrap, 925// but MakeConsisten will be called. 926func (e *Editor) Insert(r rune) { 927 // Ignore it if the current position is out of bounds 928 x, _ := e.DataX() 929 930 y := int(e.DataY()) 931 932 // If there are no lines, initialize and set the 0th rune to the given one 933 if e.lines == nil { 934 e.lines = make(map[int][]rune) 935 e.lines[0] = []rune{r} 936 return 937 } 938 939 // If the current line is empty, initialize it with a line that is just the given rune 940 _, ok := e.lines[y] 941 if !ok { 942 e.lines[y] = []rune{r} 943 return 944 } 945 if len([]rune(e.lines[y])) < x { 946 // Can only insert in the existing block of text 947 return 948 } 949 newlineLength := len(e.lines[y]) + 1 950 newline := make([]rune, newlineLength) 951 for i := 0; i < x; i++ { 952 newline[i] = e.lines[y][i] 953 } 954 newline[x] = r 955 for i := x + 1; i < newlineLength; i++ { 956 newline[i] = e.lines[y][i-1] 957 } 958 e.lines[y] = newline 959 960 e.changed = true 961 962 // Make sure no lines are nil 963 e.MakeConsistent() 964} 965 966// CreateLineIfMissing will create a line at the given Y index, if it's missing 967func (e *Editor) CreateLineIfMissing(n LineIndex) { 968 if e.lines == nil { 969 e.lines = make(map[int][]rune) 970 } 971 _, ok := e.lines[int(n)] 972 if !ok { 973 e.lines[int(n)] = make([]rune, 0) 974 e.changed = true 975 } 976} 977 978// SetColors will set the current editor theme (foreground, background). 979// The background color should be a background attribute (like vt100.BackgroundBlue). 980func (e *Editor) SetColors(fg, bg vt100.AttributeColor) { 981 e.Foreground = fg 982 e.Background = bg 983} 984 985// WordCount returns the number of spaces in the text + 1 986func (e *Editor) WordCount() int { 987 return len(strings.Fields(e.String())) 988} 989 990// ToggleSyntaxHighlight toggles syntax highlighting 991func (e *Editor) ToggleSyntaxHighlight() { 992 e.syntaxHighlight = !e.syntaxHighlight 993} 994 995// ToggleRainbow toggles rainbow parenthesis 996func (e *Editor) ToggleRainbow() { 997 e.rainbowParenthesis = !e.rainbowParenthesis 998} 999 1000// SetRainbow enables or disables rainbow parenthesis 1001func (e *Editor) SetRainbow(rainbowParenthesis bool) { 1002 e.rainbowParenthesis = rainbowParenthesis 1003} 1004 1005// SetLine will fill the given line index with the given string. 1006// Any previous contents of that line is removed. 1007func (e *Editor) SetLine(n LineIndex, s string) { 1008 e.CreateLineIfMissing(n) 1009 e.lines[int(n)] = make([]rune, 0) 1010 counter := 0 1011 // It's important not to use the index value when looping over a string, 1012 // unless the byte index is what one's after, as opposed to the rune index. 1013 for _, letter := range s { 1014 e.Set(counter, n, letter) 1015 counter++ 1016 } 1017} 1018 1019// SetCurrentLine will replace the current line with the given string 1020func (e *Editor) SetCurrentLine(s string) { 1021 e.SetLine(e.DataY(), s) 1022} 1023 1024// SplitLine will, at the given position, split the line in two. 1025// The right side of the contents is moved to a new line below. 1026func (e *Editor) SplitLine() bool { 1027 x, err := e.DataX() 1028 if err != nil { 1029 // After contents, this should not happen, do nothing 1030 return false 1031 } 1032 1033 y := e.DataY() 1034 1035 // Get the contents of this line 1036 runeLine := e.lines[int(y)] 1037 if len(runeLine) < 2 { 1038 // Did not split 1039 return false 1040 } 1041 leftContents := strings.TrimRightFunc(string(runeLine[:x]), unicode.IsSpace) 1042 rightContents := string(runeLine[x:]) 1043 // Insert a new line above this one 1044 e.InsertLineAbove() 1045 // Replace this line with the left contents 1046 e.SetLine(y, leftContents) 1047 e.SetLine(y+1, rightContents) 1048 // Splitted 1049 return true 1050} 1051 1052// DataX will return the X position in the data (as opposed to the X position in the viewport) 1053func (e *Editor) DataX() (int, error) { 1054 // the y position in the data is the lines scrolled + current screen cursor Y position 1055 dataY := e.pos.offsetY + e.pos.sy 1056 // get the current line of text 1057 screenCounter := 0 // counter for the characters on the screen 1058 // loop, while also keeping track of tab expansion 1059 // add a space to allow to jump to the position after the line and get a valid data position 1060 found := false 1061 dataX := 0 1062 runeCounter := 0 1063 for _, r := range e.lines[dataY] { 1064 // When we reached the correct screen position, use i as the data position 1065 if screenCounter == (e.pos.sx + e.pos.offsetX) { 1066 dataX = runeCounter 1067 found = true 1068 break 1069 } 1070 // Increase the counter, based on the current rune 1071 if r == '\t' { 1072 screenCounter += e.tabsSpaces.PerTab 1073 } else { 1074 screenCounter++ 1075 } 1076 runeCounter++ 1077 } 1078 if !found { 1079 return runeCounter, errors.New("position is after data") 1080 } 1081 // Return the data cursor 1082 return dataX, nil 1083} 1084 1085// DataY will return the Y position in the data (as opposed to the Y position in the viewport) 1086func (e *Editor) DataY() LineIndex { 1087 return LineIndex(e.pos.offsetY + e.pos.sy) 1088} 1089 1090// SetRune will set a rune at the current data position 1091func (e *Editor) SetRune(r rune) { 1092 // Only set a rune if x is within the current line contents 1093 if x, err := e.DataX(); err == nil { 1094 e.Set(x, e.DataY(), r) 1095 } 1096} 1097 1098// NextLine will go to the start of the next line, with scrolling 1099func (e *Editor) NextLine(y LineIndex, c *vt100.Canvas, status *StatusBar) { 1100 e.pos.sx = 0 1101 e.pos.offsetX = 0 1102 e.GoTo(y+1, c, status) 1103} 1104 1105// InsertBelow will insert the given rune at the start of the line below, 1106// starting a new line if required. 1107func (e *Editor) InsertBelow(y int, r rune) { 1108 if _, ok := e.lines[y+1]; !ok { 1109 // If the next line does not exist, create one containing just "r" 1110 e.lines[y+1] = []rune{r} 1111 } else if len(e.lines[y+1]) > 0 { 1112 // If the next line is non-empty, insert "r" at the start 1113 e.lines[y+1] = append([]rune{r}, e.lines[y+1][:]...) 1114 } else { 1115 // The next line exists, but is of length 0, should not happen, just replace it 1116 e.lines[y+1] = []rune{r} 1117 } 1118} 1119 1120// InsertStringBelow will insert the given string at the start of the line below, 1121// starting a new line if required. 1122func (e *Editor) InsertStringBelow(y int, s string) { 1123 if _, ok := e.lines[y+1]; !ok { 1124 // If the next line does not exist, create one containing the string 1125 e.lines[y+1] = []rune(s) 1126 } else if len(e.lines[y+1]) > 0 { 1127 // If the next line is non-empty, insert the string at the start 1128 e.lines[y+1] = append([]rune(s), e.lines[y+1][:]...) 1129 } else { 1130 // The next line exists, but is of length 0, should not happen, just replace it 1131 e.lines[y+1] = []rune(s) 1132 } 1133} 1134 1135// InsertStringAndMove will insert a string at the current data position 1136// and possibly move down. This will also call e.WriteRune, e.Down and e.Next, as needed. 1137func (e *Editor) InsertStringAndMove(c *vt100.Canvas, s string) { 1138 for _, r := range s { 1139 if r == '\n' { 1140 e.InsertLineBelow() 1141 e.Down(c, nil) 1142 continue 1143 } 1144 e.InsertRune(c, r) 1145 e.WriteRune(c) 1146 e.Next(c) 1147 } 1148} 1149 1150// InsertString will insert a string without newlines at the current data position. 1151// his will also call e.WriteRune and e.Next, as needed. 1152func (e *Editor) InsertString(c *vt100.Canvas, s string) { 1153 for _, r := range s { 1154 e.InsertRune(c, r) 1155 e.WriteRune(c) 1156 e.Next(c) 1157 } 1158} 1159 1160// Rune will get the rune at the current data position 1161func (e *Editor) Rune() rune { 1162 x, err := e.DataX() 1163 if err != nil { 1164 // after line contents, return a zero rune 1165 return rune(0) 1166 } 1167 return e.Get(x, e.DataY()) 1168} 1169 1170// LeftRune will get the rune to the left of the current data position 1171func (e *Editor) LeftRune() rune { 1172 y := e.DataY() 1173 x, err := e.DataX() 1174 if err != nil { 1175 // This is after the line contents, return the last rune 1176 runes, ok := e.lines[int(y)] 1177 if !ok || len(runes) == 0 { 1178 return rune(0) 1179 } 1180 // Return the last rune 1181 return runes[len(runes)-1] 1182 } 1183 if x <= 0 { 1184 // Nothing to the left of this 1185 return rune(0) 1186 } 1187 // Return the rune to the left 1188 return e.Get(x-1, e.DataY()) 1189} 1190 1191// CurrentLine will get the current data line, as a string 1192func (e *Editor) CurrentLine() string { 1193 return e.Line(e.DataY()) 1194} 1195 1196// Home will move the cursor the the start of the line (x = 0) 1197// And also scroll all the way to the left. 1198func (e *Editor) Home() { 1199 e.pos.sx = 0 1200 e.pos.offsetX = 0 1201 e.redraw = true 1202} 1203 1204// End will move the cursor to the position right after the end of the current line contents, 1205// and also trim away whitespace from the right side. 1206func (e *Editor) End(c *vt100.Canvas) { 1207 y := e.DataY() 1208 e.TrimRight(y) 1209 x := e.LastTextPosition(y) + 1 1210 e.pos.SetX(c, x) 1211 e.redraw = true 1212} 1213 1214// EndNoTrim will move the cursor to the position right after the end of the current line contents 1215func (e *Editor) EndNoTrim(c *vt100.Canvas) { 1216 x := e.LastTextPosition(e.DataY()) + 1 1217 e.pos.SetX(c, x) 1218 e.redraw = true 1219} 1220 1221// AtEndOfLine returns true if the cursor is at exactly the last character of the line, not the one after 1222func (e *Editor) AtEndOfLine() bool { 1223 return e.pos.sx+e.pos.offsetX == e.LastTextPosition(e.DataY()) 1224} 1225 1226// DownEnd will move down and then choose a "smart" X position 1227func (e *Editor) DownEnd(c *vt100.Canvas) error { 1228 tmpx := e.pos.sx 1229 err := e.pos.Down(c) 1230 if err != nil { 1231 return err 1232 } 1233 line := e.CurrentLine() 1234 if len(strings.TrimSpace(line)) == 1 { 1235 e.TrimRight(e.DataY()) 1236 e.End(c) 1237 } else if e.AfterLineScreenContentsPlusOne() && tmpx > 1 { 1238 e.End(c) 1239 if e.pos.sx != tmpx && e.pos.sx > e.pos.savedX { 1240 e.pos.savedX = tmpx 1241 } 1242 } else { 1243 e.pos.sx = e.pos.savedX 1244 1245 if e.pos.sx < 0 { 1246 e.pos.sx = 0 1247 } 1248 if e.AfterLineScreenContentsPlusOne() { 1249 e.End(c) 1250 } 1251 1252 // Also checking if e.Rune() is ' ' is nice for code, but horrible for regular text files 1253 if e.Rune() == '\t' { 1254 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1255 } 1256 1257 // 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") 1258 expandedRunes := []rune(strings.Replace(line, "\t", strings.Repeat("\t", e.tabsSpaces.PerTab), -1)) 1259 if e.pos.sx < len(expandedRunes) && expandedRunes[e.pos.sx] == '\t' { 1260 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1261 } 1262 } 1263 return nil 1264} 1265 1266// UpEnd will move up and then choose a "smart" X position 1267func (e *Editor) UpEnd(c *vt100.Canvas) error { 1268 tmpx := e.pos.sx 1269 err := e.pos.Up() 1270 if err != nil { 1271 return err 1272 } 1273 if e.AfterLineScreenContentsPlusOne() && tmpx > 1 { 1274 e.End(c) 1275 if e.pos.sx != tmpx && e.pos.sx > e.pos.savedX { 1276 e.pos.savedX = tmpx 1277 } 1278 } else { 1279 e.pos.sx = e.pos.savedX 1280 1281 if e.pos.sx < 0 { 1282 e.pos.sx = 0 1283 } 1284 if e.AfterLineScreenContentsPlusOne() { 1285 e.End(c) 1286 } 1287 1288 // Also checking if e.Rune() is ' ' is nice for code, but horrible for regular text files 1289 if e.Rune() == '\t' { 1290 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1291 } 1292 1293 // 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") 1294 expandedRunes := []rune(strings.Replace(e.CurrentLine(), "\t", strings.Repeat("\t", e.tabsSpaces.PerTab), -1)) 1295 if e.pos.sx < len(expandedRunes) && expandedRunes[e.pos.sx] == '\t' { 1296 e.pos.sx = int(e.FirstScreenPosition(e.DataY())) 1297 } 1298 } 1299 return nil 1300} 1301 1302// Next will move the cursor to the next position in the contents 1303func (e *Editor) Next(c *vt100.Canvas) error { 1304 // Ignore it if the position is out of bounds 1305 atTab := e.Rune() == '\t' 1306 if atTab { 1307 e.pos.sx += e.tabsSpaces.PerTab 1308 } else { 1309 e.pos.sx++ 1310 } 1311 // Did we move too far on this line? 1312 if e.AfterLineScreenContentsPlusOne() { 1313 // Undo the move 1314 if atTab { 1315 e.pos.sx -= e.tabsSpaces.PerTab 1316 } else { 1317 e.pos.sx-- 1318 } 1319 // Move down 1320 err := e.pos.Down(c) 1321 if err != nil { 1322 return err 1323 } 1324 // Move to the start of the line 1325 e.pos.sx = 0 1326 } 1327 return nil 1328} 1329 1330// LeftRune2 returns the rune to the left of the current position, or an error 1331func (e *Editor) LeftRune2() (rune, error) { 1332 x, err := e.DataX() 1333 if err != nil { 1334 return rune(0), err 1335 } 1336 x-- 1337 if x <= 0 { 1338 return rune(0), errors.New("no runes to the left") 1339 } 1340 return e.Get(x, e.DataY()), nil 1341} 1342 1343// TabToTheLeft returns true if there is a '\t' to the left of the current position 1344func (e *Editor) TabToTheLeft() bool { 1345 r, err := e.LeftRune2() 1346 if err != nil { 1347 return false 1348 } 1349 return r == '\t' 1350} 1351 1352// Prev will move the cursor to the previous position in the contents 1353func (e *Editor) Prev(c *vt100.Canvas) error { 1354 1355 atTab := e.TabToTheLeft() || (e.pos.sx <= e.tabsSpaces.PerTab && e.Get(0, e.DataY()) == '\t') 1356 if e.pos.sx == 0 && e.pos.offsetX > 0 { 1357 // at left edge, but can scroll to the left 1358 e.pos.offsetX-- 1359 e.redraw = true 1360 } else { 1361 // If at a tab character, move a few more positions 1362 if atTab { 1363 e.pos.sx -= e.tabsSpaces.PerTab 1364 } else { 1365 e.pos.sx-- 1366 } 1367 } 1368 if e.pos.sx < 0 { // Did we move too far and there is no X offset? 1369 // Undo the move 1370 if atTab { 1371 e.pos.sx += e.tabsSpaces.PerTab 1372 } else { 1373 e.pos.sx++ 1374 } 1375 // Move up, and to the end of the line above, if in EOL mode 1376 err := e.pos.Up() 1377 if err != nil { 1378 return err 1379 } 1380 e.End(c) 1381 } 1382 return nil 1383} 1384 1385// Right will move the cursor to the right, if possible. 1386// It will not move the cursor up or down. 1387func (p *Position) Right(c *vt100.Canvas) { 1388 w := 80 // default width 1389 if c != nil { 1390 w = int(c.Width()) 1391 } 1392 if p.sx < (w - 1) { 1393 p.sx++ 1394 } else { 1395 p.sx = 0 1396 p.offsetX += (w - 1) 1397 } 1398} 1399 1400// Left will move the cursor to the left, if possible. 1401// It will not move the cursor up or down. 1402func (p *Position) Left() { 1403 if p.sx > 0 { 1404 p.sx-- 1405 } 1406} 1407 1408// SaveX will save the current X position, if it's within reason 1409func (e *Editor) SaveX(regardless bool) { 1410 if regardless || (!e.AfterLineScreenContentsPlusOne() && e.pos.sx > 1) { 1411 e.pos.savedX = e.pos.sx 1412 } 1413} 1414 1415// ScrollDown will scroll down the given amount of lines given in scrollSpeed 1416func (e *Editor) ScrollDown(c *vt100.Canvas, status *StatusBar, scrollSpeed int) bool { 1417 // Find out if we can scroll scrollSpeed, or less 1418 canScroll := scrollSpeed 1419 1420 // Last y position in the canvas 1421 canvasLastY := int(c.H() - 1) 1422 1423 // Retrieve the current editor scroll offset offset 1424 mut.RLock() 1425 offset := e.pos.offsetY 1426 mut.RUnlock() 1427 1428 // Number of lines in the document 1429 l := e.Len() 1430 1431 if offset >= l-canvasLastY { 1432 c.Draw() 1433 // Don't redraw 1434 return false 1435 } 1436 if status != nil { 1437 status.Clear(c) 1438 } 1439 if (offset + canScroll) >= (l - canvasLastY) { 1440 // Almost at the bottom, we can scroll the remaining lines 1441 canScroll = (l - canvasLastY) - offset 1442 } 1443 1444 // Move the scroll offset 1445 mut.Lock() 1446 e.pos.offsetX = 0 1447 e.pos.offsetY += canScroll 1448 mut.Unlock() 1449 1450 // Prepare to redraw 1451 return true 1452} 1453 1454// ScrollUp will scroll down the given amount of lines given in scrollSpeed 1455func (e *Editor) ScrollUp(c *vt100.Canvas, status *StatusBar, scrollSpeed int) bool { 1456 // Find out if we can scroll scrollSpeed, or less 1457 canScroll := scrollSpeed 1458 1459 // Retrieve the current editor scroll offset offset 1460 mut.RLock() 1461 offset := e.pos.offsetY 1462 mut.RUnlock() 1463 1464 if offset == 0 { 1465 // Can't scroll further up 1466 // Status message 1467 //status.SetMessage("Start of text") 1468 //status.Show(c, p) 1469 //c.Draw() 1470 // Redraw 1471 return true 1472 } 1473 if status != nil { 1474 status.Clear(c) 1475 } 1476 if offset-canScroll < 0 { 1477 // Almost at the top, we can scroll the remaining lines 1478 canScroll = offset 1479 } 1480 // Move the scroll offset 1481 mut.Lock() 1482 e.pos.offsetX = 0 1483 e.pos.offsetY -= canScroll 1484 mut.Unlock() 1485 // Prepare to redraw 1486 return true 1487} 1488 1489// AtFirstLineOfDocument is true if we're at the first line of the document 1490func (e *Editor) AtFirstLineOfDocument() bool { 1491 return e.DataY() == LineIndex(0) 1492} 1493 1494// AtLastLineOfDocument is true if we're at the last line of the document 1495func (e *Editor) AtLastLineOfDocument() bool { 1496 return e.DataY() == LineIndex(e.Len()-1) 1497} 1498 1499// AfterLastLineOfDocument is true if we're after the last line of the document 1500func (e *Editor) AfterLastLineOfDocument() bool { 1501 return e.DataY() > LineIndex(e.Len()-1) 1502} 1503 1504// AtOrAfterLastLineOfDocument is true if we're at or after the last line of the document 1505func (e *Editor) AtOrAfterLastLineOfDocument() bool { 1506 return e.DataY() >= LineIndex(e.Len()-1) 1507} 1508 1509// AtOrAfterEndOfDocument is true if the cursor is at or after the end of the last line of the document 1510func (e *Editor) AtOrAfterEndOfDocument() bool { 1511 return (e.AtLastLineOfDocument() && e.AtOrAfterEndOfLine()) || e.AfterLastLineOfDocument() 1512} 1513 1514// AfterEndOfDocument is true if the cursor is after the end of the last line of the document 1515func (e *Editor) AfterEndOfDocument() bool { 1516 return e.AfterLastLineOfDocument() // && e.AtOrAfterEndOfLine() 1517} 1518 1519// AtEndOfDocument is true if the cursor is at the end of the last line of the document 1520func (e *Editor) AtEndOfDocument() bool { 1521 return e.AtLastLineOfDocument() && e.AtEndOfLine() 1522} 1523 1524// AtStartOfDocument is true if we're at the first line of the document 1525func (e *Editor) AtStartOfDocument() bool { 1526 return e.pos.sy == 0 && e.pos.offsetY == 0 1527} 1528 1529// AtStartOfScreenLine is true if the cursor is a the start of the screen line. 1530// The line may be scrolled all the way to the end, and the cursor moved to the left of the screen, for instance. 1531func (e *Editor) AtStartOfScreenLine() bool { 1532 return e.pos.AtStartOfScreenLine() 1533} 1534 1535// AtStartOfTheLine is true if the cursor is a the start of the screen line, and the line is not scrolled. 1536func (e *Editor) AtStartOfTheLine() bool { 1537 return e.pos.AtStartOfTheLine() 1538} 1539 1540// AtLeftEdgeOfDocument is true if we're at the first column at the document. Same as AtStarOfTheLine. 1541func (e *Editor) AtLeftEdgeOfDocument() bool { 1542 return e.pos.sx == 0 && e.pos.offsetX == 0 1543} 1544 1545// AtOrAfterEndOfLine returns true if the cursor is at or after the contents of this line 1546func (e *Editor) AtOrAfterEndOfLine() bool { 1547 if e.EmptyLine() { 1548 return true 1549 } 1550 x, err := e.DataX() 1551 if err != nil { 1552 // After end of data 1553 return true 1554 } 1555 return x >= e.LastDataPosition(e.DataY()) 1556} 1557 1558// AfterEndOfLine returns true if the cursor is after the contents of this line 1559func (e *Editor) AfterEndOfLine() bool { 1560 if e.EmptyLine() { 1561 return true 1562 } 1563 x, err := e.DataX() 1564 if err != nil { 1565 // After end of data 1566 return true 1567 } 1568 return x > e.LastDataPosition(e.DataY()) 1569} 1570 1571// AfterLineScreenContents will check if the cursor is after the current line contents 1572func (e *Editor) AfterLineScreenContents() bool { 1573 return e.pos.sx > e.LastScreenPosition(e.DataY()) 1574} 1575 1576// AfterScreenWidth checks if the current cursor position has moved after the terminal/canvas width 1577func (e *Editor) AfterScreenWidth(c *vt100.Canvas) bool { 1578 w := 80 // default width 1579 if c != nil { 1580 w = int(c.W()) 1581 } 1582 return e.pos.sx >= w 1583} 1584 1585// AfterLineScreenContentsPlusOne will check if the cursor is after the current line contents, with a margin of 1 1586func (e *Editor) AfterLineScreenContentsPlusOne() bool { 1587 return e.pos.sx > (e.LastScreenPosition(e.DataY()) + 1) 1588} 1589 1590// WriteRune writes the current rune to the given canvas 1591func (e *Editor) WriteRune(c *vt100.Canvas) { 1592 if c != nil { 1593 c.WriteRune(uint(e.pos.sx+e.pos.offsetX), uint(e.pos.sy), e.Foreground, e.Background, e.Rune()) 1594 } 1595} 1596 1597// WriteTab writes spaces when there is a tab character, to the canvas 1598func (e *Editor) WriteTab(c *vt100.Canvas) { 1599 spacesPerTab := e.tabsSpaces.PerTab 1600 for x := e.pos.sx; x < e.pos.sx+spacesPerTab; x++ { 1601 c.WriteRune(uint(x+e.pos.offsetX), uint(e.pos.sy), e.Foreground, e.Background, ' ') 1602 } 1603} 1604 1605// EmptyRightTrimmedLine checks if the current line is empty (and whitespace doesn't count) 1606func (e *Editor) EmptyRightTrimmedLine() bool { 1607 return len(strings.TrimRightFunc(e.CurrentLine(), unicode.IsSpace)) == 0 1608} 1609 1610// EmptyRightTrimmedLineBelow checks if the next line is empty (and whitespace doesn't count) 1611func (e *Editor) EmptyRightTrimmedLineBelow() bool { 1612 return len(strings.TrimRightFunc(e.Line(e.DataY()+1), unicode.IsSpace)) == 0 1613} 1614 1615// EmptyLine returns true if the current line is completely empty, no whitespace or anything 1616func (e *Editor) EmptyLine() bool { 1617 return len(e.CurrentLine()) == 0 1618} 1619 1620// EmptyTrimmedLine returns true if the current line (trimmed) is completely empty 1621func (e *Editor) EmptyTrimmedLine() bool { 1622 return len(e.TrimmedLine()) == 0 1623} 1624 1625// AtStartOfTextScreenLine returns true if the position is at the start of the text for this screen line 1626func (e *Editor) AtStartOfTextScreenLine() bool { 1627 return uint(e.pos.sx) == e.FirstScreenPosition(e.DataY()) 1628} 1629 1630// BeforeStartOfTextScreenLine returns true if the position is before the start of the text for this screen line 1631func (e *Editor) BeforeStartOfTextScreenLine() bool { 1632 return uint(e.pos.sx) < e.FirstScreenPosition(e.DataY()) 1633} 1634 1635// AtOrBeforeStartOfTextScreenLine returns true if the position is before or at the start of the text for this screen line 1636func (e *Editor) AtOrBeforeStartOfTextScreenLine() bool { 1637 return uint(e.pos.sx) <= e.FirstScreenPosition(e.DataY()) 1638} 1639 1640// GoTo will go to a given line index, counting from 0 1641// Returns true if the editor should be redrawn 1642// status is used for clearing status bar messages and can be nil 1643func (e *Editor) GoTo(dataY LineIndex, c *vt100.Canvas, status *StatusBar) bool { 1644 if dataY == e.DataY() { 1645 // Already at the correct line, but still trigger a redraw 1646 return true 1647 } 1648 reachedEnd := false 1649 // Out of bounds checking for y 1650 if dataY < 0 { 1651 dataY = 0 1652 } else if dataY >= LineIndex(e.Len()) { 1653 dataY = LineIndex(e.Len() - 1) 1654 reachedEnd = true 1655 } 1656 1657 h := 25 1658 if c != nil { 1659 // Get the current terminal height 1660 h = int(c.Height()) 1661 } 1662 1663 // Is the place we want to go within the current scroll window? 1664 topY := LineIndex(e.pos.offsetY) 1665 botY := LineIndex(e.pos.offsetY + h) 1666 1667 if dataY >= topY && dataY < botY { 1668 // No scrolling is needed, just move the screen y position 1669 e.pos.sy = int(dataY) - e.pos.offsetY 1670 if e.pos.sy < 0 { 1671 e.pos.sy = 0 1672 } 1673 } else if int(dataY) < h { 1674 // No scrolling is needed, just move the screen y position 1675 e.pos.offsetY = 0 1676 e.pos.sy = int(dataY) 1677 if e.pos.sy < 0 { 1678 e.pos.sy = 0 1679 } 1680 } else if reachedEnd { 1681 // To the end of the text 1682 e.pos.offsetY = e.Len() - h 1683 e.pos.sy = h - 1 1684 } else { 1685 prevY := e.pos.sy 1686 // Scrolling is needed 1687 e.pos.sy = 0 1688 e.pos.offsetY = int(dataY) 1689 lessJumpY := prevY 1690 lessJumpOffset := int(dataY) - prevY 1691 if (lessJumpY + lessJumpOffset) < e.Len() { 1692 e.pos.sy = lessJumpY 1693 e.pos.offsetY = lessJumpOffset 1694 } 1695 } 1696 1697 // The Y scrolling is done, move the X position according to the contents of the line 1698 e.pos.SetX(c, int(e.FirstScreenPosition(e.DataY()))) 1699 1700 // Clear all status messages 1701 if status != nil { 1702 status.ClearAll(c) 1703 } 1704 1705 // Trigger cursor redraw 1706 e.redrawCursor = true 1707 1708 // Should also redraw the text 1709 return true 1710} 1711 1712// GoToLineNumber will go to a given line number, but counting from 1, not from 0! 1713func (e *Editor) GoToLineNumber(lineNumber LineNumber, c *vt100.Canvas, status *StatusBar, center bool) bool { 1714 if lineNumber < 1 { 1715 lineNumber = 1 1716 } 1717 redraw := e.GoTo(lineNumber.LineIndex(), c, status) 1718 if redraw && center { 1719 e.Center(c) 1720 } 1721 return redraw 1722} 1723 1724// GoToLineNumberAndCol will go to a given line number and column number, but counting from 1, not from 0! 1725func (e *Editor) GoToLineNumberAndCol(lineNumber LineNumber, colNumber ColNumber, c *vt100.Canvas, status *StatusBar, center bool) bool { 1726 if colNumber < 1 { 1727 colNumber = 1 1728 } 1729 if lineNumber < 1 { 1730 lineNumber = 1 1731 } 1732 xIndex := colNumber.ColIndex() 1733 yIndex := lineNumber.LineIndex() 1734 1735 // Go to the correct line 1736 redraw := e.GoTo(yIndex, c, status) 1737 1738 // Go to the correct column as well 1739 tabs := strings.Count(e.Line(yIndex), "\t") 1740 newScreenX := int(xIndex) + (tabs * (e.tabsSpaces.PerTab - 1)) 1741 if e.pos.sx != newScreenX { 1742 redraw = true 1743 } 1744 e.pos.sx = newScreenX 1745 1746 if redraw && center { 1747 e.Center(c) 1748 } 1749 return redraw 1750} 1751 1752// Up tried to move the cursor up, and also scroll 1753func (e *Editor) Up(c *vt100.Canvas, status *StatusBar) { 1754 e.GoTo(e.DataY()-1, c, status) 1755} 1756 1757// Down tries to move the cursor down, and also scroll 1758// status is used for clearing status bar messages and can be nil 1759func (e *Editor) Down(c *vt100.Canvas, status *StatusBar) { 1760 e.GoTo(e.DataY()+1, c, status) 1761} 1762 1763// LeadingWhitespace returns the leading whitespace for this line 1764func (e *Editor) LeadingWhitespace() string { 1765 return e.CurrentLine()[:e.FirstDataPosition(e.DataY())] 1766} 1767 1768// LeadingWhitespaceAt returns the leading whitespace for a given line index 1769func (e *Editor) LeadingWhitespaceAt(y LineIndex) string { 1770 return e.Line(y)[:e.FirstDataPosition(y)] 1771} 1772 1773// LineNumber will return the current line number (data y index + 1) 1774func (e *Editor) LineNumber() LineNumber { 1775 return LineNumber(e.DataY() + 1) 1776} 1777 1778// LineIndex will return the current line index (data y index) 1779func (e *Editor) LineIndex() LineIndex { 1780 return e.DataY() 1781} 1782 1783// ColNumber will return the current column number (data x index + 1) 1784func (e *Editor) ColNumber() ColNumber { 1785 x, _ := e.DataX() 1786 return ColNumber(x + 1) 1787} 1788 1789// ColIndex will return the current column index (data x index) 1790func (e *Editor) ColIndex() ColIndex { 1791 x, _ := e.DataX() 1792 return ColIndex(x) 1793} 1794 1795// StatusMessage returns a status message, intended for being displayed at the bottom 1796func (e *Editor) StatusMessage() string { 1797 return fmt.Sprintf("line %d col %d rune %U words %d [%s]", e.LineNumber(), e.ColNumber(), e.Rune(), e.WordCount(), e.mode) 1798} 1799 1800// GoToPosition can go to the given position struct and use it as the new position 1801func (e *Editor) GoToPosition(c *vt100.Canvas, status *StatusBar, pos Position) { 1802 e.pos = pos 1803 e.redraw = e.GoTo(e.DataY(), c, status) 1804 e.redrawCursor = true 1805} 1806 1807// GoToStartOfTextLine will go to the start of the non-whitespace text, for this line 1808func (e *Editor) GoToStartOfTextLine(c *vt100.Canvas) { 1809 e.pos.SetX(c, int(e.FirstScreenPosition(e.DataY()))) 1810 e.redraw = true 1811} 1812 1813// GoToNextParagraph will jump to the next line that has a blank line above it, if possible 1814// Returns true if the editor should be redrawn 1815func (e *Editor) GoToNextParagraph(c *vt100.Canvas, status *StatusBar) bool { 1816 var lastFoundBlankLine LineIndex = -1 1817 l := e.Len() 1818 for i := e.DataY() + 1; i < LineIndex(l); i++ { 1819 // Check if this is a blank line 1820 if len(strings.TrimSpace(e.Line(i))) == 0 { 1821 lastFoundBlankLine = i 1822 } else { 1823 // This is a non-blank line, check if the line above is blank (or before the first line) 1824 if lastFoundBlankLine == (i - 1) { 1825 // Yes, this is the line we wish to jump to 1826 return e.GoTo(i, c, status) 1827 } 1828 } 1829 } 1830 return false 1831} 1832 1833// GoToPrevParagraph will jump to the previous line that has a blank line below it, if possible 1834// Returns true if the editor should be redrawn 1835func (e *Editor) GoToPrevParagraph(c *vt100.Canvas, status *StatusBar) bool { 1836 var lastFoundBlankLine = LineIndex(e.Len()) 1837 for i := e.DataY() - 1; i >= 0; i-- { 1838 // Check if this is a blank line 1839 if len(strings.TrimSpace(e.Line(i))) == 0 { 1840 lastFoundBlankLine = i 1841 } else { 1842 // This is a non-blank line, check if the line below is blank (or after the last line) 1843 if lastFoundBlankLine == (i + 1) { 1844 // Yes, this is the line we wish to jump to 1845 return e.GoTo(i, c, status) 1846 } 1847 } 1848 } 1849 return false 1850} 1851 1852// Center will scroll the contents so that the line with the cursor ends up in the center of the screen 1853func (e *Editor) Center(c *vt100.Canvas) { 1854 // Find the terminal height 1855 h := 25 1856 if c != nil { 1857 h = int(c.Height()) 1858 } 1859 1860 // General information about how the positions and offsets relate: 1861 // 1862 // offset + screen y = data y 1863 // 1864 // offset = e.pos.offset 1865 // screen y = e.pos.sy 1866 // data y = e.DataY() 1867 // 1868 // offset = data y - screen y 1869 1870 // Plan: 1871 // 1. offset = data y - (h / 2) 1872 // 2. screen y = data y - offset 1873 1874 // Find the center line 1875 centerY := h / 2 1876 y := int(e.DataY()) 1877 if y < centerY { 1878 // Not enough room to adjust 1879 return 1880 } 1881 1882 // Find the new offset and y position 1883 newOffset := y - centerY 1884 newScreenY := y - newOffset 1885 1886 // Assign the new values to the editor 1887 e.pos.offsetY = newOffset 1888 e.pos.sy = newScreenY 1889} 1890 1891// CommentOn will insert a comment marker (like # or //) in front of a line 1892func (e *Editor) CommentOn(commentMarker string) { 1893 e.SetCurrentLine(commentMarker + " " + e.CurrentLine()) 1894} 1895 1896// CommentOff will remove "//" or "// " from the front of the line if "//" is given 1897func (e *Editor) CommentOff(commentMarker string) { 1898 var ( 1899 changed bool 1900 newContents string 1901 contents = e.CurrentLine() 1902 trimContents = strings.TrimSpace(contents) 1903 ) 1904 commentMarkerPlusSpace := commentMarker + " " 1905 if strings.HasPrefix(trimContents, commentMarkerPlusSpace) { 1906 // toggle off comment 1907 newContents = strings.Replace(contents, commentMarkerPlusSpace, "", 1) 1908 changed = true 1909 } else if strings.HasPrefix(trimContents, commentMarker) { 1910 // toggle off comment 1911 newContents = strings.Replace(contents, commentMarker, "", 1) 1912 changed = true 1913 } 1914 if changed { 1915 e.SetCurrentLine(newContents) 1916 // If the line was shortened and the cursor ended up after the line, move it 1917 if e.AfterEndOfLine() { 1918 e.End(nil) 1919 } 1920 } 1921} 1922 1923// CurrentLineCommented checks if the current trimmed line starts with "//", if "//" is given 1924func (e *Editor) CurrentLineCommented(commentMarker string) bool { 1925 return strings.HasPrefix(e.TrimmedLine(), commentMarker) 1926} 1927 1928// ForEachLineInBlock will move the cursor and run the given function for 1929// each line in the current block of text (until newline or end of document) 1930// Also takes a string that will be passed on to the function. 1931func (e *Editor) ForEachLineInBlock(c *vt100.Canvas, f func(string), commentMarker string) { 1932 downCounter := 0 1933 for !e.EmptyRightTrimmedLine() { 1934 f(commentMarker) 1935 if e.AtOrAfterEndOfDocument() { 1936 break 1937 } 1938 e.Down(c, nil) 1939 downCounter++ 1940 if downCounter > 100 { // safeguard 1941 break 1942 } 1943 } 1944 // Go up again 1945 for i := downCounter; i > 0; i-- { 1946 e.Up(c, nil) 1947 } 1948} 1949 1950// Block will return the text from the given line until 1951// either a newline or the end of the document. 1952func (e *Editor) Block(n LineIndex) string { 1953 var ( 1954 bb, lb strings.Builder // block string builder and line string builder 1955 line []rune 1956 ok bool 1957 s string 1958 ) 1959 for { 1960 line, ok = e.lines[int(n)] 1961 n++ 1962 if !ok || len(line) == 0 { 1963 // End of document, empty line or invalid line: end of block 1964 return bb.String() 1965 } 1966 lb.Reset() 1967 for _, r := range line { 1968 lb.WriteRune(r) 1969 } 1970 s = lb.String() 1971 if len(strings.TrimSpace(s)) == 0 { 1972 // Empty trimmed line, end of block 1973 return bb.String() 1974 } 1975 // Save this line to bb 1976 bb.WriteString(s) 1977 // And add a newline 1978 bb.Write([]byte{'\n'}) 1979 } 1980} 1981 1982// ToggleCommentBlock will toggle comments until a blank line or the end of the document is reached 1983// The amount of existing commented lines is considered before deciding to comment the block in or out 1984func (e *Editor) ToggleCommentBlock(c *vt100.Canvas) { 1985 // If most of the lines in the block are comments, comment it out 1986 // If most of the lines in the block are not comments, comment it in 1987 1988 var ( 1989 downCounter = 0 1990 commentCounter = 0 1991 commentMarker = e.SingleLineCommentMarker() 1992 ) 1993 1994 // Count the commented lines in this block while going down 1995 for !e.EmptyRightTrimmedLine() { 1996 if e.CurrentLineCommented(commentMarker) { 1997 commentCounter++ 1998 } 1999 if e.AtOrAfterEndOfDocument() { 2000 break 2001 } 2002 e.Down(c, nil) 2003 downCounter++ 2004 if downCounter > 100 { // safeguard 2005 break 2006 } 2007 } 2008 // Go up again 2009 for i := downCounter; i > 0; i-- { 2010 e.Up(c, nil) 2011 } 2012 2013 // Check if most lines are commented out 2014 mostLinesAreComments := commentCounter >= (downCounter / 2) 2015 2016 // Handle the single-line case differently 2017 if downCounter == 1 && commentCounter == 0 { 2018 e.CommentOn(commentMarker) 2019 } else if downCounter == 1 && commentCounter == 1 { 2020 e.CommentOff(commentMarker) 2021 } else if mostLinesAreComments { 2022 e.ForEachLineInBlock(c, e.CommentOff, commentMarker) 2023 } else { 2024 e.ForEachLineInBlock(c, e.CommentOn, commentMarker) 2025 } 2026} 2027 2028// NewLine inserts a new line below and moves down one step 2029func (e *Editor) NewLine(c *vt100.Canvas, status *StatusBar) { 2030 e.InsertLineBelow() 2031 e.Down(c, status) 2032} 2033 2034// ChopLine takes a string where the tabs have been expanded 2035// and scrolls it + chops it up for display in the current viewport. 2036// e.pos.offsetX and the given viewportWidth are respected. 2037func (e *Editor) ChopLine(line string, viewportWidth int) string { 2038 var screenLine string 2039 // Shorten the screen line to account for the X offset 2040 if len([]rune(line)) > e.pos.offsetX { 2041 screenLine = line[e.pos.offsetX:] 2042 } 2043 // Shorten the screen line to account for the terminal width 2044 if len(string(screenLine)) >= viewportWidth { 2045 screenLine = screenLine[:viewportWidth] 2046 } 2047 return screenLine 2048} 2049 2050// HorizontalScrollIfNeeded will scroll along the X axis, if needed 2051func (e *Editor) HorizontalScrollIfNeeded(c *vt100.Canvas) { 2052 x := e.pos.sx 2053 w := 80 2054 if c != nil { 2055 w = int(c.W()) 2056 } 2057 if x < w { 2058 e.pos.offsetX = 0 2059 } else { 2060 e.pos.offsetX = (x - w) + 1 2061 e.pos.sx -= e.pos.offsetX 2062 } 2063 e.redraw = true 2064 e.redrawCursor = true 2065} 2066 2067// VerticalScrollIfNeeded will scroll along the X axis, if needed 2068func (e *Editor) VerticalScrollIfNeeded(c *vt100.Canvas, status *StatusBar) { 2069 y := e.pos.sy 2070 h := 25 2071 if c != nil { 2072 h = int(c.H()) 2073 } 2074 if y < h { 2075 e.pos.offsetY = 0 2076 } else { 2077 e.pos.offsetY = (y - h) + 1 2078 e.pos.sy -= e.pos.offsetY 2079 } 2080 e.redraw = true 2081 e.redrawCursor = true 2082} 2083 2084// InsertFile inserts the contents of a file at the current location 2085func (e *Editor) InsertFile(c *vt100.Canvas, filename string) error { 2086 data, err := ioutil.ReadFile(filename) 2087 if err != nil { 2088 return err 2089 } 2090 s := opinionatedStringReplacer.Replace(strings.TrimRightFunc(string(data), unicode.IsSpace)) 2091 e.InsertStringAndMove(c, s) 2092 return nil 2093} 2094 2095// AbsFilename returns the absolute filename for this editor, 2096// cleaned with filepath.Clean. 2097func (e *Editor) AbsFilename() (string, error) { 2098 absFilename, err := filepath.Abs(e.filename) 2099 if err != nil { 2100 return "", err 2101 } 2102 return filepath.Clean(absFilename), nil 2103} 2104 2105// Switch replaces the current editor with a new Editor that opens the given file. 2106// The undo stack is also swapped. 2107// Only works for switching to one file, and then back again. 2108func (e *Editor) Switch(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, lk *LockKeeper, filenameToOpen string, forceOpen bool) error { 2109 absFilename, err := e.AbsFilename() 2110 if err != nil { 2111 return err 2112 } 2113 // Unlock and save the lock file 2114 lk.Unlock(absFilename) 2115 lk.Save() 2116 // Now open the header filename instead of the current file. Save the current file first. 2117 e.Save(c, tty) 2118 // Save the current location in the location history and write it to file 2119 e.SaveLocation(absFilename, e.locationHistory) 2120 2121 var ( 2122 e2 *Editor 2123 statusMessage string 2124 ) 2125 2126 if switchBuffer.Len() == 1 { 2127 // Load the Editor from the switchBuffer if switchBuffer has length 1, then use that editor. 2128 switchBuffer.Restore(e) 2129 undo, switchUndoBackup = switchUndoBackup, undo 2130 } else { 2131 e2, statusMessage, err = NewEditor(tty, c, filenameToOpen, LineNumber(0), ColNumber(0), e.Theme, e.syntaxHighlight) 2132 if err == nil { // no issue 2133 // Save the current Editor to the switchBuffer if switchBuffer if empty, then use the new editor. 2134 switchBuffer.Snapshot(e) 2135 2136 // Now use e2 as the current editor 2137 *e = *e2 2138 (*e).lines = (*e2).lines 2139 (*e).pos = (*e2).pos 2140 2141 } else { 2142 panic(err) 2143 } 2144 undo, switchUndoBackup = switchUndoBackup, undo 2145 } 2146 2147 e.redraw = true 2148 e.redrawCursor = true 2149 2150 if statusMessage != "" { 2151 status.Clear(c) 2152 status.SetMessage(statusMessage) 2153 status.Show(c, e) 2154 } 2155 2156 return err 2157} 2158 2159// TrimmedLine returns the current line, trimmed in both ends 2160func (e *Editor) TrimmedLine() string { 2161 return strings.TrimSpace(e.CurrentLine()) 2162} 2163 2164// LineContentsFromCursorPosition returns the rest of the line, 2165// from the current cursor position, trimmed. 2166func (e *Editor) LineContentsFromCursorPosition() string { 2167 x, err := e.DataX() 2168 if err != nil { 2169 return "" 2170 } 2171 return strings.TrimSpace(e.CurrentLine()[x:]) 2172} 2173 2174// LettersBeforeCursor returns the current word up until the cursor (for autocompletion) 2175func (e *Editor) LettersBeforeCursor() string { 2176 y := int(e.DataY()) 2177 runes, ok := e.lines[y] 2178 if !ok { 2179 // This should never happen 2180 return "" 2181 } 2182 // Either find x or use the last index of the line 2183 x, err := e.DataX() 2184 if err != nil { 2185 x = len(runes) 2186 } 2187 2188 var word []rune 2189 2190 // Loop from the position before the current one and then leftwards on the current line 2191 for i := x - 1; i >= 0; i-- { 2192 r := runes[i] 2193 if !unicode.IsLetter(r) { 2194 break 2195 } 2196 // Gather the letters in reverse 2197 word = append([]rune{r}, word...) 2198 } 2199 return string(word) 2200} 2201 2202// LettersOrDotBeforeCursor returns the current word up until the cursor (for autocompletion). 2203// Will also include ".". 2204func (e *Editor) LettersOrDotBeforeCursor() string { 2205 y := int(e.DataY()) 2206 runes, ok := e.lines[y] 2207 if !ok { 2208 // This should never happen 2209 return "" 2210 } 2211 // Either find x or use the last index of the line 2212 x, err := e.DataX() 2213 if err != nil { 2214 x = len(runes) 2215 } 2216 2217 var word []rune 2218 2219 // Loop from the position before the current one and then leftwards on the current line 2220 for i := x - 1; i >= 0; i-- { 2221 r := runes[i] 2222 if !(r == '.' || unicode.IsLetter(r)) { 2223 break 2224 } 2225 // Gather the letters in reverse 2226 word = append([]rune{r}, word...) 2227 } 2228 return string(word) 2229} 2230 2231// LastLineNumber returns the last line number (not line index) of the current file 2232func (e *Editor) LastLineNumber() LineNumber { 2233 // The last line (by line number, not by index, e.Len() returns an index which is why there is no -1) 2234 return LineNumber(e.Len()) 2235} 2236