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