turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

build-an-editor.md · 293 lines · 6.9 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1# Tutorial: build a Turbo editor for your own language
2
3By 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.
4
5No knowledge of turbo-core is required. You need Go 1.26 or later and a terminal.
6
7## Prerequisites
8
9- Go 1.26 or later on your PATH.
10- A checkout of turbo-core beside where you are about to work. If you do not have one:
11
12```bash
📦 Turbo Core d662ceb k33g 11h ago13git clone ssh://git@rickub.com/turbo-editors/turbo-core.git
🛟 Updated. 28d5985 k33g 18h ago14```
15
16## Step 1 — Make the module
17
18Beside your turbo-core checkout, type:
19
20```bash
21mkdir turbo-zig && cd turbo-zig
22go mod init example.com/turbo-zig
23```
24
25You should see:
26
27```
28go: creating new go.mod: module example.com/turbo-zig
29```
30
31We now have an empty module. Next we point it at the library.
32
33## Step 2 — Depend on turbo-core
34
35Type:
36
37```bash
38go mod edit -require=codeberg.org/turbo-editors/turbo-core@v0.1.0
39go mod edit -replace=codeberg.org/turbo-editors/turbo-core=../turbo-core
40```
41
42Nothing is printed. Look at `go.mod`:
43
44```bash
45cat go.mod
46```
47
48You 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.
49
50## Step 3 — Say which editor this is
51
52Create `profile.go`:
53
54```go
55package main
56
57import "codeberg.org/turbo-editors/turbo-core/profile"
58
59// zig is the editor we are building.
60func zig() profile.Profile {
61 return profile.Profile{
62 Name: "Turbo Zig",
63 Slug: "turbo-zig",
64 Language: "Zig",
65 ToolsMenu: "~Z~ig",
66 RootMarkers: []string{"build.zig"},
67 Server: profile.Server{
68 Command: "zls",
69 InstallHint: "see https://github.com/zigtools/zls",
70 },
71 Templates: profile.Templates{
72 Settings: settingsTemplate,
73 Snippets: snippetsTemplate,
74 Tools: toolsTemplate,
75 },
76 }
77}
78```
79
80That is the whole of what makes this editor different from Turbo Go. We have not written the templates yet — that is the next step.
81
82## Step 4 — Write the three starter files
83
84Create `templates.go`:
85
86```go
87package main
88
89const settingsTemplate = `# turbo-zig project settings.
90
91[editor]
92
93theme = %q
94autosave = false
95autosave_delay = %q
96`
97
98const snippetsTemplate = `# turbo-zig snippets.
99#
100# A snippet with no group goes into %s.
101# Your own live in:
102# %s
103
104[[snippet]]
105name = "TODO"
106body = "TODO: "
107`
108
109const toolsTemplate = `# turbo-zig tools.
110
111[[tool]]
112name = "~B~uild"
113command = "zig build"
114output = "popup"
115
116[[tool]]
117name = "~T~est"
118command = "zig build test"
119output = "popup"
120
121[[tool]]
122name = "~R~un"
123command = "zig build run"
124output = "terminal"
125`
126```
127
128The 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.
129
130## Step 5 — Write the command
131
132Create `main.go`:
133
134```go
135package main
136
137import (
138 "context"
139 "fmt"
140 "os"
141
142 "github.com/gdamore/tcell/v2"
143
144 "codeberg.org/turbo-editors/turbo-core/app"
145)
146
147func main() {
148 if err := run(); err != nil {
149 fmt.Fprintln(os.Stderr, "turbo-zig:", err)
150 os.Exit(1)
151 }
152}
153
154func run() error {
155 screen, err := tcell.NewScreen()
156 if err != nil {
157 return err
158 }
159 if err := screen.Init(); err != nil {
160 return err
161 }
162 defer screen.Fini()
163 screen.EnableMouse()
164 screen.EnablePaste()
165
166 p := zig()
167 editor := app.New(screen, "turbo-classic", p)
168
169 files := os.Args[1:]
170 if len(files) == 0 {
171 editor.NewFile()
172 }
173 for _, file := range files {
174 editor.Open(file)
175 }
176
177 ctx, cancel := context.WithCancel(context.Background())
178 defer cancel()
179 editor.StartLanguageServer(ctx, app.ProjectRoot(p, files))
180 defer editor.Language().Stop(context.Background())
181
182 return editor.Run()
183}
184```
185
186## Step 6 — Run it
187
188Type:
189
190```bash
191go mod tidy
192go run . main.go
193```
194
195Your terminal fills with a blue desktop. Along the top you should see:
196
197```
198 File Edit Search Run Options Window Snippets Zig Help
199```
200
201and 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.
202
203We 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.
204
205## Step 7 — Colour the language
206
207Zig files are not coloured yet, because nothing has told the library what Zig is. Create `syntax.go`:
208
209```go
210package main
211
212import "codeberg.org/turbo-editors/turbo-core/syntax"
213
214// registerZig teaches the library a very small amount of Zig: its keywords,
215// its comments and its strings.
216func registerZig() {
217 syntax.Register(syntax.Definition{
218 Language: "zig",
219 Extensions: []string{".zig"},
220 Highlight: highlightZig,
221 })
222}
223
224var zigKeywords = map[string]bool{
225 "const": true, "var": true, "fn": true, "pub": true, "return": true,
226 "if": true, "else": true, "while": true, "for": true, "struct": true,
227 "try": true, "defer": true, "test": true, "comptime": true,
228}
229
230func highlightZig(src string) [][]syntax.Span {
231 return syntax.ScanLines(src, func(line []rune, carry struct{}) ([]syntax.Span, struct{}) {
232 s := syntax.NewLineScanner(line)
233 for !s.AtEnd() {
234 switch r := s.Peek(0); {
235 case r == ' ' || r == '\t':
236 s.SkipSpaces()
237 case s.HasPrefix(0, "//"):
238 s.TakeRest(syntax.ClassComment)
239 case r == '"':
240 syntax.TakeQuoted(s, '"', syntax.ClassString)
241 case syntax.IsDigit(r):
242 s.TakeWhile(syntax.ClassNumber, syntax.IsWordRune)
243 case syntax.IsLetter(r) || r == '_':
244 start := s.Pos()
245 for !s.AtEnd() && syntax.IsWordRune(s.Peek(0)) {
246 s.Advance(1)
247 }
248 class := syntax.ClassIdentifier
249 if zigKeywords[string(line[start:s.Pos()])] {
250 class = syntax.ClassKeyword
251 }
252 s.Emit(start, s.Pos(), class)
253 default:
254 s.Advance(1)
255 }
256 }
257 return s.Spans(), carry
258 })
259}
260```
261
262Add one line to `run`, just above `p := zig()`:
263
264```go
265 registerZig()
266```
267
268## Step 8 — See it coloured
269
270Write a Zig file and open it:
271
272```bash
273cat > hello.zig <<'EOF'
274// A greeting.
275const std = @import("std");
276
277pub fn main() void {
278 std.debug.print("hello {d}\n", .{42});
279}
280EOF
281go run . hello.zig
282```
283
284`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.
285
286## What now?
287
288You 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.
289
290- To write a proper one → [Add a language](../how-to/add-a-language.md)
291- To make completion work → [Talk to a language server](../how-to/talk-to-a-language-server.md)
292- To see every field you skipped → [Profile reference](../reference/profile.md)
293- To understand why the library is shaped this way → [Architecture](../explanation/architecture.md)