gilo/editor/editor.go
Timothy Warren 42e50dfebb
All checks were successful
timw4mail/gilo/pipeline/head This commit looks good
Add quit confirmation for modified document
2021-04-02 10:34:19 -04:00

275 lines
4.7 KiB
Go

package editor
import (
"fmt"
"time"
"timshome.page/gilo/key"
"timshome.page/gilo/terminal"
)
// ----------------------------------------------------------------------------
// !Editor
// ----------------------------------------------------------------------------
type point struct {
x int
y int
}
type statusMsg struct {
message string
created time.Time
}
type editor struct {
screen *terminal.Screen
cursor *point
offset *point
document *document
status *statusMsg
quitTimes uint8
renderX int
}
func New() *editor {
screen := terminal.Size()
// Subtract rows for status bar and message bar/prompt
screen.Rows -= 2
cursor := &point{0, 0}
offset := &point{0, 0}
document := newDocument()
status := &statusMsg{
"",
time.Now(),
}
return &editor{
screen,
cursor,
offset,
document,
status,
KiloQuitTimes,
0,
}
}
func (e *editor) Open(filename string) {
e.document.open(filename)
}
func (e *editor) save() {
size := e.document.save()
if size > 0 {
e.SetStatusMessage("%d bytes written to disk", size)
} else {
e.SetStatusMessage("Failed to save file")
}
}
func (e *editor) SetStatusMessage(template string, a ...interface{}) {
e.status = &statusMsg{
fmt.Sprintf(template, a...),
time.Now(),
}
}
func (e *editor) ProcessKeypress() bool {
ch, _ := terminal.ReadKey()
switch ch {
case key.Ctrl('q'):
if e.document.dirty && e.quitTimes > 0 {
e.SetStatusMessage("WARNING!!! File has unsaved changes. Press Ctrl-Q %d more tiems to quite.", e.quitTimes)
e.quitTimes -= 1
return true
}
// Clean up on exit
terminal.Write(terminal.ClearScreen + terminal.ResetCursor)
return false
case key.Ctrl('s'):
e.save()
case key.Enter:
case key.Backspace, key.Ctrl('h'):
case key.Esc, key.Ctrl('l'):
// Modifier keys that return ANSI escape sequences
str := parseEscapeSequence()
switch str {
case keyUp,
keyDown,
keyLeft,
keyRight,
keyPageUp,
keyPageDown,
keyHome,
keyEnd:
e.moveCursor(str)
}
default:
e.insertChar(ch)
}
if e.quitTimes != KiloQuitTimes {
e.quitTimes = KiloQuitTimes
}
return true
}
func (e *editor) moveCursor(key string) {
var row *row
if e.cursor.y >= e.document.rowCount() {
row = nil
} else {
row = e.document.rows[e.cursor.y]
}
switch key {
case keyLeft:
if e.cursor.x != 0 {
e.cursor.x -= 1
}
// Move from beginning of current row to end of previous row
if e.cursor.y > 0 {
e.cursor.y -= 1
e.cursor.x = e.document.rows[e.cursor.y].size()
}
case keyRight:
if row != nil && e.cursor.x < row.size() {
e.cursor.x += 1
}
// Move from end of current line to beginning of next line
if row != nil && e.cursor.x == row.size() && e.cursor.y < e.document.rowCount()-1 {
e.cursor.y += 1
e.cursor.x = 0
}
case keyUp:
if e.cursor.y != 0 {
e.cursor.y -= 1
}
case keyDown:
if e.cursor.y < e.document.rowCount() {
e.cursor.y += 1
}
case keyPageUp:
if e.cursor.y > e.screen.Rows {
e.cursor.y -= e.screen.Rows
} else {
e.cursor.y = 0
}
case keyPageDown:
if e.cursor.y+e.screen.Rows > e.document.rowCount() {
e.cursor.y += e.screen.Rows
} else {
e.cursor.y = e.document.rowCount() - 1
}
case keyHome:
e.cursor.x = 0
case keyEnd:
if row != nil {
e.cursor.x = row.size()
}
}
if e.cursor.y < e.document.rowCount() {
row = e.document.rows[e.cursor.y]
rowLen := row.size()
// Snap to the end of a shorter line from a longer one
if e.cursor.x > rowLen {
e.cursor.x = rowLen
}
}
}
func (e *editor) insertChar(ch rune) {
if e.cursor.y == e.document.rowCount() {
e.document.appendRow("")
}
e.document.rows[e.cursor.y].insertRune(ch, e.cursor.x)
e.cursor.x += 1
e.document.dirty = true
}
// Convert the raw ANSI escape sequences to the type of key input
func parseEscapeSequence() string {
var runes []rune
for i := 0; i < 3; i++ {
ch, size := terminal.ReadKey()
if size == 0 {
return "\x1b"
}
runes = append(runes, ch)
if i == 1 && runes[1] >= 'A' {
// \eOH \eOF
if runes[0] == 'O' {
switch runes[1] {
case 'H':
return keyHome
case 'F':
return keyEnd
}
}
// \e[A
if runes[0] == '[' {
switch runes[1] {
case 'A':
return keyUp
case 'B':
return keyDown
case 'C':
return keyRight
case 'D':
return keyLeft
case 'H':
return keyHome
case 'F':
return keyEnd
}
}
}
// \e[1~
if i == 2 && runes[0] == '[' && runes[2] == '~' {
switch runes[1] {
case '1':
return keyHome
case '3':
return keyDelete
case '4':
return keyEnd
case '5':
return keyPageUp
case '6':
return keyPageDown
case '7':
return keyHome
case '8':
return keyEnd
}
}
}
return string('\x1b')
}