Create a Text Editor in Go – Syntax Highlighting

You can access the code of this chapter in the Kilo-Go github repository in the syntax branch.

Currently your file structure should look something like this:

Now that we have a functional text editor, we will do the last step in this tutorial, and …


This content originally appeared on DEV Community and was authored by Andres Court

You can access the code of this chapter in the Kilo-Go github repository in the syntax branch.

Currently your file structure should look something like this:

Now that we have a functional text editor, we will do the last step in this tutorial, and that is add syntax highlighting

Colorful digits

Let's start by having some color, we will highlight numbers by coloring each digit red.

File: utils/digits.go

package utils

func IsDigit(c byte) bool {
    return c >= '0' && c <= '9'
}

File: editor/output.go

func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
    for y := range e.screenrows {
        filerow := y + e.rowoffset
        if filerow >= e.numrows {
            ...
        } else {
            chars := e.rows[filerow].render
            ...
            for j := range chars {
                if utils.IsDigit(chars[j]) {
                    fmt.Fprintf(abuf, "%c[38;2;255;0;0m", utils.ESC)

                    fmt.Fprintf(abuf, "%c", chars[j])
                    fmt.Fprintf(abuf, "%c[39m", utils.ESC)
                } else {
                    fmt.Fprintf(abuf, "%c", chars[j])
                }
            }
        }
        ...
    }
}

Note: the <Esc> [ 30 m through the <Esc> [ 37 m sequences define the text color, being 30 black and 37 white, and the <Esc> [ 39 m will reset the color back to normal.
To use RGB colors in text you can use <Esc> [ 38 ; 2 ; <Red> ; <Green> ; <Blue> m

Refactor syntax highlighting

Now that we know how to color text, we need to find a way that we save the highlights for each row before we display it

File: utils/constants.go

type EditorHighlight int

const (
    HL_NORMAL EditorHighlight = iota
    HL_NUMBER
)

Note: since Go doesn't have enums we are simulating it by the use of the EditorHighlight type

File: editor/editor.go

type EditorRow struct {
    ...
    hl     []utils.EditorHighlight
}

File: editor/row.go

func (e *EditorConfig) editorUpdateRow(row *EditorRow) {
    for j := 0; j < len(row.chars); j++ {
        ...
    }

    editorUpdateSyntax(row)
}

func (e *EditorConfig) editorInsertRow(at int, s string) {
    e.rows = append(e.rows[:at], append([]EditorRow{{chars: s}}, e.rows[at:]...)...)

    e.rows[at].hl = nil
    e.numrows++
    e.isDirty = true
}

File: editor/row.go

func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
    for y := range e.screenrows {
        filerow := y + e.rowoffset
        if filerow >= e.numrows {
            ...
        } else {
            ...
            for j := range chars {
                r, g, b := editorSyntaxToColor(e.rows[filerow].hl[j])
                fmt.Fprintf(abuf, "%c[38;2;%d;%d;%dm", utils.ESC, r, g, b)
                fmt.Fprintf(abuf, "%c", chars[j])
            }
            fmt.Fprintf(abuf, "%c[39m", utils.ESC)
        }
        ...
    }
}

File: editor/syntax.go

package editor

import "github.com/alcb1310/kilo-go/utils"

func editorUpdateSyntax(row *EditorRow) {
    row.hl = make([]utils.EditorHighlight, len(row.render))

    for i := range len(row.render) {
        if utils.IsDigit(row.render[i]) {
            row.hl[i] = utils.HL_NUMBER
        }
    }
}

func editorSyntaxToColor(hl utils.EditorHighlight) (r uint8, g uint8, b uint8) {
    switch hl {
    case utils.HL_NUMBER:
        return 255, 0, 0
    case utils.HL_NORMAL:
        return 255, 255, 255
    default:
        return 255, 255, 255
    }
}

Colorful search results

Now that we've refactored our code to have the ability to highlight, lets change the text color of a search query

File: utils/constants.go

const (
    HL_NORMAL EditorHighlight = iota
    HL_NUMBER
    HL_MATCH
)

File: editor/syntax.go

func editorSyntaxToColor(hl utils.EditorHighlight) (r uint8, g uint8, b uint8) {
    switch hl {
    ...
    case utils.HL_MATCH:
        return 0, 0, 255
    ...
    }
}

File: editor/output.go

func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
    for y := range e.screenrows {
        filerow := y + e.rowoffset
        if filerow >= e.numrows {
            ...
        } else {
            ...
            for j := range render {
                r, g, b := editorSyntaxToColor(e.rows[filerow].hl[j])
                fmt.Fprintf(abuf, "%c[38;2;%d;%d;%dm", utils.ESC, r, g, b)
                fmt.Fprintf(abuf, "%c", render[j])
            }
            fmt.Fprintf(abuf, "%c[39m", utils.ESC)

        }
        ...
    }
}

File: editor/find.go

func (e *EditorConfig) editorFindCallback(query string, key int) {
    ...
    for range e.numrows {
        ...
        if strings.Contains(row.chars, query) {
            ...
            rx := editorRowCxToRx(row, e.cx)
            j := rx
            for i := 0; i < len(query); i++ {
                row.hl[j] = utils.HL_MATCH
                j++
            }
            return
        }
    }
}

File: editor/row.go

func (e *EditorConfig) editorAppendRow(s string) {
    row := EditorRow{
        chars:  s,
        render: make([]byte, 0),
    }
    ...
}

Restore syntax highlighting after search

Now when we've highlighted the search term, we want that once we finish the search the text will not be highlighted

File: editor/find.go

var saved_hl_line uint = 0
var saved_hl []utils.EditorHighlight = nil

func (e *EditorConfig) editorFindCallback(query string, key int) {
    if saved_hl_line != 0 {
        copy(e.rows[saved_hl_line].hl, saved_hl)
        saved_hl = nil
    }
    ...
    for range e.numrows {
        ...
        if strings.Contains(row.chars, query) {
            saved_hl_line = uint(current)
            saved_hl = make([]utils.EditorHighlight, len(row.render))
            copy(saved_hl, row.hl)
            ...
        }
    }
}

Colorful numbers

Currently we are highlighting every digit regardless if it is part of an expression (eg. in uint8 we are highlighting the 8) or if it is a number. We just want to highlight the digits only if it is a number

File: utils/constants.go

const (
    ...
    KILO_DECIMAL_SEPARATOR      = '.'
)

File: utils/digits.go

func IsSeparator(c byte) bool {
    return IsSpace(c) || strings.ContainsRune(",.()+-/*=~%<>[];{}", rune(c))
}

func IsSpace(c byte) bool {
    return c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

File: editor/syntax.go

func editorUpdateSyntax(row *EditorRow) {
    prevSep := true
    row.hl = make([]utils.EditorHighlight, len(row.render))

    i := 0
    for i < len(row.render) {
        c := row.render[i]
        var prevHL utils.EditorHighlight
        if i > 0 {
            prevHL = row.hl[i-1]
        } else {
            prevHL = utils.HL_NORMAL
        }

        if utils.IsDigit(c) &&
            (prevSep || prevHL == utils.HL_NUMBER) ||
            (c == utils.KILO_DECIMAL_SEPARATOR && prevHL == utils.HL_NUMBER) {
            row.hl[i] = utils.HL_NUMBER
            i++
            prevSep = false
            continue
        }

        prevSep = utils.IsSeparator(c)
        i++
    }
}

Detect filetype

No we want to detect what filetype are we opening so we can apply the different highlight options for it

File: utils/constants.go

const (
    HL_HIGHLIGHT_NUMBER = (1 << 0)
)

File: editor/editor.go

type EditorSyntax struct {
    filetype  string
    filematch []string
    flags     uint
}

var GO_HL_EXTENSIONS = []string{".go"}
var HLDB = []EditorSyntax{
    {"go", GO_HL_EXTENSIONS, utils.HL_HIGHLIGHT_NUMBER},
}

type EditorConfig struct {
    ...
    syntax        *EditorSyntax
}

func NewEditor(f func()) *EditorConfig {
    ...
    return &EditorConfig{
        ...
        syntax:        nil,
    }
}

File: editor/syntax.go

func (e *EditorConfig) editorUpdateSyntax(row *EditorRow) {
    ...
    if e.syntax == nil {
        return
    }

    i := 0
    for i < len(row.render) {
        ...
        if e.syntax.flags&utils.HL_HIGHLIGHT_NUMBER == 1 {
            if utils.IsDigit(c) &&
                (prevSep || prevHL == utils.HL_NUMBER) ||
                (c == utils.KILO_DECIMAL_SEPARATOR && prevHL == utils.HL_NUMBER) {
                row.hl[i] = utils.HL_NUMBER
                i++
                prevSep = false
                continue
            }
        }
        ...
    }
}

func (e *EditorConfig) editorSelectSyntaxHighlight() {
    e.syntax = nil
    if e.filename == "" {
        return
    }

    lastIndex := strings.LastIndex(e.filename, ".")
    if lastIndex == -1 {
        return
    }
    ext := e.filename[lastIndex:]

    for i, s := range HLDB {
        isExt := s.filematch[i][0] == '.'

        if (isExt && ext == s.filematch[i]) ||
            (!isExt && strings.Contains(ext, s.filematch[i])) {
            e.syntax = &s

            for _, row := range e.rows {
                e.editorUpdateSyntax(&row)
            }

            return
        }
    }
}

File: editor/row.go

func (e *EditorConfig) editorUpdateRow(row *EditorRow) {
    ...
    e.editorUpdateSyntax(row)
}

File: editor/output.go

func (e *EditorConfig) editorDrawStatusBar(abuf *ab.AppendBuffer) {
    ...
    filetype := "[no ft]"
    if e.syntax != nil {
        filetype = "[" + e.syntax.filetype + "]"
    }
    ...
    rstatus := fmt.Sprintf("%s | column: %d row: %d/%d ", filetype, e.rx+1, e.cy+1, e.numrows)
    ...
}

File: editor/file.go

func (e *EditorConfig) editorOpen(filename string) {
    ...
    e.filename = filename
    e.editorSelectSyntaxHighlight()
    ...
}

func (e *EditorConfig) editorSave() {
    if e.filename == "" {
        ...
        e.editorSelectSyntaxHighlight()
    }
    ...
}

Colorful strings

Our next step will be recognizing strings and highlighting them

File: utils/constants.go

const (
    HL_HIGHLIGHT_NUMBER = (1 << 0)
    HL_HIGHLIGHT_STRING = (1 << 1)
)
...
const (
    HL_NORMAL EditorHighlight = iota
    HL_STRING
    ...
)

File: editor/editor.go

var HLDB = []EditorSyntax{
    {
        "go",
        GO_HL_EXTENSIONS,
        utils.HL_HIGHLIGHT_NUMBER | utils.HL_HIGHLIGHT_STRING,
    },
}

File: editor/syntax.go

func (e *EditorConfig) editorUpdateSyntax(row *EditorRow) {
    ...
    var inString byte = 0
    ...
    for i < len(row.render) {
        ...
        if e.syntax.flags&utils.HL_HIGHLIGHT_STRING == 2 {
            if inString != 0 {
                row.hl[i] = utils.HL_STRING
                if c == '\\' && i+1 < len(row.render) {
                    i++
                    row.hl[i] = utils.HL_STRING
                    i++
                    continue
                }
                if c == inString {
                    inString = 0
                }
                i++
                prevSep = true
                continue
            } else {
                if c == '"' || c == '\'' {
                    inString = c
                    row.hl[i] = utils.HL_STRING
                    i++
                    continue
                }
            }
        }
        ...
    }
}

func editorSyntaxToColor(hl utils.EditorHighlight) (r uint8, g uint8, b uint8) {
    r = 255
    g = 255
    b = 255

    switch hl {
    case utils.HL_NORMAL:
        return
    case utils.HL_NUMBER:
        g = 0
        b = 0
        return
    case utils.HL_MATCH:
        r = 51
        b = 0
        return
    case utils.HL_STRING:
        g = 39
        b = 155
        return
    default:
        return
    }
}

Colorful comments

Now we want to add color to the comments

File: utils/constants.go

const (
    HL_NORMAL EditorHighlight = iota
    HL_COMMENT
    ...
)

File: editor/editor.go

type EditorSyntax struct {
    ...
    singleLineComment string
}

var HLDB = []EditorSyntax{
    {
        filetype:          "go",
        filematch:         GO_HL_EXTENSIONS,
        flags:             utils.HL_HIGHLIGHT_NUMBER | utils.HL_HIGHLIGHT_STRING,
        singleLineComment: "//",
    },
}

File: editor/syntax.go

func (e *EditorConfig) editorUpdateSyntax(row *EditorRow) {
    ...
    scs := e.syntax.singleLineComment
    ...
    for i < len(row.render) {
        ...
        if len(scs) > 0 && inString == 0 {
            if strings.HasPrefix(string(row.render[i:]), scs) {
                for j := i; j < len(row.render); j++ {
                    row.hl[j] = utils.HL_COMMENT
                }
                break
            }
        }
        ...
    }
}

func editorSyntaxToColor(hl utils.EditorHighlight) (r uint8, g uint8, b uint8) {
    ...
    switch hl {
    ...
    case utils.HL_COMMENT:
        r = 0
        return
    ...
    }
}

Note: To disable comment highlight, just set the single line comment to an empty string

Colorful keywords

Now it's turn to highlight keywords, since Go is a strong typed language we will be using two colors:

  • First color for the actual keywords
  • Second color for the type names

File: utils/constants.go

const (
    HL_NORMAL EditorHighlight = iota
    ...
    HL_KEYWORD
    HL_TYPE_KEYWORD
    ...
)

File: utils/digits.go

func IsSeparator(c byte) bool {
    return IsSpace(c) || strings.ContainsRune(":,.()+-/*=~%<>[];{}", rune(c)) || rune(c) == 0
}

File: editor/editor.go

type EditorSyntax struct {
    ...
    keywords          []string
    types             []string
}
...
var GO_HL_KEYWORDS = []string{
    "package",
    "import",
    "func",
    "type",
    "var",
    "const",
    "if",
    "else",
    "switch",
    "case",
    "default",
    "for",
    "range",
    "goto",
    "continue",
    "select",
    "return",
    "break",
}

var GO_HL_TYPES = []string{
    "bool",
    "byte",
    "error",
    "float32",
    "float64",
    "int",
    "int16",
    "int32",
    "int64",
    "int8",
    "rune",
    "string",
    "uint",
    "uint16",
    "uint32",
    "uint64",
    "uint8",
}

var HLDB = []EditorSyntax{
    {
        ...
        keywords:          GO_HL_KEYWORDS,
        types:             GO_HL_TYPES,
    },
}

File: editor/operations.go

func (e *EditorConfig) editorInsertNewline() {
    if e.cy == len(e.rows) {
        ...
    } else {
        ...
        row.render = make([]byte, 0)
        ...
        row.render = make([]byte, 0)
        ...
    }
    ...
}

File: editor/row.go

func (e *EditorConfig) editorRowInsertChar(row *EditorRow, at int, c byte) {
    row.render = make([]byte, 0)
    ...
}

func (e *EditorConfig) editorRowDeleteChar(row *EditorRow, at int) {
    ...
    row.render = make([]byte, 0)
    ...
}

func (e *EditorConfig) editorRowAppendString(row *EditorRow, s string) {
    ...
    row.render = make([]byte, 0)
    ...
}

File: editor/syntax.go

func (e *EditorConfig) editorUpdateSyntax(row *EditorRow) {
    ...
    keywords := e.syntax.keywords
    types := e.syntax.types
    ...
    for i < len(row.render) {
        ...
        if prevSep {
            j := 0
            for j = 0; j < len(keywords); j++ {
                key := keywords[j]
                if strings.HasPrefix(string(row.render[i:]), key) &&
                    ((i+len(key) < len(row.render) &&
                        utils.IsSeparator(row.render[i+len(key)])) ||
                        i+len(key) == len(row.render)) {
                    for k := range key {
                        row.hl[i+k] = utils.HL_KEYWORD
                    }
                    i += len(key) - 1
                    break
                }
            }

            if j < len(keywords) {
                prevSep = false
                continue
            }

            m := 0
            for m = 0; m < len(types); m++ {
                key := types[m]
                if strings.HasPrefix(string(row.render[i:]), key) &&
                    ((i+len(key) < len(row.render) &&
                        utils.IsSeparator(row.render[i+len(key)])) ||
                        i+len(key) == len(row.render)) {
                    for k := range key {
                        row.hl[i+k] = utils.HL_TYPE_KEYWORD
                    }
                    i += len(key) - 1
                    break
                }
            }

            if m < len(types) {
                prevSep = false
                continue
            }
        }

        prevSep = utils.IsSeparator(c)
        i++
    }
}

func editorSyntaxToColor(hl utils.EditorHighlight) (r uint8, g uint8, b uint8) {
    r = 255
    g = 255
    b = 255

    switch hl {
    case utils.HL_NUMBER:
        g = 0
        b = 0
    case utils.HL_MATCH:
        r = 51
        b = 0
    case utils.HL_STRING:
        g = 39
        b = 155
    case utils.HL_COMMENT:
        r = 0
    case utils.HL_KEYWORD:
        g = 239
        b = 0
    case utils.HL_TYPE_KEYWORD:
        g = 55
        b = 239
        r = 126
    }
    return
}

Multiline comments

Finally we will highlight multiline comments, and we will have it have the same color as the single line comments

File: utils/constants.go

const (
    HL_NORMAL EditorHighlight = iota
    ...
    HL_MLCOMMENT
    ...
)

File: editor/editor.go

type EditorSyntax struct {
    ...
    multiLineCommentStart string
    multiLineCommentEnd   string
    ...
}

var HLDB = []EditorSyntax{
    {
        ...
        multiLineCommentStart: "/*",
        multiLineCommentEnd:   "*/",
        ...
    },
}

type EditorRow struct {
    idx           int
    hlOpenComment bool
    ...
}

File: editor/row.go

func (e *EditorConfig) editorUpdateRow(row *EditorRow) {
    ...
    row.idx = e.numrows
    ...
}

func (e *EditorConfig) editorRowDeleteChar(row *EditorRow, at int) {
    ...
    for j := at; j <= e.numrows-1; j++ {
        e.rows[j].idx--
    }
    ...
}

func (e *EditorConfig) editorInsertRow(at int, s string) {
    ...
    for j := at + 1; j <= e.numrows; j++ {
        e.rows[j].idx++
    }

    e.rows[at].idx = at
    ...
}

File: editor/syntax.go

func (e *EditorConfig) editorUpdateSyntax(row *EditorRow) {
    ...
    inComment := row.idx > 0 && e.rows[row.idx-1].hlOpenComment
    ...
    scs := e.syntax.singleLineComment
    mcs := e.syntax.multiLineCommentStart
    mce := e.syntax.multiLineCommentEnd
    ...

    scsLen := len(scs)
    mcsLen := len(mcs)
    mceLen := len(mce)

    i := 0
    for i < len(row.render) {
        ...
        if scsLen > 0 && inString == 0 && !inComment {
            ...
        }

        if mcsLen > 0 && mceLen > 0 && inString == 0 {
            if inComment {
                row.hl[i] = utils.HL_COMMENT
                if strings.HasPrefix(string(row.render[i:]), mce) {
                    for j := i; j < mceLen; j++ {
                        row.hl[j] = utils.HL_COMMENT
                    }
                    i += mceLen
                    inComment = false
                    prevSep = true
                    continue
                } else {
                    i++
                    continue
                }
            } else if strings.HasPrefix(string(row.render[i:]), mcs) {
                for j := i; j < mcsLen; j++ {
                    row.hl[j] = utils.HL_COMMENT
                }
                i += mcsLen
                inComment = true
                continue
            }
        }
        ...
    }

    changed := row.hlOpenComment != inComment
    row.hlOpenComment = inComment

    if changed && row.idx+1 < e.numrows {
        e.editorUpdateSyntax(&e.rows[row.idx+1])
    }
}

func editorSyntaxToColor(hl utils.EditorHighlight) (r uint8, g uint8, b uint8) {
    ...
    switch hl {
    ...
    case utils.HL_COMMENT, utils.HL_MLCOMMENT:
        r = 0
    ...
    }
    return
}

Finally we have completed the tutorial on a basic text editor, in future posts we will be going through on improving it with a serious features.


This content originally appeared on DEV Community and was authored by Andres Court


Print Share Comment Cite Upload Translate Updates
APA

Andres Court | Sciencx (2025-11-25T02:05:48+00:00) Create a Text Editor in Go – Syntax Highlighting. Retrieved from https://www.scien.cx/2025/11/25/create-a-text-editor-in-go-syntax-highlighting/

MLA
" » Create a Text Editor in Go – Syntax Highlighting." Andres Court | Sciencx - Tuesday November 25, 2025, https://www.scien.cx/2025/11/25/create-a-text-editor-in-go-syntax-highlighting/
HARVARD
Andres Court | Sciencx Tuesday November 25, 2025 » Create a Text Editor in Go – Syntax Highlighting., viewed ,<https://www.scien.cx/2025/11/25/create-a-text-editor-in-go-syntax-highlighting/>
VANCOUVER
Andres Court | Sciencx - » Create a Text Editor in Go – Syntax Highlighting. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/25/create-a-text-editor-in-go-syntax-highlighting/
CHICAGO
" » Create a Text Editor in Go – Syntax Highlighting." Andres Court | Sciencx - Accessed . https://www.scien.cx/2025/11/25/create-a-text-editor-in-go-syntax-highlighting/
IEEE
" » Create a Text Editor in Go – Syntax Highlighting." Andres Court | Sciencx [Online]. Available: https://www.scien.cx/2025/11/25/create-a-text-editor-in-go-syntax-highlighting/. [Accessed: ]
rf:citation
» Create a Text Editor in Go – Syntax Highlighting | Andres Court | Sciencx | https://www.scien.cx/2025/11/25/create-a-text-editor-in-go-syntax-highlighting/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.