DEV Community

Cover image for Highlight Multiple Languages
Andres Court
Andres Court

Posted on

Highlight Multiple Languages

The changes explained in this post can be found in the Kilo-go Github repository, in the multilingual branch.

At the moment, we have syntax highlighting but only for Go, and we want our editor to be able to highlight multiple languages. What do we want to achieve:

  • Highlight multiple languages
  • Multiple language support
  • Add new languages without editing the code

Refactor: move the syntax struct to its own package

First we will be moving the syntax struct to its own package so we have the methods we will be creating in it

File: sytnax/syntax.go

package syntax

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

var GO_HL_EXTENSIONS = []string{".go"}
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 GO_SYNTAX = EditorSyntax{
    Filetype:              "go",
    Filematch:             GO_HL_EXTENSIONS,
    Flags:                 utils.HL_HIGHLIGHT_NUMBER | utils.HL_HIGHLIGHT_STRING,
    SingleLineComment:     "//",
    MultiLineCommentStart: "/*",
    MultiLineCommentEnd:   "*/",
    Keywords:              GO_HL_KEYWORDS,
    Types:                 GO_HL_TYPES,
}

type EditorSyntax struct {
    Filetype              string
    Filematch             []string
    Flags                 uint
    SingleLineComment     string
    MultiLineCommentStart string
    MultiLineCommentEnd   string
    Keywords              []string
    Types                 []string
}

var HLDB = []EditorSyntax{
    GO_SYNTAX,
    C_SYNTAX,
}
Enter fullscreen mode Exit fullscreen mode

File: editor/editor.go

type EditorConfig struct {
    ...
    syntax        *syntax.EditorSyntax
}
Enter fullscreen mode Exit fullscreen mode

File: editor/output.go

func (e *EditorConfig) editorDrawStatusBar(abuf *ab.AppendBuffer) {
    ...
    if e.syntax != nil {
        filetype = "[" + e.syntax.Filetype + "]"
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

File: editor/syntax.go

func (e *EditorConfig) editorUpdateSyntax(row *EditorRow) {
    ...
    scs := e.syntax.SingleLineComment
    mcs := e.syntax.MultiLineCommentStart
    mce := e.syntax.MultiLineCommentEnd
    keywords := e.syntax.Keywords
    types := e.syntax.Types
    ...
    i := 0
    for i < len(row.render) {
        ...

        if e.syntax.Flags&utils.HL_HIGHLIGHT_STRING == 2 {
            ...
        }

        if e.syntax.Flags&utils.HL_HIGHLIGHT_NUMBER == 1 {
            ...
        }
        ...
    }
    ...
}

func (e *EditorConfig) editorSelectSyntaxHighlight() {
    ...
    for i, s := range syntax.HLDB {
        isExt := s.Filematch[i][0] == '.'

        if (isExt && ext == s.Filematch[i]) ||
            (!isExt && strings.Contains(ext, s.Filematch[i])) {
            ...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Adding C/C++ support

Now the next step is to add support for another language, we will add C/C++

File: syntax/syntax.go

package syntax

type EditorSyntax struct {
    Filetype              string
    Filematch             []string
    Flags                 uint
    SingleLineComment     string
    MultiLineCommentStart string
    MultiLineCommentEnd   string
    Keywords              []string
    Types                 []string
}

var HLDB = []EditorSyntax{
    GO_SYNTAX,
    C_SYNTAX,
}
Enter fullscreen mode Exit fullscreen mode

File: syntax/go.go

package syntax

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

var GO_HL_EXTENSIONS = []string{".go"}
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 GO_SYNTAX = EditorSyntax{
    Filetype:              "go",
    Filematch:             GO_HL_EXTENSIONS,
    Flags:                 utils.HL_HIGHLIGHT_NUMBER | utils.HL_HIGHLIGHT_STRING,
    SingleLineComment:     "//",
    MultiLineCommentStart: "/*",
    MultiLineCommentEnd:   "*/",
    Keywords:              GO_HL_KEYWORDS,
    Types:                 GO_HL_TYPES,
}
Enter fullscreen mode Exit fullscreen mode

File: syntax/c.go

package syntax

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

var C_HL_EXTENSIONS []string = []string{".c", ".h", ".cpp"}

var C_SYNTAX = EditorSyntax{
    Filetype:              "c",
    Filematch:             C_HL_EXTENSIONS,
    Flags:                 utils.HL_HIGHLIGHT_NUMBER | utils.HL_HIGHLIGHT_STRING,
    SingleLineComment:     "//",
    MultiLineCommentStart: "/*",
    MultiLineCommentEnd:   "*/",
    Keywords: []string{"switch", "if", "while", "for", "break", "continue", "return", "else",
        "struct", "union", "typedef", "static", "enum", "class", "case", "#include", "#define", "#ifndef", "#ifdef", "#endif", "#else"},
    Types: []string{"int", "long", "double", "float", "char", "unsigned", "signed",
        "void"},
}
Enter fullscreen mode Exit fullscreen mode

After testing we can see that there is a bug that we need to address, that's why we need to update:

File: editor/syntax.go

func (e *EditorConfig) editorSelectSyntaxHighlight() {
    ...
    for _, s := range syntax.HLDB {
        for j := range s.Filematch {
            isExt := s.Filematch[j][0] == '.'
            if (isExt && ext == s.Filematch[j]) ||
                (!isExt && strings.Contains(ext, s.Filematch[j])) {
                e.syntax = &s

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

                return
            }
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Multilingual support without changing the editor

Finally we have multilingual support, but what happens if we need to add another language, well, we will need to add a new file in the syntax package for that language with the fields required and recompile the application. I'm sure we can do better, in the editor's config directory we can create a highlight directory where we can add files with the required information, so lets do that

File: ${XDG_CONFIG}/kilo/highlight/go.toml

number=true
string=true
filetype="GO"
extensions=[".go"]
keywords=[ "package", "import", "func", "type", "var", "const", "if", "else",
    "switch", "case", "default", "for", "range", "goto", "continue", "select", "return", "break",
]
types=[ "bool", "byte", "error", "float32", "float64", "int", "int16", "int32", "nil",
    "int64", "int8", "rune", "string", "uint", "uint16", "uint32", "uint64", "uint8",
]
slc="//"
mlcs="/*"
mlce="*/"
Enter fullscreen mode Exit fullscreen mode

File: ${XDG_CONFIG}/kilo/highlight/c.toml

number=true
string=true
filetype="C/CPP"
extensions=[".c", ".h", ".cpp"]
keywords=["switch", "if", "while", "for", "break", "continue", "return", "else",
        "struct", "union", "typedef", "static", "enum", "class", "case", "#include", "#define", "#ifndef", "#ifdef", "#endif", "#else"
]
types=["int", "long", "double", "float", "char", "unsigned", "signed",
        "void", "NULL"]
slc="//"
mlcs="/*"
mlce="*/"
Enter fullscreen mode Exit fullscreen mode

File: main.go

func init() {
    ...
    syntax.LoadSyntax()
}
Enter fullscreen mode Exit fullscreen mode

File: syntax/syntax.go

package syntax

import (
    "fmt"
    "os"
    "path"

    "github.com/BurntSushi/toml"
    "github.com/alcb1310/kilo-go/utils"
)

type EditorSyntax struct {
    Filetype              string   `toml:"filetype"`
    Filematch             []string `toml:"extensions"`
    Flags                 uint
    SingleLineComment     string `toml:"slc"`
    MultiLineCommentStart string `toml:"mlcs"`
    MultiLineCommentEnd   string `toml:"mlce"`
    Keywords              []string
    Types                 []string
    NumberFlag            bool `toml:"number"`
    StringFlag            bool `toml:"string"`
}

var HLDB = []EditorSyntax{}

func LoadSyntax() error {
    dir, err := os.UserConfigDir()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
        return err
    }

    tomlDir := path.Join(path.Join(dir, "kilo"), "highlight")
    _, err = os.Stat(tomlDir)
    if err != nil {
        if os.IsNotExist(err) {
            return nil
        }

        fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
        return err
    }

    files, err := os.ReadDir(tomlDir)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
        return err
    }

    for _, file := range files {
        s := EditorSyntax{}
        if file.IsDir() {
            continue
        }

        _, err := toml.DecodeFile(path.Join(tomlDir, file.Name()), &s)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
            return err
        }

        if s.NumberFlag {
            s.Flags |= utils.HL_HIGHLIGHT_NUMBER
        }

        if s.StringFlag {
            s.Flags |= utils.HL_HIGHLIGHT_STRING
        }

        HLDB = append(HLDB, s)
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)