Create a Text Editor in Go – Moving the Cursor
You can access the code of this chapter in the Kilo-Go github repository in the movingaround branch
Currently your file structure should look something like this:
Cursor state
First we need a way to track the cursor position
File: editor/editor.go
type EditorConfig struct {
...
cx, cy int
}
func NewEditor(f func()) *EditorConfig {
...
return &EditorConfig{
...
cx: 0,
cy: 0,
}
}
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
...
e.editorDrawRows(abuf)
fmt.Fprintf(abuf, "%c[%d;%dH", utils.ESC, e.cy + 1, e.cx+1)
fmt.Fprintf(abuf, "%c[?25h", utils.ESC)
...
}
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.rows {
if y == e.rows/3 {
...
welcomeLen := min(len(welcomeMessage), e.cols)
...
}
}
}
Cursor movement
Now that we’ve tracked the cursor position, we can add the logic to move the cursor with vim-like keys so we will be using h, j, k and l
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
case utils.CtrlKey('q'):
utils.SafeExit(e.restoreFunc, nil)
case 'h', 'j', 'k', 'l':
e.editorMoveCursor(b)
}
}
func (e *EditorConfig) editorMoveCursor(key byte) {
switch key {
case 'h':
e.cx--
case 'j':
e.cy++
case 'k':
e.cy--
case 'l':
e.cx++
}
}
Reading the arrow keys
Lets read the arrow keys next, each one is represented by 3 bytes:
-
Up Arrowis represented by<Esc> [ A -
Down Arrowis represented by<Esc> [ B -
Right Arrowis represented by<Esc> [ C -
Left Arrowis represented by<Esc> [ D
So with that in mind, lets read them
File: editor/terminal.go
import (
"github.com/alcb1310/kilo-go/utils"
)
func (e *EditorConfig) editorReadKey() (byte, error) {
b, err := e.reader.ReadByte()
if b == utils.ESC {
seq := make([]byte, 2)
seq[0], err = e.reader.ReadByte()
if err != nil {
return utils.ESC, nil
}
seq[1], err = e.reader.ReadByte()
if err != nil {
return utils.ESC, nil
}
if seq[0] == '[' {
switch seq[1] {
case 'A':
return 'k', nil
case 'B':
return 'j', nil
case 'C':
return 'l', nil
case 'D':
return 'h', nil
}
}
return utils.ESC, nil
}
return b, err
}
Refactor: improve readability
Now that we have the arrow keys working, lets improve the readability by creating constants for it
File: utils/constants.go
const (
ARROW_UP = 'k'
ARROW_DOWN = 'j'
ARROW_LEFT = 'h'
ARROW_RIGHT = 'l'
)
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
case utils.CtrlKey('q'):
utils.SafeExit(e.restoreFunc, nil)
case utils.ARROW_DOWN, utils.ARROW_LEFT, utils.ARROW_RIGHT, utils.ARROW_UP:
e.editorMoveCursor(b)
}
}
func (e *EditorConfig) editorMoveCursor(key byte) {
switch key {
case utils.ARROW_LEFT:
e.cx--
case utils.ARROW_DOWN:
e.cy++
case utils.ARROW_UP:
e.cy--
case utils.ARROW_RIGHT:
e.cx++
}
}
File: editor/terminal.go
func (e *EditorConfig) editorReadKey() (byte, error) {
...
if seq[0] == '[' {
switch seq[1] {
case 'A':
return utils.ARROW_UP, nil
case 'B':
return utils.ARROW_DOWN, nil
case 'C':
return utils.ARROW_RIGHT, nil
case 'D':
return utils.ARROW_LEFT, nil
}
}
...
}
Refactor: only use arrows to move
Now that we’ve added constants to the arrow keys we can give them a value outside of the byte range and start using them as an int. However doing so, we will need to change several files in order to return int instead of byte
File: utils/constant.go
const (
ARROW_UP = iota + 1000
ARROW_DOWN
ARROW_LEFT
ARROW_RIGHT
)
File: utils/ctrl.go
func CtrlKey(key byte) int {
return int(key & 0x1f)
}
File editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
...
}
File editor/terminal.go
func (e *EditorConfig) editorReadKey() (int, error) {
...
return int(b), err
}
Note: We mostly needed to change the function signature for it to work
Prevent moving the cursor off screen
Currently, we can have the cx and cy values to go into the negatives or go past the right and bottom edges of the screen, so lets prevent that
File: editor/input.go
func (e *EditorConfig) editorMoveCursor(key int) {
switch key {
case utils.ARROW_LEFT:
if e.cx != 0 {
e.cx--
}
case utils.ARROW_DOWN:
if e.cy != e.rows-1 {
e.cy++
}
case utils.ARROW_UP:
if e.cy != 0 {
e.cy--
}
case utils.ARROW_RIGHT:
if e.cx != e.cols-1 {
e.cx++
}
}
}
The Page Up and Page Down keys
To complete our movements, we need to detect a few more special keypresses that use escape sequences, like the arrow keys did. We’ll start with the Page Up which is sent as <Esc> [ 5 ~ and Page Down which is sent as <Esc> [ 6 ~
File utils/constants.go
const (
ARROW_UP = iota + 1000
ARROW_DOWN
ARROW_LEFT
ARROW_RIGHT
PAGE_UP
PAGE_DOWN
)
File: editor/terminal.go
func (e *EditorConfig) editorProcessKeypress() {
...
case utils.PAGE_DOWN, utils.PAGE_UP:
times := e.rows
for range times {
if b == utils.PAGE_DOWN {
e.editorMoveCursor(utils.ARROW_DOWN)
} else {
e.editorMoveCursor(utils.ARROW_UP)
}
}
}
}
File: editor/terminal.go
func (e *EditorConfig) editorReadKey() (int, error) {
...
if b == utils.ESC {
seq := make([]byte, 3)
...
if seq[0] == '[' {
if seq[1] >= '0' && seq[1] <= '9' {
seq[2], err = e.reader.ReadByte()
if err != nil {
return utils.ESC, nil
}
if seq[2] == '~' {
switch seq[1] {
case '5':
return utils.PAGE_UP, nil
case '6':
return utils.PAGE_DOWN, nil
}
}
} else {
switch seq[1] {
case 'A':
return utils.ARROW_UP, nil
case 'B':
return utils.ARROW_DOWN, nil
case 'C':
return utils.ARROW_RIGHT, nil
case 'D':
return utils.ARROW_LEFT, nil
}
}
}
...
}
The Home and End keys
Like the previous keys, these keys also send escape sequences. Unlike previous keys, there are many different escape sequences that could be sent by these keys.
- The
Homekey could be sent as<Esc> [ 1 ~,<Esc> [ 7 ~,<Esc> [ Hor<Esc> O H - The
Endkey could be sent as<Esc> [ 4 ~,<Esc> [ 8 ~,<Esc> [ For<Esc> O F
File: utils/constants.go
const (
ARROW_UP = iota + 1000
ARROW_DOWN
ARROW_LEFT
ARROW_RIGHT
HOME_KEY
END_KEY
PAGE_UP
PAGE_DOWN
)
File: editor/terminal.go
func (e *EditorConfig) editorReadKey() (int, error) {
...
if seq[0] == '[' {
if seq[1] >= '0' && seq[1] <= '9' {
...
if seq[2] == '~' {
switch seq[1] {
case '1':
return utils.HOME_KEY, nil
case '4':
return utils.END_KEY, nil
case '5':
return utils.PAGE_UP, nil
case '6':
return utils.PAGE_DOWN, nil
case '7':
return utils.HOME_KEY, nil
case '8':
return utils.END_KEY, nil
}
}
} else {
switch seq[1] {
case 'A':
return utils.ARROW_UP, nil
case 'B':
return utils.ARROW_DOWN, nil
case 'C':
return utils.ARROW_RIGHT, nil
case 'D':
return utils.ARROW_LEFT, nil
case 'H':
return utils.HOME_KEY, nil
case 'F':
return utils.END_KEY, nil
}
}
} else if seq[0] == 'O' {
switch seq[1] {
case 'H':
return utils.HOME_KEY, nil
case 'F':
return utils.END_KEY, nil
}
}
...
}
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.HOME_KEY:
e.cx = 0
case utils.END_KEY:
e.cx = e.cols - 1
}
}
The Delete key
Lastly we will detect when the Delete key is pressed. It simply sends the escape sequence <Esc> [ 3 ~, so it will be easy to add it to our switch statement. For now we will just log when the key is pressed
File: utils/constants.go
const (
ARROW_LEFT = iota + 1000
ARROW_RIGHT
ARROW_UP
ARROW_DOWN
DEL_KEY
HOME_KEY
END_KEY
PAGE_UP
PAGE_DOWN
)
File: editor/terminal.go
func (e *EditorConfig) editorReadKey() (int, error) {
...
if b == utils.ESC {
...
switch seq[0] {
case '[':
if seq[1] >= '0' && seq[1] <= '9' {
...
if seq[2] == '~' {
switch seq[1] {
case '1':
return utils.HOME_KEY, nil
case '3':
return utils.DEL_KEY, nil
...
}
}
...
case 'O':
switch seq[1] {
case 'H':
return utils.HOME_KEY, nil
case 'F':
return utils.END_KEY, nil
}
}
...
}
File: editor/input.go
func (e *EditorConfig) editorProcessKeypress() {
...
switch b {
...
case utils.DEL_KEY:
slog.Info("DEL_KEY")
...
}