diff --git a/editor/editor.go b/editor/editor.go index f02d3ed..89754e0 100644 --- a/editor/editor.go +++ b/editor/editor.go @@ -3,7 +3,6 @@ package editor import ( "fmt" "time" - "timshome.page/gilo/key" "timshome.page/gilo/terminal" ) @@ -60,7 +59,7 @@ func (e *editor) Open(filename string) { e.document.open(filename) } -func (e *editor) save() { +func (e *editor) Save() { size := e.document.save() if size > 0 { @@ -83,151 +82,6 @@ func (e *editor) ProcessKeypress() bool { return e.processKeypressChar(ch) } -/** - * Determine what to do with an individual character of input - */ -func (e *editor) processKeypressChar(ch rune) bool { - 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'): - e.delChar() - - case key.Esc, key.Ctrl('l'): - // Modifier keys that return ANSI escape sequences - str := parseEscapeSequence() - - // Don't swallow a character after ESC if it doesn't - // start an ANSI escape sequence - if len(str) == 1 { - return e.processKeypressChar(rune(str[0])) - } - - e.processKeypressStr(str) - - default: - e.insertChar(ch) - } - - // Clear the quit message and restart the - // confirmation count if confirmation is not - // completed - if e.quitTimes != KiloQuitTimes { - e.quitTimes = KiloQuitTimes - e.SetStatusMessage("") - } - - return true -} - -/** - * Determine what do do with a parsed ANSI escape sequence - */ -func (e *editor) processKeypressStr(key string) { - switch key { - case keyUp, - keyDown, - keyLeft, - keyRight, - keyPageUp, - keyPageDown, - keyHome, - keyEnd: - - e.moveCursor(key) - - case keyDelete: - e.moveCursor(keyRight) - e.delChar() - } -} - -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("") @@ -250,87 +104,3 @@ func (e *editor) delChar() { e.document.dirty = true } - -// Convert the raw ANSI escape sequences to the type of key input -func parseEscapeSequence() string { - // If we aren't starting an escape sequence, - // return the character - startChar, _ := terminal.ReadKey() - if startChar != '[' && startChar != 'O' { - return string(startChar) - } - - // Read one or two characters after - // \e[ or \eO, which is the end of the - // handled escape sequences - runes := [2]rune{'\000', '\000'} - for i := 0; i < 2; i++ { - ch, size := terminal.ReadKey() - if size == 0 { - return string(rune(key.Esc)) - } - runes[i] = ch - - if i == 0 && runes[0] >= 'A' && runes[0] <= 'Z' { - return escSeqToKey([]rune{startChar, runes[0]}) - } - - // \e[*~ - if i == 1 && startChar == '[' && runes[1] == '~' { - return escSeqToKey([]rune{startChar, runes[0], runes[1]}) - } - } - - return string(rune(key.Esc)) -} - -func escSeqToKey (seq []rune) string { - // \eO* - // \e[* - if len(seq) == 2 { - startChar, cmd := seq[0], seq[1] - if startChar == 'O' { - switch cmd { - case 'H': - return keyHome - case 'F': - return keyEnd - } - } else if startChar == '[' { - switch cmd { - case 'A': - return keyUp - case 'B': - return keyDown - case 'C': - return keyRight - case 'D': - return keyLeft - case 'H': - return keyHome - case 'F': - return keyEnd - } - } - } else if len(seq) == 3 { // \e[*~ - cmd := seq[1] - switch cmd { - 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(rune(key.Esc)) -} diff --git a/editor/editor_test.go b/editor/editor_test.go index ee7e869..dde7865 100644 --- a/editor/editor_test.go +++ b/editor/editor_test.go @@ -10,42 +10,6 @@ func TestNew(t *testing.T) { } } -type moveCursor struct { - keys []string - withFile bool - cursor *point -} - -var cursorTests = []moveCursor{ - {[]string{"\x1b"}, false, &point{0, 0}}, - {[]string{keyRight}, true, &point{1, 0}}, - {[]string{keyEnd}, true, &point{14, 0}}, - {[]string{keyEnd, keyHome}, true, &point{0, 0}}, - {[]string{keyRight, keyLeft}, true, &point{0, 0}}, - {[]string{keyDown, keyUp}, true, &point{0, 0}}, - // {[]string{keyPageUp}, true, &point{0, 0}}, -} - -func TestMoveCursor(t *testing.T) { - for _, test := range cursorTests { - e := New() - - if test.withFile { - e.Open("editor.go") - } - - for _, key := range test.keys { - e.moveCursor(key) - } - want := test.cursor - got := e.cursor - - if got.x != want.x || got.y != want.y { - t.Errorf("Output %v not equal to expected %v for input %q", got, want, test.keys) - } - } -} - func TestInsertChar(t *testing.T) { e := New() e.insertChar('q') diff --git a/editor/input.go b/editor/input.go new file mode 100644 index 0000000..8313251 --- /dev/null +++ b/editor/input.go @@ -0,0 +1,235 @@ +package editor + +import ( + "timshome.page/gilo/key" + "timshome.page/gilo/terminal" +) + +/** + * Determine what to do with an individual character of input + */ +func (e *editor) processKeypressChar(ch rune) bool { + 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'): + e.delChar() + + case key.Esc, key.Ctrl('l'): + // Modifier keys that return ANSI escape sequences + str := parseEscapeSequence() + + // Don't swallow a character after ESC if it doesn't + // start an ANSI escape sequence + if len(str) == 1 { + return e.processKeypressChar(rune(str[0])) + } + + e.processKeypressStr(str) + + default: + e.insertChar(ch) + } + + // Clear the quit message and restart the + // confirmation count if confirmation is not + // completed + if e.quitTimes != KiloQuitTimes { + e.quitTimes = KiloQuitTimes + e.SetStatusMessage("") + } + + return true +} + +/** + * Determine what do do with a parsed ANSI escape sequence + */ +func (e *editor) processKeypressStr(key string) { + switch key { + case keyUp, + keyDown, + keyLeft, + keyRight, + keyPageUp, + keyPageDown, + keyHome, + keyEnd: + + e.moveCursor(key) + + case keyDelete: + e.moveCursor(keyRight) + e.delChar() + } +} + +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 + } + } +} + +// Convert the raw ANSI escape sequences to the type of key input +func parseEscapeSequence() string { + // If we aren't starting an escape sequence, + // return the character + startChar, _ := terminal.ReadKey() + if startChar != '[' && startChar != 'O' { + return string(startChar) + } + + // Read one or two characters after + // \e[ or \eO, which is the end of the + // handled escape sequences + runes := [2]rune{'\000', '\000'} + for i := 0; i < 2; i++ { + ch, size := terminal.ReadKey() + if size == 0 { + return string(rune(key.Esc)) + } + runes[i] = ch + + if i == 0 && runes[0] >= 'A' && runes[0] <= 'Z' { + return escSeqToKey([]rune{startChar, runes[0]}) + } + + // \e[*~ + if i == 1 && startChar == '[' && runes[1] == '~' { + return escSeqToKey([]rune{startChar, runes[0], runes[1]}) + } + } + + return string(rune(key.Esc)) +} + +func escSeqToKey(seq []rune) string { + // \eO* + // \e[* + if len(seq) == 2 { + startChar, cmd := seq[0], seq[1] + if startChar == 'O' { + switch cmd { + case 'H': + return keyHome + case 'F': + return keyEnd + } + } else if startChar == '[' { + switch cmd { + case 'A': + return keyUp + case 'B': + return keyDown + case 'C': + return keyRight + case 'D': + return keyLeft + case 'H': + return keyHome + case 'F': + return keyEnd + } + } + } else if len(seq) == 3 { // \e[*~ + cmd := seq[1] + switch cmd { + 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(rune(key.Esc)) +} diff --git a/editor/input_test.go b/editor/input_test.go new file mode 100644 index 0000000..26d25d3 --- /dev/null +++ b/editor/input_test.go @@ -0,0 +1,39 @@ +package editor + +import "testing" + +type moveCursor struct { + keys []string + withFile bool + cursor *point +} + +var cursorTests = []moveCursor{ + {[]string{"\x1b"}, false, &point{0, 0}}, + {[]string{keyRight}, true, &point{1, 0}}, + {[]string{keyEnd}, true, &point{14, 0}}, + {[]string{keyEnd, keyHome}, true, &point{0, 0}}, + {[]string{keyRight, keyLeft}, true, &point{0, 0}}, + {[]string{keyDown, keyUp}, true, &point{0, 0}}, + // {[]string{keyPageUp}, true, &point{0, 0}}, +} + +func TestMoveCursor(t *testing.T) { + for _, test := range cursorTests { + e := New() + + if test.withFile { + e.Open("editor.go") + } + + for _, key := range test.keys { + e.moveCursor(key) + } + want := test.cursor + got := e.cursor + + if got.x != want.x || got.y != want.y { + t.Errorf("Output %v not equal to expected %v for input %q", got, want, test.keys) + } + } +}