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  }