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.

architecture.md · 64 lines · 6.9 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1# Architecture — explanation
2
3## What is this about?
4
5turbo-core is a terminal IDE with a hole where the language goes. Eighteen packages implement everything an editor does — text, undo, windows, menus, dialogs, themes, a VT emulator, a file tree, an LSP client, a coding agent — and none of them knows what language is being edited. An editor built on it is a command, a `profile.Profile`, and a scanner.
6
7The library was not designed that way. It was extracted from Turbo Go, which had been a single 35,000-line program for a fortnight, at the moment a second editor was wanted. Nearly all of it moved unchanged; the seam is where it turned out to have to be, not where anybody predicted.
8
9## The rule that keeps the shape
10
11**The packages holding the interesting logic do not know a terminal exists.**
12
13`buffer`, `jsonrpc`, `lsp`, `syntax` and `profile` are testable by calling functions and comparing values. `acp` splits the same way: its protocol client and its `Transcript` are values in and values out, and only its `View` needs a screen. `ui`, `editor`, `filetree` and `app` are tested through tcell's `SimulationScreen`. `terminal` splits the same way internally: its parser and screen take bytes and return a grid with no process behind them, and that is where most of its tests are.
14
15Dependencies run strictly downwards. There are no cycles, and — this is the part worth defending — no interface indirection introduced to prevent one. Upward communication is by function field (`OnChange`, `OnCursorMove`, `OnCompletionRequest`) rather than by interface, because a field is one line and an interface is a type, an implementation and a name for both.
16
17## Why the profile is a struct and not an interface
18
19The obvious design for "the parts that differ per editor" is an interface: `type Editor interface { Name() string; Server() Server; … }`. It was rejected.
20
21An interface would be implemented once per editor, which is a type and a set of methods that do nothing but return constants. A struct is filled in once per editor, which is a literal. The difference matters most for the third editor, which is the one the whole exercise is for: filling in a literal is something you can do by reading the reference, and implementing an interface is something you do by reading somebody else's implementation.
22
23The struct also makes the *whole* difference visible in one place. `golang.Profile()` is sixty lines and is the entire answer to "what makes this Turbo Go?".
24
25## Why the language scanner lives outside
26
27Eight languages are coloured here — TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell — and the language the editor is *for* is not. Turbo Go registers Go; Turbo Rust registers Rust; neither is known to the library.
28
29The eight that stayed are the ones every editor meets whatever it is for: a project's own configuration is TOML or YAML, its documentation is Markdown, its scripts are shell, its container build a Dockerfile. The one that left is the one that defines the editor. Having Turbo Rust colour Go would not be harmful, exactly — but it would mean the library grew a language every time somebody built an editor, and the first question about a new editor would stop being "what does it register?".
30
31The cost, accepted: a language author writes against an exported scanner toolkit rather than a private one, so `LineScanner` and its eleven methods are public API and cannot be reshaped freely. That is a real constraint and it is the price of the seam being real.
32
33## Why the registry is package-level state
34
35`syntax.Register` writes into a package-level map. That is mutable global state, which is usually a smell.
36
37It is the shape the standard library gives the same problem — `image.RegisterFormat`, `database/sql.Register` — and the reasons transfer: registration happens once at start-up before anything reads it, nothing ever removes an entry, and the alternative is threading a registry value through `editor.View`, `syntax.Cache` and every call site that asks what language a file is.
38
39It is not safe from two goroutines at once, and there is no reason for it to be. Registration belongs in `main`, beside the flags.
40
41## Why `theme` takes a directory and everything else takes a profile
42
43`settings`, `snippets`, `tools` and `lsp` all take a `profile.Profile`, because each of them needs several things from it — a directory, a template, a menu name, a command. `theme` takes a plain `userDir string`, because it needs exactly one thing and a single named argument says what it is.
44
45This looks inconsistent and is deliberate. A package that takes a profile it uses one field of would be a package that depends on the editor's whole identity to answer a question about a directory.
46
47## How it relates to the rest
48
49The editors built on it are the proof the seam is real. Turbo Go's own packages went from fourteen to one; Turbo Rust was built on the library without changing it, and the four defects that surfaced while building it were all in Turbo Rust's own scanner rather than in the library — which is the outcome the extraction was for. Turbo Python, the third, was built against a published tag with no change to the library at all.
50
51## See also
52
53- What is in each package: [packages reference](../reference/packages.md)
54- Where the line falls: [what belongs here](what-belongs-here.md)
55
56## Why jsonrpc was pulled out of lsp
57
58The library talked one JSON-RPC protocol for a long time, and the code for it sat in `lsp` where it belonged. A second protocol — the [Agent Client Protocol](https://agentclientprotocol.com), which `acp` speaks to a coding agent — made that the wrong place, and the difference between the two turned out to be one thing: how a message is marked off from the next. LSP puts an HTTP-style `Content-Length` header in front of each one; ACP separates them with a newline. Everything above that layer is identical.
59
60So `jsonrpc` holds the protocol and takes a `Framer`; `lsp.Framing` and `acp.Framing` are the two implementations of it, each about ten lines. Neither client knows the other exists.
61
62The extraction changed one thing about the protocol layer rather than merely moving it. A language server's questions can all be answered where they arrive: it asks for configuration during start-up, and the client already knows the answer. An agent's cannot — `session/request_permission` is answered by a person looking at a dialog, and opening one belongs to the goroutine that draws. So a request handler is now handed a `*jsonrpc.Request` and returns nothing: it may reply at once, as `lsp` does, or keep the request and reply several turns of an event loop later, from another goroutine.
63
64That is the fourth time this library has reached the same conclusion — autosave, the language server's re-announcement and the terminal's redraws are the others. `PostEvent` is allowed to drop what does not fit, so an event may *cause* a turn of the event loop but must never be the only thing carrying a fact.