644 lines
20 KiB
Go
Executable File
644 lines
20 KiB
Go
Executable File
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/fivemoreminix/qedit/pkg/buffer"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/mattn/go-runewidth"
|
|
)
|
|
|
|
// TextEdit is a field for line-based editing. It features syntax highlighting
|
|
// tools, is autocomplete ready, and contains the various information about
|
|
// content being edited.
|
|
type TextEdit struct {
|
|
Buffer buffer.Buffer
|
|
Highlighter *buffer.Highlighter
|
|
LineNumbers bool // Whether to render line numbers (and therefore the column)
|
|
Dirty bool // Whether the buffer has been edited
|
|
UseHardTabs bool // When true, tabs are '\t'
|
|
TabSize int // How many spaces to indent by
|
|
IsCRLF bool // Whether the file's line endings are CRLF (\r\n) or LF (\n)
|
|
FilePath string // Will be empty if the file has not been saved yet
|
|
|
|
screen *tcell.Screen // We keep our own reference to the screen for cursor purposes.
|
|
cursor buffer.Cursor
|
|
scrollx, scrolly int // X and Y offset of view, known as scroll
|
|
theme *Theme
|
|
|
|
selection buffer.Region // Selection: selectMode determines if it should be used
|
|
selectMode bool // Whether the user is actively selecting text
|
|
|
|
baseComponent
|
|
}
|
|
|
|
// New will initialize the buffer using the given 'contents'. If the 'filePath' or 'FilePath' is empty,
|
|
// it can be assumed that the TextEdit has no file association, or it is unsaved.
|
|
func NewTextEdit(screen *tcell.Screen, filePath string, contents []byte, theme *Theme) *TextEdit {
|
|
te := &TextEdit{
|
|
Buffer: nil, // Set in SetContents
|
|
Highlighter: nil, // Set in SetContents
|
|
LineNumbers: true,
|
|
UseHardTabs: true,
|
|
TabSize: 4,
|
|
FilePath: filePath,
|
|
|
|
screen: screen,
|
|
baseComponent: baseComponent{theme: theme},
|
|
}
|
|
te.SetContents(contents)
|
|
return te
|
|
}
|
|
|
|
// SetContents applies the string to the internal buffer of the TextEdit component.
|
|
// The string is determined to be either CRLF or LF based on line-endings.
|
|
func (t *TextEdit) SetContents(contents []byte) {
|
|
var i int
|
|
loop:
|
|
for i < len(contents) {
|
|
switch contents[i] {
|
|
case '\n':
|
|
t.IsCRLF = false
|
|
break loop
|
|
case '\r':
|
|
// We could check for a \n after, but what's the point?
|
|
t.IsCRLF = true
|
|
break loop
|
|
}
|
|
_, size := utf8.DecodeRune(contents[i:])
|
|
i += size
|
|
}
|
|
|
|
t.Buffer = buffer.NewRopeBuffer(contents)
|
|
t.cursor = buffer.NewCursor(&t.Buffer)
|
|
t.Buffer.RegisterCursor(&t.cursor)
|
|
t.selection = buffer.NewRegion(&t.Buffer)
|
|
|
|
// TODO: replace with automatic determination of language via filetype
|
|
lang := &buffer.Language{
|
|
Name: "Go",
|
|
Filetypes: []string{".go"},
|
|
Rules: map[*buffer.RegexpRegion]buffer.Syntax{
|
|
&buffer.RegexpRegion{Start: regexp.MustCompile("\\/\\/.*")}: buffer.Comment,
|
|
&buffer.RegexpRegion{Start: regexp.MustCompile("\".*?\"")}: buffer.String,
|
|
&buffer.RegexpRegion{
|
|
Start: regexp.MustCompile("\\b(var|const|if|else|range|for|switch|fallthrough|case|default|break|continue|go|func|return|defer|import|type|package)\\b"),
|
|
}: buffer.Keyword,
|
|
&buffer.RegexpRegion{
|
|
Start: regexp.MustCompile("\\b(u?int(8|16|32|64)?|rune|byte|string|bool|struct)\\b"),
|
|
}: buffer.Type,
|
|
&buffer.RegexpRegion{
|
|
Start: regexp.MustCompile("\\b([1-9][0-9]*|0[0-7]*|0[Xx][0-9A-Fa-f]+|0[Bb][01]+)\\b"),
|
|
}: buffer.Number,
|
|
&buffer.RegexpRegion{
|
|
Start: regexp.MustCompile("\\b(len|cap|panic|make|copy|append)\\b"),
|
|
}: buffer.Builtin,
|
|
&buffer.RegexpRegion{
|
|
Start: regexp.MustCompile("\\b(nil|true|false)\\b"),
|
|
}: buffer.Special,
|
|
},
|
|
}
|
|
|
|
colorscheme := &buffer.Colorscheme{
|
|
buffer.Default: tcell.Style{}.Foreground(tcell.ColorLightGray).Background(tcell.ColorBlack),
|
|
buffer.Column: tcell.Style{}.Foreground(tcell.ColorDarkGray).Background(tcell.ColorBlack),
|
|
buffer.Comment: tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack),
|
|
buffer.String: tcell.Style{}.Foreground(tcell.ColorOlive).Background(tcell.ColorBlack),
|
|
buffer.Keyword: tcell.Style{}.Foreground(tcell.ColorNavy).Background(tcell.ColorBlack),
|
|
buffer.Type: tcell.Style{}.Foreground(tcell.ColorPurple).Background(tcell.ColorBlack),
|
|
buffer.Number: tcell.Style{}.Foreground(tcell.ColorFuchsia).Background(tcell.ColorBlack),
|
|
buffer.Builtin: tcell.Style{}.Foreground(tcell.ColorBlue).Background(tcell.ColorBlack),
|
|
buffer.Special: tcell.Style{}.Foreground(tcell.ColorFuchsia).Background(tcell.ColorBlack),
|
|
}
|
|
|
|
t.Highlighter = buffer.NewHighlighter(t.Buffer, lang, colorscheme)
|
|
}
|
|
|
|
// GetLineDelimiter returns "\r\n" for a CRLF buffer, or "\n" for an LF buffer.
|
|
func (t *TextEdit) GetLineDelimiter() string {
|
|
if t.IsCRLF {
|
|
return "\r\n"
|
|
} else {
|
|
return "\n"
|
|
}
|
|
}
|
|
|
|
// Changes a file's line delimiters. If `crlf` is true, then line delimiters are replaced
|
|
// with Windows CRLF (\r\n). If `crlf` is false, then line delimtiers are replaced with Unix
|
|
// LF (\n). The TextEdit `IsCRLF` variable is updated with the new value.
|
|
func (t *TextEdit) ChangeLineDelimiters(crlf bool) {
|
|
t.IsCRLF = crlf
|
|
t.Dirty = true
|
|
// line delimiters are constructed with String() function
|
|
// TODO: ^ not true anymore ^
|
|
panic("Cannot ChangeLineDelimiters")
|
|
}
|
|
|
|
// Delete with `forwards` false will backspace, destroying the character before the cursor,
|
|
// while Delete with `forwards` true will delete the character after (or on) the cursor.
|
|
// In insert mode, forwards is always true.
|
|
func (t *TextEdit) Delete(forwards bool) {
|
|
t.Dirty = true
|
|
|
|
var deletedLine bool // Whether any whole line has been deleted (changing the # of lines)
|
|
cursLine, cursCol := t.cursor.GetLineCol()
|
|
startingLine := cursLine
|
|
|
|
if t.selectMode { // If text is selected, delete the whole selection
|
|
t.selectMode = false // Disable selection and prevent infinite loop
|
|
|
|
startLine, startCol := t.selection.Start.GetLineCol()
|
|
endLine, endCol := t.selection.End.GetLineCol()
|
|
|
|
// Delete the region
|
|
t.Buffer.Remove(startLine, startCol, endLine, endCol)
|
|
t.cursor.SetLineCol(startLine, startCol) // Set cursor to start of region
|
|
|
|
deletedLine = startLine != endLine
|
|
} else { // Not deleting selection
|
|
if forwards { // Delete the character after the cursor
|
|
// If the cursor is not at the end of the last line...
|
|
if cursLine < t.Buffer.Lines()-1 || cursCol < t.Buffer.RunesInLine(cursLine) {
|
|
bytes := t.Buffer.Slice(cursLine, cursCol, cursLine, cursCol) // Get the character at cursor
|
|
deletedLine = bytes[0] == '\n'
|
|
|
|
t.Buffer.Remove(cursLine, cursCol, cursLine, cursCol) // Remove character at cursor
|
|
}
|
|
} else { // Delete the character before the cursor
|
|
// If the cursor is not at the first column of the first line...
|
|
if cursLine > 0 || cursCol > 0 {
|
|
t.cursor = t.cursor.Left() // Back up to that character
|
|
cursLine, cursCol = t.cursor.GetLineCol()
|
|
|
|
bytes := t.Buffer.Slice(cursLine, cursCol, cursLine, cursCol) // Get the char at cursor
|
|
deletedLine = bytes[0] == '\n'
|
|
|
|
t.Buffer.Remove(cursLine, cursCol, cursLine, cursCol) // Remove character at cursor
|
|
}
|
|
}
|
|
}
|
|
|
|
t.ScrollToCursor()
|
|
t.updateCursorVisibility()
|
|
|
|
if deletedLine {
|
|
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
|
|
} else {
|
|
t.Highlighter.InvalidateLines(startingLine, startingLine)
|
|
}
|
|
}
|
|
|
|
// Writes `contents` at the cursor position. Line delimiters and tab character supported.
|
|
// Any other control characters will be printed. Overwrites any active selection.
|
|
func (t *TextEdit) Insert(contents string) {
|
|
t.Dirty = true
|
|
|
|
if t.selectMode { // If there is a selection...
|
|
// Go to and delete the selection
|
|
t.Delete(true) // The parameter doesn't matter with selection
|
|
}
|
|
|
|
var lineInserted bool // True if contents contains a '\n'
|
|
cursLine, cursCol := t.cursor.GetLineCol()
|
|
startingLine := cursLine
|
|
|
|
runes := []rune(contents)
|
|
for i := 0; i < len(runes); i++ {
|
|
ch := runes[i]
|
|
switch ch {
|
|
case '\r':
|
|
// If the character after is a \n, then it is a CRLF
|
|
if i+1 < len(runes) && runes[i+1] == '\n' {
|
|
i++ // Consume '\n' after
|
|
t.Buffer.Insert(cursLine, cursCol, []byte{'\n'})
|
|
lineInserted = true
|
|
}
|
|
case '\n':
|
|
t.Buffer.Insert(cursLine, cursCol, []byte{'\n'})
|
|
lineInserted = true
|
|
case '\b':
|
|
t.Delete(false) // Delete the character before the cursor
|
|
case '\t':
|
|
if !t.UseHardTabs { // If this file does not use hard tabs...
|
|
// Insert spaces
|
|
spaces := strings.Repeat(" ", t.TabSize)
|
|
t.Buffer.Insert(cursLine, cursCol, []byte(spaces))
|
|
break
|
|
}
|
|
fallthrough // Append the \t character
|
|
default:
|
|
// Insert character into line
|
|
t.Buffer.Insert(cursLine, cursCol, []byte(string(ch)))
|
|
}
|
|
}
|
|
|
|
t.ScrollToCursor()
|
|
t.updateCursorVisibility()
|
|
|
|
if lineInserted {
|
|
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
|
|
} else {
|
|
t.Highlighter.InvalidateLines(startingLine, startingLine)
|
|
}
|
|
}
|
|
|
|
// getTabCountInLineAtCol returns tabs in the given line, before the column position,
|
|
// if hard tabs are enabled. If hard tabs are not enabled, the function returns zero.
|
|
// Multiply returned tab count by TabSize to get the offset produced by tabs.
|
|
// Col must be a valid column position in the given line. Maybe call clampLineCol before
|
|
// this function.
|
|
func (t *TextEdit) getTabCountInLineAtCol(line, col int) int {
|
|
if t.UseHardTabs {
|
|
return t.Buffer.Count(line, 0, line, col, []byte{'\t'})
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// updateCursorVisibility sets the position of the terminal's cursor with the
|
|
// cursor of the TextEdit. Sends a signal to show the cursor if the TextEdit
|
|
// is focused and not in select mode.
|
|
func (t *TextEdit) updateCursorVisibility() {
|
|
if t.focused && !t.selectMode {
|
|
columnWidth := t.getColumnWidth()
|
|
line, col := t.cursor.GetLineCol()
|
|
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1)
|
|
(*t.screen).ShowCursor(t.x+columnWidth+col+tabOffset-t.scrollx, t.y+line-t.scrolly)
|
|
}
|
|
}
|
|
|
|
// Scroll the screen if the cursor is out of view.
|
|
func (t *TextEdit) ScrollToCursor() {
|
|
line, col := t.cursor.GetLineCol()
|
|
|
|
// Handle hard tabs
|
|
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1) // Offset for the current line from hard tabs
|
|
|
|
// Scroll the screen when going to lines out of view
|
|
if line >= t.scrolly+t.height-1 { // If the new line is below view...
|
|
t.scrolly = line - t.height + 1 // Scroll just enough to view that line
|
|
} else if line < t.scrolly { // If the new line is above view
|
|
t.scrolly = line
|
|
}
|
|
|
|
columnWidth := t.getColumnWidth()
|
|
|
|
// Scroll the screen horizontally when going to columns out of view
|
|
if col+tabOffset >= t.scrollx+(t.width-columnWidth-1) { // If the new column is right of view
|
|
t.scrollx = (col + tabOffset) - (t.width - columnWidth) + 1 // Scroll just enough to view that column
|
|
} else if col+tabOffset < t.scrollx { // If the new column is left of view
|
|
t.scrollx = col + tabOffset // Scroll left enough to view that column
|
|
}
|
|
}
|
|
|
|
func (t *TextEdit) GetCursor() buffer.Cursor {
|
|
return t.cursor
|
|
}
|
|
|
|
func (t *TextEdit) SetCursor(newCursor buffer.Cursor) {
|
|
t.cursor = newCursor
|
|
t.updateCursorVisibility()
|
|
}
|
|
|
|
// getColumnWidth returns the width of the line numbers column if it is present.
|
|
func (t *TextEdit) getColumnWidth() int {
|
|
var columnWidth int
|
|
if t.LineNumbers {
|
|
// Set columnWidth to max count of line number digits
|
|
columnWidth = Max(3, 1+len(strconv.Itoa(t.Buffer.Lines()))) // Column has minimum width of 2
|
|
}
|
|
return columnWidth
|
|
}
|
|
|
|
// GetSelectedBytes returns a byte slice of the region of the buffer that is currently selected.
|
|
// If the returned string is empty, then nothing was selected. The slice returned may or may not
|
|
// be a copy of the buffer, so do not write to it.
|
|
func (t *TextEdit) GetSelectedBytes() []byte {
|
|
// TODO: there's a bug with copying text
|
|
if t.selectMode {
|
|
startLine, startCol := t.selection.Start.GetLineCol()
|
|
endLine, endCol := t.selection.End.GetLineCol()
|
|
return t.Buffer.Slice(startLine, startCol, endLine, endCol)
|
|
}
|
|
return []byte{}
|
|
}
|
|
|
|
// Draw renders the TextEdit component.
|
|
func (t *TextEdit) Draw(s tcell.Screen) {
|
|
columnWidth := t.getColumnWidth()
|
|
bufferLines := t.Buffer.Lines()
|
|
|
|
selectedStyle := t.theme.GetOrDefault("TextEditSelected")
|
|
columnStyle := t.Highlighter.Colorscheme.GetStyle(buffer.Column)
|
|
|
|
t.Highlighter.UpdateInvalidatedLines(t.scrolly, t.scrolly+(t.height-1))
|
|
|
|
var tabBytes []byte
|
|
if t.UseHardTabs {
|
|
// Only call Repeat once for each draw in hard tab files
|
|
tabBytes = bytes.Repeat([]byte{' '}, t.TabSize)
|
|
}
|
|
|
|
defaultStyle := t.Highlighter.Colorscheme.GetStyle(buffer.Default)
|
|
currentStyle := defaultStyle
|
|
|
|
for lineY := t.y; lineY < t.y+t.height; lineY++ { // For each line we can draw...
|
|
line := lineY + t.scrolly - t.y // The line number being drawn (starts at zero)
|
|
|
|
lineNumStr := "" // Line number as a string
|
|
|
|
if line < bufferLines { // Only index buffer if we are within it...
|
|
lineNumStr = strconv.Itoa(line + 1) // Only set for lines within the buffer (not view)
|
|
|
|
var origLineBytes []byte = t.Buffer.Line(line)
|
|
var lineBytes []byte = origLineBytes // Line to be drawn
|
|
|
|
if t.UseHardTabs {
|
|
lineBytes = bytes.ReplaceAll(lineBytes, []byte{'\t'}, tabBytes)
|
|
}
|
|
|
|
lineHighlightData := t.Highlighter.GetLineMatches(line)
|
|
var lineHighlightDataIdx int
|
|
|
|
var byteIdx int // Byte index of lineStr
|
|
var runeIdx int // Index into lineStr (as runes) we draw the next character at
|
|
col := t.x + columnWidth // X offset we draw the next rune at (some runes can be 2 cols wide)
|
|
|
|
for runeIdx < t.scrollx && byteIdx < len(lineBytes) {
|
|
_, size := utf8.DecodeRune(lineBytes[byteIdx:]) // Respect UTF-8
|
|
byteIdx += size
|
|
runeIdx++
|
|
}
|
|
|
|
tabOffsetAtRuneIdx := func(idx int) int {
|
|
var count int
|
|
for _, r := range string(origLineBytes) {
|
|
if r == '\t' {
|
|
count++
|
|
}
|
|
}
|
|
return count * (t.TabSize - 1)
|
|
}
|
|
|
|
// origRuneIdx converts a rune index from lineBytes to a runeIndex from origLineBytes
|
|
// not affected by the hard tabs becoming 4 or 8 spaces.
|
|
origRuneIdx := func(idx int) int { // returns the idx that is not mutated by hard tabs
|
|
var ridx int // new rune idx
|
|
for _, r := range string(origLineBytes) {
|
|
if idx <= 0 {
|
|
break
|
|
}
|
|
|
|
if r == '\t' {
|
|
idx -= t.TabSize
|
|
} else {
|
|
idx--
|
|
}
|
|
if idx >= 0 { // causes ridx = 0, when idx = 3
|
|
ridx++
|
|
}
|
|
}
|
|
return ridx
|
|
}
|
|
|
|
for col < t.x+t.width { // For each column in view...
|
|
var r rune = ' ' // Rune to draw this iteration
|
|
var size int = 1 // Size of the rune (in bytes)
|
|
var selected bool // Whether this rune should be styled as selected
|
|
|
|
tabOffsetAtRuneIdx := tabOffsetAtRuneIdx(runeIdx)
|
|
|
|
if byteIdx < len(lineBytes) { // If we are drawing part of the line contents...
|
|
r, size = utf8.DecodeRune(lineBytes[byteIdx:])
|
|
|
|
if r == '\n' {
|
|
r = ' '
|
|
}
|
|
|
|
startLine, startCol := t.selection.Start.GetLineCol()
|
|
endLine, endCol := t.selection.End.GetLineCol()
|
|
|
|
// Determine whether we select the current rune. Also only select runes within
|
|
// the line bytes range.
|
|
if t.selectMode && line >= startLine && line <= endLine { // If we're part of a selection...
|
|
_origRuneIdx := origRuneIdx(runeIdx)
|
|
if line == startLine { // If selection starts at this line...
|
|
if _origRuneIdx >= startCol { // And we're at or past the start col...
|
|
// If the start line is also the end line...
|
|
if line == endLine {
|
|
if _origRuneIdx <= endCol { // And we're before the end of that...
|
|
selected = true
|
|
}
|
|
} else { // Definitely highlight
|
|
selected = true
|
|
}
|
|
}
|
|
} else if line == endLine { // If selection ends at this line...
|
|
if _origRuneIdx <= endCol { // And we're at or before the end col...
|
|
selected = true
|
|
}
|
|
} else { // We're between the start and the end lines, definitely highlight.
|
|
selected = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine the style of the rune we draw next:
|
|
|
|
if selected {
|
|
currentStyle = selectedStyle
|
|
} else {
|
|
currentStyle = defaultStyle
|
|
|
|
if lineHighlightDataIdx < len(lineHighlightData) { // Works for single-line highlights
|
|
data := lineHighlightData[lineHighlightDataIdx]
|
|
if runeIdx-tabOffsetAtRuneIdx >= data.Col {
|
|
if runeIdx-tabOffsetAtRuneIdx > data.EndCol { // Passed that highlight data
|
|
currentStyle = defaultStyle
|
|
lineHighlightDataIdx++ // Go to next one
|
|
} else { // Start coloring as this syntax style
|
|
currentStyle = t.Highlighter.Colorscheme.GetStyle(data.Syntax)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw the rune
|
|
s.SetContent(col, lineY, r, nil, currentStyle)
|
|
|
|
col += runewidth.RuneWidth(r)
|
|
|
|
byteIdx += size
|
|
runeIdx++
|
|
}
|
|
}
|
|
|
|
columnStr := fmt.Sprintf("%s%s│", strings.Repeat(" ", columnWidth-len(lineNumStr)-1), lineNumStr) // Right align line number
|
|
|
|
DrawStr(s, t.x, lineY, columnStr, columnStyle) // Draw column
|
|
}
|
|
|
|
t.updateCursorVisibility()
|
|
}
|
|
|
|
// SetFocused sets whether the TextEdit is focused. When focused, the cursor is set visible
|
|
// and its position is updated on every event.
|
|
func (t *TextEdit) SetFocused(v bool) {
|
|
t.focused = v
|
|
if v {
|
|
t.updateCursorVisibility()
|
|
} else {
|
|
(*t.screen).HideCursor()
|
|
}
|
|
}
|
|
|
|
// HandleEvent allows the TextEdit to handle `event` if it chooses, returns
|
|
// whether the TextEdit handled the event.
|
|
func (t *TextEdit) HandleEvent(event tcell.Event) bool {
|
|
switch ev := event.(type) {
|
|
case *tcell.EventKey:
|
|
switch ev.Key() {
|
|
// Cursor movement
|
|
case tcell.KeyUp:
|
|
if ev.Modifiers() == tcell.ModShift {
|
|
if !t.selectMode {
|
|
var endCursor buffer.Cursor
|
|
if cursLine, _ := t.cursor.GetLineCol(); cursLine != 0 {
|
|
endCursor = t.cursor.Left()
|
|
} else {
|
|
endCursor = t.cursor
|
|
}
|
|
t.selection.End = endCursor
|
|
t.SetCursor(t.cursor.Up())
|
|
t.selection.Start = t.cursor
|
|
t.selectMode = true
|
|
t.ScrollToCursor()
|
|
break // Select only a single character at start
|
|
}
|
|
|
|
if t.selection.Start.Eq(t.cursor) {
|
|
t.SetCursor(t.cursor.Up())
|
|
t.selection.Start = t.cursor
|
|
} else {
|
|
t.SetCursor(t.cursor.Up())
|
|
t.selection.End = t.cursor
|
|
}
|
|
} else {
|
|
t.selectMode = false
|
|
t.SetCursor(t.cursor.Up())
|
|
}
|
|
t.ScrollToCursor()
|
|
case tcell.KeyDown:
|
|
if ev.Modifiers() == tcell.ModShift {
|
|
if !t.selectMode {
|
|
t.selection.Start = t.cursor
|
|
t.SetCursor(t.cursor.Down())
|
|
t.selection.End = t.cursor
|
|
t.selectMode = true
|
|
t.ScrollToCursor()
|
|
break
|
|
}
|
|
|
|
if t.selection.End.Eq(t.cursor) {
|
|
t.SetCursor(t.cursor.Down())
|
|
t.selection.End = t.cursor
|
|
} else {
|
|
t.SetCursor(t.cursor.Down())
|
|
t.selection.Start = t.cursor
|
|
}
|
|
} else {
|
|
t.selectMode = false
|
|
t.SetCursor(t.cursor.Down())
|
|
}
|
|
t.ScrollToCursor()
|
|
case tcell.KeyLeft:
|
|
if ev.Modifiers() == tcell.ModShift {
|
|
if !t.selectMode {
|
|
t.SetCursor(t.cursor.Left())
|
|
t.selection.Start, t.selection.End = t.cursor, t.cursor
|
|
t.selectMode = true
|
|
t.ScrollToCursor()
|
|
break // Select only a single character at start
|
|
}
|
|
|
|
if t.selection.Start.Eq(t.cursor) {
|
|
t.SetCursor(t.cursor.Left())
|
|
t.selection.Start = t.cursor
|
|
} else {
|
|
t.SetCursor(t.cursor.Left())
|
|
t.selection.End = t.cursor
|
|
}
|
|
} else {
|
|
t.selectMode = false
|
|
t.SetCursor(t.cursor.Left())
|
|
}
|
|
t.ScrollToCursor()
|
|
case tcell.KeyRight:
|
|
if ev.Modifiers() == tcell.ModShift {
|
|
if !t.selectMode {
|
|
t.selection.Start, t.selection.End = t.cursor, t.cursor
|
|
t.selectMode = true
|
|
break
|
|
}
|
|
|
|
if t.selection.End.Eq(t.cursor) {
|
|
t.SetCursor(t.cursor.Right())
|
|
t.selection.End = t.cursor
|
|
} else {
|
|
t.SetCursor(t.cursor.Right())
|
|
t.selection.Start = t.cursor
|
|
}
|
|
} else {
|
|
t.selectMode = false
|
|
t.SetCursor(t.cursor.Right())
|
|
}
|
|
t.ScrollToCursor()
|
|
case tcell.KeyHome:
|
|
cursLine, _ := t.cursor.GetLineCol()
|
|
// TODO: go to first (non-whitespace) character on current line, if we are not already there
|
|
// otherwise actually go to first (0) character of the line
|
|
t.SetCursor(t.cursor.SetLineCol(cursLine, 0))
|
|
t.ScrollToCursor()
|
|
case tcell.KeyEnd:
|
|
cursLine, _ := t.cursor.GetLineCol()
|
|
t.SetCursor(t.cursor.SetLineCol(cursLine, math.MaxInt32)) // Max column
|
|
t.ScrollToCursor()
|
|
case tcell.KeyPgUp:
|
|
_, cursCol := t.cursor.GetLineCol()
|
|
t.SetCursor(t.cursor.SetLineCol(t.scrolly-t.height, cursCol)) // Go a page up
|
|
t.ScrollToCursor()
|
|
case tcell.KeyPgDn:
|
|
_, cursCol := t.cursor.GetLineCol()
|
|
t.SetCursor(t.cursor.SetLineCol(t.scrolly+t.height*2-1, cursCol)) // Go a page down
|
|
t.ScrollToCursor()
|
|
|
|
// Deleting
|
|
case tcell.KeyBackspace:
|
|
fallthrough
|
|
case tcell.KeyBackspace2:
|
|
t.Delete(false)
|
|
case tcell.KeyDelete:
|
|
t.Delete(true)
|
|
|
|
// Other control
|
|
case tcell.KeyTab:
|
|
t.Insert("\t") // (can translate to four spaces)
|
|
case tcell.KeyEnter:
|
|
t.Insert("\n")
|
|
|
|
// Inserting
|
|
case tcell.KeyRune:
|
|
t.Insert(string(ev.Rune())) // Insert rune
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|