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 mthrough the<Esc> [ 37 msequences define the text color, being30black and37white, and the<Esc> [ 39 mwill 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
enumswe are simulating it by the use of theEditorHighlighttype
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.

Top comments (0)