turbo-editors/turbo-pythonpublic Fork 0
6fc62eaf6b5d18531e5b99104320a4614139e566
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-python.git
git clone ssh://git@rickub.com/turbo-editors/turbo-python.git

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

📦 Turbo Python 6fc62ea · on 6fc62eaf6b5d18531e5b99104320a4614139e566 · k33g · 12h ago
architecture.md · 97 lines · 7.4 KBmarkdown
Blame HistoryOpen raw

Architecture — explanation

What is this about?

Turbo Python 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/pythonlang      the whole of what makes this Turbo Python
  pythonlang.go          the profile: name, menu, server, root markers, where pylsp hides
  scan.go                the scanner's dispatcher, comments, decorators
  literals.go            the sixteen spellings of a string literal
  words.go               numbers, keywords, builtins, the naming conventions
  templates.go           three //go:embed declarations
  *.toml.tmpl            the three starter files a project gets, embedded

About eight hundred lines, of which six hundred are the scanner. 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 pythonlang.Register(), which teaches the library to colour .py files.
  3. Builds pythonlang.Profile() — the value that says this editor is Turbo Python.
  4. Reads .turbo-python/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 pylsp in the project root, 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 Python.

The profile is the seam

profile.Profile{
	Name:        "Turbo Python",
	Slug:        "turbo-python",
	Language:    "Python",
	ToolsMenu:   "~P~ython",
	RootMarkers: []string{"pyproject.toml", "setup.py", "setup.cfg"},
	Server:      profile.Server{Command: "pylsp", …},
	Templates:   profile.Templates{Settings: …, Snippets: …, Tools: …},
}

Everything that would otherwise be a hardcoded "turbo-python", "pylsp" or "pyproject.toml" 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-python, the project directory is .turbo-python, the user's own configuration lives in ~/.config/turbo-python, and the environment variables that override it are TURBO_PYTHON_THEME_DIR and TURBO_PYTHON_SNIPPET_DIR — all derived from that one word.

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.

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

That could have gone the other way. Putting all three scanners in the library would let any editor colour any of the languages, at no cost in dependencies — a Python 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 toolchain menu is ~P~ython and not ~U~v

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 P 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 Turbo Rust's did. The menu holds whatever the project put in its tools file, and that is not always uv: the first tools file anybody writes outgrows the language's own toolchain, because a project's commands include containers, databases and a Makefile target somebody added in 2019. A menu called uv holding docker compose up is a lie about what the menu is, in exactly the way the library's own documentation warns about. Python is the language, and the language is what this editor is for.

Why the tests drive the real editor

internal/pythonlang/editor_test.go builds a whole Turbo Python on a simulated terminal — app.New(screen, "turbo-classic", pythonlang.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 .py file comes out coloured. A bug where main forgot to register Python would pass every test in turbo-core.

The same file drives a real pylsp end to end, four times over. It writes a project, opens a file, starts the server, and then:

  • types text that exists only in the buffer and asks for a completion — text already on disk proves nothing, because the server answers from disk for anything it has not been told is open;
  • asks for the references of a name used in three places, which is the answer shape that used to be truncated to one;
  • asks for the file's symbols, which is a different answer shape again;
  • 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.

A fifth test pins what pylsp cannot do: it advertises neither implementation nor workspace/symbol, the documentation says so, and the test fails if a future pylsp starts answering — so the page gets revisited rather than quietly going stale.

Rejected alternatives

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

A plugin system. Turbo Python 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.

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
# Architecture — explanation

## What is this about?

Turbo Python 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/pythonlang      the whole of what makes this Turbo Python
  pythonlang.go          the profile: name, menu, server, root markers, where pylsp hides
  scan.go                the scanner's dispatcher, comments, decorators
  literals.go            the sixteen spellings of a string literal
  words.go               numbers, keywords, builtins, the naming conventions
  templates.go           three //go:embed declarations
  *.toml.tmpl            the three starter files a project gets, embedded
```

About eight hundred lines, of which six hundred are the scanner. 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 `pythonlang.Register()`, which teaches the library to colour `.py` files.
3. Builds `pythonlang.Profile()` — the value that says this editor is Turbo Python.
4. Reads `.turbo-python/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 pylsp in the project root, 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 Python.

## The profile is the seam

```go
profile.Profile{
	Name:        "Turbo Python",
	Slug:        "turbo-python",
	Language:    "Python",
	ToolsMenu:   "~P~ython",
	RootMarkers: []string{"pyproject.toml", "setup.py", "setup.cfg"},
	Server:      profile.Server{Command: "pylsp", },
	Templates:   profile.Templates{Settings: , Snippets: , Tools: },
}
```

Everything that would otherwise be a hardcoded `"turbo-python"`, `"pylsp"` or `"pyproject.toml"` 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-python`, the project directory is `.turbo-python`, the user's own configuration lives in `~/.config/turbo-python`, and the environment variables that override it are `TURBO_PYTHON_THEME_DIR` and `TURBO_PYTHON_SNIPPET_DIR` — all derived from that one word.

## 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.

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

That could have gone the other way. Putting all three scanners in the library would let any editor colour any of the languages, at no cost in dependencies — a Python 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 toolchain menu is `~P~ython` and not `~U~v`

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 `P` 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 Turbo Rust's did. The menu holds whatever the project put in its tools file, and that is not always uv: the first tools file anybody writes outgrows the language's own toolchain, because a project's commands include containers, databases and a `Makefile` target somebody added in 2019. A menu called **uv** holding `docker compose up` is a lie about what the menu is, in exactly the way the library's own documentation warns about. `Python` is the language, and the language is what this editor is for.

## Why the tests drive the real editor

`internal/pythonlang/editor_test.go` builds a whole Turbo Python on a simulated terminal — `app.New(screen, "turbo-classic", pythonlang.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 `.py` file comes out coloured. A bug where `main` forgot to register Python would pass every test in turbo-core.

The same file drives a **real pylsp** end to end, four times over. It writes a project, opens a file, starts the server, and then:

- **types text that exists only in the buffer** and asks for a completion — text already on disk proves nothing, because the server answers from disk for anything it has not been told is open;
- asks for the **references** of a name used in three places, which is the answer shape that used to be truncated to one;
- asks for the **file's symbols**, which is a different answer shape again;
- 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.

A fifth test pins what pylsp *cannot* do: it advertises neither `implementation` nor `workspace/symbol`, the documentation says so, and the test fails if a future pylsp starts answering — so the page gets revisited rather than quietly going stale.

## Rejected alternatives

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

**A plugin system.** Turbo Python 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.

## 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: [Python tools](python-tools.md)
- The decisions that outlived the refactoring: [Design decisions](design-decisions.md)