turbo-moonbit — summary
A snapshot of the present. Edited in place; the history is in history.md.
What this is
A Turbo C-style terminal IDE for MoonBit, written in Go, built on turbo-core — the library Turbo Go, Turbo Rust and Turbo Python already share. This repository holds the command, the profile that says the editor is for MoonBit, and the MoonBit scanner. Everything else — the event loop, the windows, the dialogs, the themes, the LSP client, the terminal emulator, the project tree, the snippets and tools machinery — is the library's, and none of it is copied here.
Module rickub.com/turbo-editors/turbo-moonbit, requireing turbo-core v0.4.2 from the module proxy with no active replace. The commented-out replace at the bottom of go.mod documents the escape hatch without being one; 01-release.tag.sh refuses to tag a release whose go.mod carries a live one.
Layout
main.go |
flags, the terminal, moonbitlang.Register(), the profile, the loop |
internal/moonbitlang/moonbitlang.go |
Name, Slug, Language, Profile(), Register(), the server directories |
internal/moonbitlang/scan.go |
the dispatcher, comments, attributes, multi-line string lines, package names, what follows a dot |
internal/moonbitlang/literals.go |
the five quoted forms — "…", b"…", re"…", 'c', b'c' |
internal/moonbitlang/words.go |
numbers, keywords, labels, the identifier-case rule |
internal/moonbitlang/*.toml.tmpl |
the three starter files, embedded by templates.go |
diagram_test.go |
holds docs/diagrams/packages.drawio to go list |
internal/moonbitlang/reference_test.go |
holds docs/*/reference/languages.md to the scanner, keyword row included |
docs/{en,fr}/ |
34 pages each (README included), Diátaxis |
demos/ |
three MoonBit projects to open in the editor: hello, shapes, syntax-tour |
How to build, test and measure
make check # fmt, vet, then the whole suite — what a commit should pass
make build # into bin/turbo-moonbit, then check the binary reports its version
make install # build, install onto PATH, report what it found
go test ./... # the moon-lsp tests skip themselves without the toolchain
Quality gate, separate from the tests:
python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .
To build against a turbo-core you have changed but not released:
go work init . ../turbo-core
go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app # must NOT be under pkg/mod
go.work and go.work.sum are gitignored. Everything still builds and still passes while testing the published library instead of your changes, so run that second line.
Decisions in force
- The scanner carries no state at all, and it is the only one in this family that does not. MoonBit has no block comment, no literal that may reach the next line (a newline before a closing quote is an unterminated literal error), a multi-line string is a run of self-contained
#|/$|lines, and an attribute is explicitly one line. Thecarrytype is empty and named rather thanstruct{}inline, so the reasoning has somewhere to live and a future construct that crosses lines has somewhere to go. - The scanner was written against the published lexical grammar, not against examples.
docs.moonbitlang.com/en/latest/language/lexical-conventions.htmlgives every literal production, the keyword list, and the sentence that decides1..=2. Where the scanner departs from it, the departure is named inreference/languages.md. - There is no table of built-in types, and there does not need to be. A
uident"begins with an ASCII uppercase letter" is a lexical rule, so every capitalised name is a type by the same line of code. The cost is that an enum constructor of your own is coloured as a type; nothing in the syntax separatesCircle(1.0)from a type applied to arguments. - The prelude table was read out of
moonbitlang/core/prelude's generated.mbti, not remembered. That is howprintstayed out of it: MoonBit hasprintlnand has never hadprint, and the compiler confirmed it during the tutorial's first draft. - A number's dot is only part of it when a second dot does not follow. Without that,
1..=2reads as the double1.and.=2, and every range in every file is miscoloured. Suffixes are upper case or they are not suffixes. - A name after a dot is looked up without the keyword table, because dot-identifiers "use the identifier case rules without consulting the keyword table, so
.ifis valid". - A labelled argument's
name~isClassAttribute, tilde included. The tilde appears in no MoonBit operator, so it can only be a label — except after a capital or a keyword, which the grammar excludes. - An attribute takes the whole line, because the grammar hands it everything through the next newline.
packageis coloured as a keyword in.mbttoo, although it is only reserved there. It is a real keyword in the.mbtifiles this editor also colours, and in a.mbtthe colour says what the compiler is about to.- The toolchain menu is
~M~oonBit, notmoon. M is free — the fixed menus take F, E, S, R, C, O, W, N and H, which rules out both the O and the N of MoonBit. Named after the language because the menu holds whatever the project put in its tools file. - The language server is
moon-lsp --stdio, and the install hint installs the whole toolchain:curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash. The--stdiois not optional — with no argument moon-lsp prints its usage and exits, which the editor would see as a server that died at once. - moon-lsp answers seven of turbo-core's nine questions. It advertises neither
typeDefinitionnorimplementation, so those two items report nothing found. It does answerworkspace/symbol, which Turbo Python's server does not. A test asserts both halves. - Two directories are searched for the server besides
PATH:$MOON_HOME/binand~/.moon/bin. The MoonBit installer's last act is to append its bin directory to one shell profile, which does nothing for an editor started from a shell that was already open. - Root markers, in order:
moon.mod,moon.mod.json.moon.workis deliberately absent althoughmoonitself looks for it: a workspace manifest only ever sits above amoon.mod, so naming it would make a rare case look like part of the rule. Profile()is a function, not a variable, becauseServer.Dirsreads the environment and a variable would freeze whateverMOON_HOMEsaid at link time.- Snippet bodies are TOML literal multi-line strings — three apostrophes, not three double quotes. MoonBit interpolates with
\{…}, and a backslash before a brace is not one of TOML's escapes, so a body in basic strings would not parse and the file the editor had just offered to create would be refused when read back. - Snippet bodies are indented two spaces, which is what
moon fmtwrites. A snippet that disagrees with the formatter turns one insertion into a whole-file diff. - Nine tools in the starter file,
moon checkfirst because it answers "is this sound?" without producing anything. Three ask for a value and one names a menu of its own — both features are invisible otherwise. autosave = truein the starter settings file,falseinsettings.Default(). Two statements in two places on purpose.- The starter templates are embedded files, not Go constants, with the
.tmplsuffix becausesettings.toml.tmplholdstheme = %q, which is not valid TOML. - The three demo projects must build, run and survive
moon fmtunchanged. They are the only MoonBit in this repository that a compiler ever sees, and they are what caught three things the scanner alone could not:derive(Show)is deprecated in favour ofderive(Debug), the functionalloop (a, b) { … }form is deprecated in favour offor i = a, j = b { … }, andtypealiasis not accepted at top level by this compiler although it is in the grammar's keyword list. The first two were in the starter snippets file and are now fixed. - Agent windows are turbo-core's, and what belongs here is the starter file.
acp.toml.tmplis the fourth embedded template, andprofile.Templates.Agentsis the whole of Turbo MoonBit's contribution to the feature. The example agent isdocker agent serve acp .turbo-moonbit/agent.yaml; the only other thing about this editor in it is the sentence saying a ```moonbit fence is coloured by the scanner this editor colours its own files with. Every other editor got agent windows the same way — one file, one line. The reasoning, and why it could not have been built here, is indocs/*/explanation/agent-windows.md. - The starter agents file teaches the window's keyboard as well as the format.
Enter,Alt-Enter,Tab,Esc,Ctrl-Wand the copying keys are all in its comments, because a file the editor hands you is the one document a user is guaranteed to see.
State as of 2026-09-09
- Complete and green.
make checkpasses;gofmt -landgo vetare clean. 374 tests and subtests. - Quality gate: PASS, twice. 0 errors, 0 warnings, 0 smells; total complexity 87, worst file 40 against a limit of 60.
- Documentation: 34 files × EN + FR, a
README.mdat the root, anddocs/diagrams/packages.drawiochecked againstgo listbydiagram_test.go. - Every test was falsified. 21 mutations of the scanner and profile, 16 of the templates, 5 of the language-server tests, 8 of the diagram/installer/Makefile checks — all caught. Two genuinely weak tests were found this way and strengthened: one that would have passed with the language list missing from the snippets comment, and one that missed a literal running past its closing quote.
- Verified in a real pty, not only by tests: the bar reading
File Edit Search Run Code Options Window Snippets MoonBit Help;Alt-Mopening the MoonBit menu withCreate tools fileavailable (30;42) andOpen tools filegreyed (90;47), then the two swapping once the file exists; keywords97;44;1, types96;44, functions93;44;1, builtins96;44;1, strings92;44, numbers95;44, comments38;2;143;143;143; a string span covering its\{…}interpolations whole; the About box reading "A Turbo C-style editor for MoonBit, / written in Go.";moon fmtrun from the menu adding a trailing comma and the editor reloading the file; and — the one that matters — a file that does not compile, opened by a relative path, showing×in the gutter (91;44;1) and⚠ The value identifier undefined_name is unbound.on the status bar. - The arrow counts in the tutorial were read off a pty, not counted by hand: five
→from File reaches Options, eight reaches MoonBit. - The server is found outside
PATH—scripts/install.shlocated it at~/.moon/bin/moon-lspand the profile searches the same places. - The editor is registered in the family. turbo-core's
README.md, both docREADMEs,profile/profile.go's package comment, both workspace how-tos and.memory/summary.mdnow count four editors. turbo-core's.gostrings were swept for hardcoded language names; every hit was a false positive, a doc-comment example of the seam, or the true statement that these editors are written in Go. No library change was needed.
Known boundaries
- A
.mbtcreated in the editor is diagnosed from its first save — with turbo-core ≥ v1.0.2. moon-lsp lists a package's files from the directory, so a document it was told is open but never told exists was never diagnosed; turbo-core now sendsworkspace/didChangeWatchedFileswhen a save creates the file. Until 2026-09-19 this was pinned as a limit (TestAFileCreatedAfterTheServerStartedIsNotDiagnosed); that test went red on macOS, where moon-lsp notices new files by itself, and is nowTestAFileCreatedInTheEditorIsDiagnosedFromItsFirstSaveWithRealMoonLSP— which fails against turbo-core v1.0.1 and passes from v1.0.2.enable-completion.mdhad promised this since 2026-09-18. The test helpertypeTextnow sends Enter for a newline; typed as a rune,\nwas dropped and a four-line fixture became one///|comment line. - In
turbo-classicalone,syntax.attributeandsyntax.identifierare both plain yellow, so a MoonBit attribute or label is not told apart from an ordinary name in that one theme. The other seven distinguish them. It is a turbo-core theme matter that hits Turbo Rust's#[derive]equally, so it was not fixed from here. - A non-ASCII identifier is left uncoloured. MoonBit allows CJK and other Unicode ranges in a name; turbo-core's rune predicates are ASCII.
- A string nested inside an interpolation ends the outer literal.
"a \{b} c"is one span, but"a \{f("x")} c"scans as string, thenxas an identifier, then string, because the first unescaped quote is taken as the closer. The grammar says nested literals do not count, so a correct implementation needs the parser. The spans stay ordered and non-overlapping, so nothing downstream misbehaves. Pinned byTestAStringInsideAnInterpolationEndsTheOuterLiteral, anddemos/syntax-tour/tour.mbtcarries a labelled line that shows it.
Not yet established
-
Agent windows have never been opened in this editor. The feature is turbo-core's and was driven end to end from turbo-go against a real
docker agentand a real llama.cpp; what is here is the starter file, covered by tests that create it, load it back and check it names this editor's own language. Nobody has runturbo-moonbit, pressedAlt-Aand talked to an agent from it. -
Run on macOS once (2026-09-19), by the user's
make check, and it found a turbo-core defect: symlinked temp dirs (/var→/private/var) made moon-lsp treat the fixture as a file of no package. Fixed in turbo-core v1.0.1; this editor must re-pin before releasing. Never run on Windows. -
No CI. There is no pipeline configuration in the repository.
-
Never released. No tag exists;
01–04have only ever been read here, never run. -
Performance on a large file is unmeasured. The scanner is a line at a time and carries nothing, but nothing has been timed.
-
.mbtxscripts are claimed but untested against a real one. The extension is registered and coloured; no.mbtxfile has been opened in the editor. -
Nothing checks the demos automatically. They were built, run and formatted by hand; no test or CI step re-runs
moon checkover them, so a future MoonBit release could deprecate something in them without anything noticing. -
moon.workworkspaces are untested. The root markers deliberately stop at the module, and no multi-module project has been opened.
State as of 2026-09-19 — moved to Rickub, released by a workflow
- Module path
rickub.com/turbo-editors/turbo-moonbit, depending onrickub.com/turbo-editors/turbo-core v1.0.0— the first turbo-core version published under that path (v0.9.0on the proxy still declares the Codeberg path and cannot be required asrickub.com/…). Every import, the Makefile'sVERSION_PKG,scripts/install.sh, the README and the docs sayrickub.com.GOWORK=off make checkgreen. The repository on this side is a freshgit initwithoriginatssh://git@rickub.com/turbo-editors/turbo-moonbit.gitand no commit yet;01-release.tag.shmakes the first one. - Releases are one script and one workflow, modelled on turbo-core's and identical to turbo-go's.
01-release.tag.shrunsmake checkunderTURBO_MOONBIT_RELEASING=1, refuses a tag taken locally or on origin (bump, never move), refuses areplaceingo.mod, commits, pushes the current branch, then tags and pushes the tag. That push starts.github/workflows/release.yml:go testwithTURBO_MOONBIT_RELEASING=1,./02-build-releases.sh "${GITHUB_REF_NAME}", release notes from the tag message, a run artifact, thensoftprops/action-gh-release@v2attachingturbo-moonbit-*,SHA256SUMSandREADME.mdwith the job's ownGITHUB_TOKEN— the only credential Rickub's release API accepts.02-release.publish.shand04-release.upload-binaries.share gone; the build script is now02-build-releases.sh, takes the tag as$1(CI has norelease.env), validates it, refuses areplace, and starts from an emptyrelease/${TAG}/.release.envholds onlyTAGandABOUT;turbo-moonbit.token.envis read by nothing. release_test.goruns01for real against a throwaway bare remote (publishes, then refuses the same tag), runs02alone to see it refusev0.o.0, and asserts the workflow's trigger,contents: write,./02-build-releases.sh,fail_on_unmatched_files, docs linked at the tag, nosecrets., andTURBO_MOONBIT_RELEASING. The copy of the module for the clone leaves out.git,bin,release,kits,demo,demos,*.envandgo.work*; children run withGOWORK=off.
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 |
|