turbo-editors/turbo-golopublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-golo.git
git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git

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

📦 Turbo Golo d710c1b · on main · k33g · 11h ago
architecture.md · 111 lines · 10.2 KBmarkdown
Blame HistoryOpen raw

Architecture — explanation

What is this about?

Turbo Golo is a command, a profile and a scanner. Everything else — the editing widget, the windows, the menus, the dialogs, the themes, the terminal emulator, the file tree, the LSP client — is turbo-core, the library every Turbo editor is built on.

This page is about that split: what is here, what is there, and why the line falls where it does.

What is in this repository

main.go                  flags, the terminal, and the wiring
internal/gololang        the whole of what makes this Turbo Golo
  gololang.go            the profile: name, menu, server, where golo is installed
  scan.go                the scanner's dispatcher, comments, what crosses a line
  literals.go            the three quoted forms — "…", """…""" and '…'
  words.go               numbers, keywords, the 157 builtins, the naming conventions
  templates.go           three //go:embed declarations
  *.toml.tmpl            the three starter files a project gets, embedded

About a thousand lines counting the comments, of which some six hundred are the scanner — under five hundred lines of code by qlty's count, and a third of those are the builtin table. There is no internal/app, no internal/ui, no internal/buffer — those exist once, in the library, and all six editors use them unchanged.

What main does

Six things, in this order:

  1. Parses the flags.
  2. Calls gololang.Register(), which teaches the library to colour .golo files and scripts whose first line names golo.
  3. Builds gololang.Profile() — the value that says this editor is Turbo Golo.
  4. Reads .turbo-golo/settings.toml from the working directory, if there is one.
  5. Opens the terminal and hands the screen, the theme name and the profile to app.New.
  6. Starts golo lsp in the directory of the file being edited, and runs the event loop.

That is the whole command. Every decision it makes — which theme wins, which files to open, whether to start a language server — is about this run, not about Golo.

The profile is the seam

profile.Profile{
	Name:        "Turbo Golo",
	Slug:        "turbo-golo",
	Language:    "Golo",
	ToolsMenu:   "~G~olo",
	RootMarkers: nil,
	Server:      profile.Server{Command: "golo", Args: []string{"lsp"}, …},
	Templates:   profile.Templates{Settings: …, Snippets: …, Tools: …},
}

Everything that would otherwise be a hardcoded "turbo-golo", "golo" or ".golo" somewhere in eleven thousand lines is one field here. The library reads them; nothing in the library knows what any of them mean.

Slug carries more than it looks. The binary is turbo-golo, the project directory is .turbo-golo, the user's own configuration lives in ~/.config/turbo-golo, and the environment variables that override it are TURBO_GOLO_THEME_DIR and TURBO_GOLO_SNIPPET_DIR — all derived from that one word.

RootMarkers is the one field that is empty here and full in every sibling. Go has go.mod, Rust Cargo.toml, Python pyproject.toml, MoonBit moon.mod; Golo has no manifest at all. A script is a file and a program is a directory of them, so there is nothing to walk up towards, and the library's ProjectRoot — given no markers — answers with the directory of the file. That is also all golo lsp needs: it answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk.

Why the scanner is here and not in the library

turbo-core colours eight languages itself: TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell. Those are the ones every editor meets whatever it is for — a project's configuration is TOML or YAML, its documentation is Markdown, its scripts are shell, its image build a Dockerfile.

Golo is not one of them, and neither are Go, Rust, Python or MoonBit. The language that defines an editor is registered by that editor, which is why a .mbt file opens as plain text here and a .golo file opens as plain text in Turbo MoonBit.

That could have gone the other way. Putting all six scanners in the library would let any editor colour any of the languages, at no cost in dependencies — a Golo scanner is ordinary Go. It was rejected because it would mean the library grows a language every time somebody builds an editor, and because "what does this editor register?" would stop being the first question about a new one.

Why the scanner was not borrowed from GoloScript

GoloScript is written in Go, and its lexer package is exactly the tokeniser this scanner reimplements. Turbo Go reaches for go/scanner in the same situation, so the question is fair.

The answer is the module's name. GoloScript's go.mod declares module golo, a bare name with no host in it, and Go's module system cannot fetch a module by a path like that from anywhere: importing golo/lexer from another module needs a replace pointing at a checkout beside this one, and a committed replace is what 01-release.tag.sh refuses to release. So the lexer's rules are carried here rather than called — and lexer/lexer.go and token/token.go are the specification the scanner is written against, line for line where it matters. The colouring page names the places where that specification and the interpreter's own parser disagree.

Why the toolchain menu is ~G~olo and not ~g~olo, gogolo or wagolo

The hot key was the easy part. Nine letters are taken by the fixed menus — F, E, S, R, C, O, W, N and H — and G is not one of them, so it lands on the first letter of the word, which costs nobody a second glance. Turbo Rust had no such luck and ended up on Rus~t~.

The name was the real decision, and it went the same way every sibling's did. The menu holds whatever the project put in its tools file, and that is not always the interpreter: GoloScript itself is three binaries — golo, gogolo, wagolo — and the first tools file anybody writes outgrows all three, because a project's commands include containers, databases and a Makefile target somebody added in 2019. A menu called golo holding wagolo build is already a small lie, and one holding docker compose up is a large one. Golo is the language, and the language is what this editor is for.

Why the tests drive the real editor

internal/gololang/editor_test.go builds a whole Turbo Golo on a simulated terminal — app.New(screen, "turbo-classic", gololang.Profile()) — opens a file and checks the colouring, the menu bar and the hot keys. It uses only the library's public API.

That is deliberate. The library's own suite proves the library works; what these tests prove is that this editor is assembled correctly — that Register was called, that the profile reached the menu bar, that a .golo file comes out coloured and a script with a golo shebang does too. A bug where main forgot to register Golo would pass every test in turbo-core.

The same file drives a real golo lsp end to end, ten times over. It writes a script, opens it, starts the server, and then:

  • types a function declaration that exists only in the buffer, then types its first letters on another line and asks for a completion — golo lsp offers keywords and builtins for any file at all, so a completion holding println would prove nothing; one holding a function that is not on disk proves the buffer was sent;
  • asks for the completion of an empty prefix and compares the answer with the scanner's own tables in both directions — every keyword and builtin the scanner colours must be one the server offers, and everything the server offers must be one the scanner knows, which is how the 157-entry builtin table is held to the interpreter rather than to memory;
  • asks for the definition of a call and the hover over it, which comes back with the # comment written above the declaration;
  • asks for the file's symbols;
  • asks for the references of a call — the declaration and every call within the file — and for its implementation, which is the declaration itself, Golo having no interfaces;
  • searches the project for a symbol, including one in a file the editor never opened;
  • opens a file that does not parse and waits for a diagnostic to arrive unasked — the one feature whose failure looks exactly like success, because an editor with no error to show and one that cannot find the error are the same blank gutter;
  • opens a file with a C-style // comment and waits for the lint that says Golo uses #.

A last test pins what golo lsp still cannot do: it does not advertise typeDefinition, the documentation says so, and the test fails if a future golo starts answering — so the page gets revisited rather than quietly going stale. That is how the three tests before it came to exist: until GoloScript v0.2.0 the same test pinned references, implementations and project-wide symbols as refusals, and went red the day the server learnt them.

Rejected alternatives

Forking Turbo MoonBit. The obvious way to get a fifth editor, and the reason the library exists instead: five copies of eleven thousand lines drift within a month, and every fix has to be made five times by somebody who remembers there are five.

A plugin system. Turbo Golo is a Go program that imports a library. There is no dynamic loading and no ABI. Adding one would mean freezing the API of every package in turbo-core rather than of the handful a profile touches.

A configuration file instead of a profile. The profile could have been TOML read at start-up, which would make a new editor a file rather than a program. It would also make the scanner inexpressible, and a half-configurable editor — everything but the colouring — is worse than either whole answer.

Importing GoloScript's lexer. Weighed above: the module cannot be fetched, and a replace cannot be released.

How it relates to the rest

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
# Architecture — explanation

## What is this about?

Turbo Golo is a command, a profile and a scanner. Everything else — the editing widget, the windows, the menus, the dialogs, the themes, the terminal emulator, the file tree, the LSP client — is [turbo-core](https://rickub.com/turbo-editors/turbo-core), the library every Turbo editor is built on.

This page is about that split: what is here, what is there, and why the line falls where it does.

## What is in this repository

```
main.go                  flags, the terminal, and the wiring
internal/gololang        the whole of what makes this Turbo Golo
  gololang.go            the profile: name, menu, server, where golo is installed
  scan.go                the scanner's dispatcher, comments, what crosses a line
  literals.go            the three quoted forms — "…", """…""" and '…'
  words.go               numbers, keywords, the 157 builtins, the naming conventions
  templates.go           three //go:embed declarations
  *.toml.tmpl            the three starter files a project gets, embedded
```

About a thousand lines counting the comments, of which some six hundred are the scanner — under five hundred lines of code by qlty's count, and a third of those are the builtin table. There is no `internal/app`, no `internal/ui`, no `internal/buffer` — those exist once, in the library, and all six editors use them unchanged.

## What `main` does

Six things, in this order:

1. Parses the flags.
2. Calls `gololang.Register()`, which teaches the library to colour `.golo` files and scripts whose first line names `golo`.
3. Builds `gololang.Profile()` — the value that says this editor is Turbo Golo.
4. Reads `.turbo-golo/settings.toml` from the working directory, if there is one.
5. Opens the terminal and hands the screen, the theme name and the profile to `app.New`.
6. Starts `golo lsp` in the directory of the file being edited, and runs the event loop.

That is the whole command. Every decision it makes — which theme wins, which files to open, whether to start a language server — is about *this run*, not about Golo.

## The profile is the seam

```go
profile.Profile{
	Name:        "Turbo Golo",
	Slug:        "turbo-golo",
	Language:    "Golo",
	ToolsMenu:   "~G~olo",
	RootMarkers: nil,
	Server:      profile.Server{Command: "golo", Args: []string{"lsp"}, },
	Templates:   profile.Templates{Settings: , Snippets: , Tools: },
}
```

Everything that would otherwise be a hardcoded `"turbo-golo"`, `"golo"` or `".golo"` somewhere in eleven thousand lines is one field here. The library reads them; nothing in the library knows what any of them mean.

`Slug` carries more than it looks. The binary is `turbo-golo`, the project directory is `.turbo-golo`, the user's own configuration lives in `~/.config/turbo-golo`, and the environment variables that override it are `TURBO_GOLO_THEME_DIR` and `TURBO_GOLO_SNIPPET_DIR` — all derived from that one word.

`RootMarkers` is the one field that is empty here and full in every sibling. Go has `go.mod`, Rust `Cargo.toml`, Python `pyproject.toml`, MoonBit `moon.mod`; Golo has no manifest at all. A script is a file and a program is a directory of them, so there is nothing to walk up towards, and the library's `ProjectRoot` — given no markers — answers with the directory of the file. That is also all `golo lsp` needs: it answers about the file it is given and resolves imports from the modules embedded in the binary, never from disk.

## Why the scanner is here and not in the library

turbo-core colours eight languages itself: TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell. Those are the ones every editor meets whatever it is for — a project's configuration is TOML or YAML, its documentation is Markdown, its scripts are shell, its image build a Dockerfile.

Golo is not one of them, and neither are Go, Rust, Python or MoonBit. The language that *defines* an editor is registered by that editor, which is why a `.mbt` file opens as plain text here and a `.golo` file opens as plain text in Turbo MoonBit.

That could have gone the other way. Putting all six scanners in the library would let any editor colour any of the languages, at no cost in dependencies — a Golo scanner is ordinary Go. It was rejected because it would mean the library grows a language every time somebody builds an editor, and because "what does this editor register?" would stop being the first question about a new one.

## Why the scanner was not borrowed from GoloScript

GoloScript is written in Go, and its `lexer` package is exactly the tokeniser this scanner reimplements. Turbo Go reaches for `go/scanner` in the same situation, so the question is fair.

The answer is the module's name. GoloScript's `go.mod` declares `module golo`, a bare name with no host in it, and Go's module system cannot fetch a module by a path like that from anywhere: importing `golo/lexer` from another module needs a `replace` pointing at a checkout beside this one, and a committed `replace` is what `01-release.tag.sh` refuses to release. So the lexer's rules are carried here rather than called — and `lexer/lexer.go` and `token/token.go` are the specification the scanner is written against, line for line where it matters. The [colouring page](colouring-and-completion.md) names the places where that specification and the interpreter's own parser disagree.

## Why the toolchain menu is `~G~olo` and not `~g~olo`, `gogolo` or `wagolo`

The hot key was the easy part. Nine letters are taken by the fixed menus — F, E, S, R, C, O, W, N and H — and `G` is not one of them, so it lands on the first letter of the word, which costs nobody a second glance. Turbo Rust had no such luck and ended up on `Rus~t~`.

The name was the real decision, and it went the same way every sibling's did. The menu holds whatever the project put in its tools file, and that is not always the interpreter: GoloScript itself is three binaries — `golo`, `gogolo`, `wagolo` — and the first tools file anybody writes outgrows all three, because a project's commands include containers, databases and a `Makefile` target somebody added in 2019. A menu called **golo** holding `wagolo build` is already a small lie, and one holding `docker compose up` is a large one. `Golo` is the language, and the language is what this editor is for.

## Why the tests drive the real editor

`internal/gololang/editor_test.go` builds a whole Turbo Golo on a simulated terminal — `app.New(screen, "turbo-classic", gololang.Profile())` — opens a file and checks the colouring, the menu bar and the hot keys. It uses only the library's public API.

That is deliberate. The library's own suite proves the library works; what these tests prove is that *this editor is assembled correctly* — that `Register` was called, that the profile reached the menu bar, that a `.golo` file comes out coloured and a script with a `golo` shebang does too. A bug where `main` forgot to register Golo would pass every test in turbo-core.

The same file drives a **real `golo lsp`** end to end, ten times over. It writes a script, opens it, starts the server, and then:

- **types a function declaration that exists only in the buffer**, then types its first letters on another line and asks for a completion — `golo lsp` offers keywords and builtins for any file at all, so a completion holding `println` would prove nothing; one holding a function that is not on disk proves the buffer was sent;
- asks for the **completion of an empty prefix** and compares the answer with the scanner's own tables in both directions — every keyword and builtin the scanner colours must be one the server offers, and everything the server offers must be one the scanner knows, which is how the 157-entry builtin table is held to the interpreter rather than to memory;
- asks for the **definition** of a call and the **hover** over it, which comes back with the `#` comment written above the declaration;
- asks for the **file's symbols**;
- asks for the **references** of a call — the declaration and every call within the file — and for its **implementation**, which is the declaration itself, Golo having no interfaces;
- searches the **project for a symbol**, including one in a file the editor never opened;
- opens a file that **does not parse** and waits for a diagnostic to arrive unasked — the one feature whose failure looks exactly like success, because an editor with no error to show and one that cannot find the error are the same blank gutter;
- opens a file with a **C-style `//` comment** and waits for the lint that says Golo uses `#`.

A last test pins what `golo lsp` still *cannot* do: it does not advertise `typeDefinition`, the documentation says so, and the test fails if a future golo starts answering — so the page gets revisited rather than quietly going stale. That is how the three tests before it came to exist: until GoloScript v0.2.0 the same test pinned references, implementations and project-wide symbols as refusals, and went red the day the server learnt them.

## Rejected alternatives

**Forking Turbo MoonBit.** The obvious way to get a fifth editor, and the reason the library exists instead: five copies of eleven thousand lines drift within a month, and every fix has to be made five times by somebody who remembers there are five.

**A plugin system.** Turbo Golo is a Go program that imports a library. There is no dynamic loading and no ABI. Adding one would mean freezing the API of every package in turbo-core rather than of the handful a profile touches.

**A configuration file instead of a profile.** The profile could have been TOML read at start-up, which would make a new editor a file rather than a program. It would also make the scanner inexpressible, and a half-configurable editor — everything but the colouring — is worse than either whole answer.

**Importing GoloScript's lexer.** Weighed above: the module cannot be fetched, and a `replace` cannot be released.

## How it relates to the rest

- What each of the library's packages does: [turbo-core's package reference](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/en/reference/packages.md)
- How the colouring works here: [Colouring and completion](colouring-and-completion.md)
- Why the tools menu is data: [Golo tools](golo-tools.md)
- The decisions that outlived the refactoring: [Design decisions](design-decisions.md)