github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/keyloop.go (about)

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