# Architecture — explanation ## What is this about? turbo-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. The 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. ## The rule that keeps the shape **The packages holding the interesting logic do not know a terminal exists.** `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. Dependencies 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. ## Why the profile is a struct and not an interface The obvious design for "the parts that differ per editor" is an interface: `type Editor interface { Name() string; Server() Server; … }`. It was rejected. An 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. The 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?". ## Why the language scanner lives outside Eight 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. The 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?". The 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. ## Why the registry is package-level state `syntax.Register` writes into a package-level map. That is mutable global state, which is usually a smell. It 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. It is not safe from two goroutines at once, and there is no reason for it to be. Registration belongs in `main`, beside the flags. ## Why `theme` takes a directory and everything else takes a profile `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. This 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. ## How it relates to the rest The 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. ## See also - What is in each package: [packages reference](../reference/packages.md) - Where the line falls: [what belongs here](what-belongs-here.md) ## Why jsonrpc was pulled out of lsp The 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. So `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. The 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. That 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.