# Tutorial: build a Turbo editor for your own language By the end of this tutorial you will have a working full-screen terminal IDE — menu bar, movable windows, mouse, themes, a file tree, terminal windows and syntax colouring — for a language of your choosing. We will build one for **Zig**, and it will be about eighty lines of Go. No knowledge of turbo-core is required. You need Go 1.26 or later and a terminal. ## Prerequisites - Go 1.26 or later on your PATH. - A checkout of turbo-core beside where you are about to work. If you do not have one: ```bash git clone ssh://git@rickub.com/turbo-editors/turbo-core.git ``` ## Step 1 — Make the module Beside your turbo-core checkout, type: ```bash mkdir turbo-zig && cd turbo-zig go mod init example.com/turbo-zig ``` You should see: ``` go: creating new go.mod: module example.com/turbo-zig ``` We now have an empty module. Next we point it at the library. ## Step 2 — Depend on turbo-core Type: ```bash go mod edit -require=codeberg.org/turbo-editors/turbo-core@v0.1.0 go mod edit -replace=codeberg.org/turbo-editors/turbo-core=../turbo-core ``` Nothing is printed. Look at `go.mod`: ```bash cat go.mod ``` You should see a `require` line and a `replace` line naming turbo-core. The `replace` is what makes the checkout beside you the one that gets used. ## Step 3 — Say which editor this is Create `profile.go`: ```go package main import "codeberg.org/turbo-editors/turbo-core/profile" // zig is the editor we are building. func zig() profile.Profile { return profile.Profile{ Name: "Turbo Zig", Slug: "turbo-zig", Language: "Zig", ToolsMenu: "~Z~ig", RootMarkers: []string{"build.zig"}, Server: profile.Server{ Command: "zls", InstallHint: "see https://github.com/zigtools/zls", }, Templates: profile.Templates{ Settings: settingsTemplate, Snippets: snippetsTemplate, Tools: toolsTemplate, }, } } ``` That is the whole of what makes this editor different from Turbo Go. We have not written the templates yet — that is the next step. ## Step 4 — Write the three starter files Create `templates.go`: ```go package main const settingsTemplate = `# turbo-zig project settings. [editor] theme = %q autosave = false autosave_delay = %q ` const snippetsTemplate = `# turbo-zig snippets. # # A snippet with no group goes into %s. # Your own live in: # %s [[snippet]] name = "TODO" body = "TODO: " ` const toolsTemplate = `# turbo-zig tools. [[tool]] name = "~B~uild" command = "zig build" output = "popup" [[tool]] name = "~T~est" command = "zig build test" output = "popup" [[tool]] name = "~R~un" command = "zig build run" output = "terminal" ` ``` The blanks matter: `settingsTemplate` takes the theme name and the autosave delay, `snippetsTemplate` takes the ungrouped group's name and the user's snippets path. The [Profile reference](../reference/profile.md) lists them exactly. ## Step 5 — Write the command Create `main.go`: ```go package main import ( "context" "fmt" "os" "github.com/gdamore/tcell/v2" "codeberg.org/turbo-editors/turbo-core/app" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "turbo-zig:", err) os.Exit(1) } } func run() error { screen, err := tcell.NewScreen() if err != nil { return err } if err := screen.Init(); err != nil { return err } defer screen.Fini() screen.EnableMouse() screen.EnablePaste() p := zig() editor := app.New(screen, "turbo-classic", p) files := os.Args[1:] if len(files) == 0 { editor.NewFile() } for _, file := range files { editor.Open(file) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() editor.StartLanguageServer(ctx, app.ProjectRoot(p, files)) defer editor.Language().Stop(context.Background()) return editor.Run() } ``` ## Step 6 — Run it Type: ```bash go mod tidy go run . main.go ``` Your terminal fills with a blue desktop. Along the top you should see: ``` File Edit Search Run Options Window Snippets Zig Help ``` and a window in the middle showing `main.go`. Press **F10** to open the menu bar, the arrow keys to move along it, and **Alt-X** to leave. We have a working editor. Everything on that bar works: **F3** opens a file, **F8** opens a terminal window with a real shell in it, **F9** opens the project tree, **Options ▸ Theme** offers eleven colour themes. ## Step 7 — Colour the language Zig files are not coloured yet, because nothing has told the library what Zig is. Create `syntax.go`: ```go package main import "codeberg.org/turbo-editors/turbo-core/syntax" // registerZig teaches the library a very small amount of Zig: its keywords, // its comments and its strings. func registerZig() { syntax.Register(syntax.Definition{ Language: "zig", Extensions: []string{".zig"}, Highlight: highlightZig, }) } var zigKeywords = map[string]bool{ "const": true, "var": true, "fn": true, "pub": true, "return": true, "if": true, "else": true, "while": true, "for": true, "struct": true, "try": true, "defer": true, "test": true, "comptime": true, } func highlightZig(src string) [][]syntax.Span { return syntax.ScanLines(src, func(line []rune, carry struct{}) ([]syntax.Span, struct{}) { s := syntax.NewLineScanner(line) for !s.AtEnd() { switch r := s.Peek(0); { case r == ' ' || r == '\t': s.SkipSpaces() case s.HasPrefix(0, "//"): s.TakeRest(syntax.ClassComment) case r == '"': syntax.TakeQuoted(s, '"', syntax.ClassString) case syntax.IsDigit(r): s.TakeWhile(syntax.ClassNumber, syntax.IsWordRune) case syntax.IsLetter(r) || r == '_': start := s.Pos() for !s.AtEnd() && syntax.IsWordRune(s.Peek(0)) { s.Advance(1) } class := syntax.ClassIdentifier if zigKeywords[string(line[start:s.Pos()])] { class = syntax.ClassKeyword } s.Emit(start, s.Pos(), class) default: s.Advance(1) } } return s.Spans(), carry }) } ``` Add one line to `run`, just above `p := zig()`: ```go registerZig() ``` ## Step 8 — See it coloured Write a Zig file and open it: ```bash cat > hello.zig <<'EOF' // A greeting. const std = @import("std"); pub fn main() void { std.debug.print("hello {d}\n", .{42}); } EOF go run . hello.zig ``` `const`, `pub`, `fn` are drawn in the keyword colour, `// A greeting.` in the comment colour, `"hello {d}\n"` in the string colour and `42` in the number colour. Press **Alt-X** to leave. ## What now? You have an editor. What it does not yet have is a real scanner — the one above knows fourteen keywords and nothing about `\\` multiline strings or `\\'` character literals. - To write a proper one → [Add a language](../how-to/add-a-language.md) - To make completion work → [Talk to a language server](../how-to/talk-to-a-language-server.md) - To see every field you skipped → [Profile reference](../reference/profile.md) - To understand why the library is shaped this way → [Architecture](../explanation/architecture.md)