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 Arrow is represented by <Esc> [ A
  • Down Arrow is represented by <Esc> [ B
  • Right Arrow is represented by <Esc> [ C
  • Left Arrow is 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 Home key could be sent as <Esc> [ 1 ~, <Esc> [ 7 ~, <Esc> [ H or <Esc> O H
  • The End key could be sent as <Esc> [ 4 ~, <Esc> [ 8 ~, <Esc> [ F or <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")
    ...
}

Similar Posts