| 📦 Turbo MoonBit cc1f595 k33g yesterday | 1 | // Package moonbitlang is everything about Turbo MoonBit that is about |
| 2 | // *MoonBit*: how the editor names itself, which language server it talks to, |
| 3 | // what a project's starter files say, and how MoonBit source is coloured. |
| 4 | // |
| 5 | // Everything else the editor does lives in turbo-core, which knows nothing |
| 6 | // about MoonBit. This package is the whole of the difference between Turbo |
| 7 | // MoonBit and Turbo Python, which is what makes a fifth editor a matter of |
| 8 | // writing one of these rather than forking anything. |
| 9 | // |
| 10 | // moonbitlang.Register() // teach the library to colour MoonBit |
| 11 | // editor := app.New(screen, name, moonbitlang.Profile()) |
| 12 | package moonbitlang |
| 13 | |
| 14 | import ( |
| 15 | "os" |
| 16 | "path/filepath" |
| 17 | |
| 18 | "rickub.com/turbo-editors/turbo-core/profile" |
| 19 | "rickub.com/turbo-editors/turbo-core/syntax" |
| 20 | ) |
| 21 | |
| 22 | // Name and Slug are what the editor calls itself. The slug is also its binary, |
| 23 | // its project directory (as .turbo-moonbit) and the stem of its environment |
| 24 | // variables (as TURBO_MOONBIT_…), so it is not free to change. |
| 25 | const ( |
| 26 | Name = "Turbo MoonBit" |
| 27 | Slug = "turbo-moonbit" |
| 28 | ) |
| 29 | |
| 30 | // Language is the name MoonBit is known by: the value LanguageOf returns for a |
| 31 | // .mbt file, and what a snippets file writes in its languages key. |
| 32 | // |
| 33 | // It is lower case because every other name in the registry is — "moonbit" |
| 34 | // beside "toml" and "dockerfile" — while the language's own spelling, the one |
| 35 | // a person reads, is Profile().Language. |
| 36 | const Language syntax.Language = "moonbit" |
| 37 | |
| 38 | // ServerCommand is the language server Turbo MoonBit talks to, and InstallHint |
| 39 | // the single command that installs it. |
| 40 | // |
| 41 | // moon-lsp ships inside the MoonBit toolchain rather than being installed |
| 42 | // separately, so the hint installs the whole toolchain: the same one command |
| 43 | // puts moon, moonc and moon-lsp in place, and a machine that has moon but not |
| 44 | // moon-lsp is not a machine anybody has. |
| 45 | // |
| 46 | // It answers seven of the nine questions turbo-core asks — completion, hover, |
| 47 | // definition, references, the file's symbols and the project's symbols — and |
| 48 | // publishes diagnostics unasked. It advertises neither typeDefinition nor |
| 49 | // implementation, so those two items report nothing found; that is documented |
| 50 | // rather than worked around, and a test asserts it so a future moon-lsp gaining |
| 51 | // them is noticed. |
| 52 | const ( |
| 53 | ServerCommand = "moon-lsp" |
| 54 | InstallHint = "curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash" |
| 55 | ) |
| 56 | |
| 57 | // ServerArgs is what moon-lsp is started with. |
| 58 | // |
| 59 | // The --stdio is not optional: moon-lsp with no argument prints its usage and |
| 60 | // exits, which the editor would see as a server that died at once. It is a |
| 61 | // function rather than a variable so that no caller can append to the package's |
| 62 | // own slice. |
| 63 | func ServerArgs() []string { return []string{"--stdio"} } |
| 64 | |
| 65 | // Profile returns the editor Turbo MoonBit is. |
| 66 | // |
| 67 | // It is a function rather than a variable because Server.Dirs is worked out |
| 68 | // from the environment, and a variable would freeze whatever MOON_HOME said |
| 69 | // when the package was linked. |
| 70 | func Profile() profile.Profile { |
| 71 | return profile.Profile{ |
| 72 | Name: Name, |
| 73 | Slug: Slug, |
| 74 | // The language's own spelling, camel case and all. This is what the |
| 75 | // About box and the status bar read out, so it is written the way the |
| 76 | // people who made it write it. |
| 77 | Language: "MoonBit", |
| 78 | // M is free: the fixed menus take F, E, S, R, C, O, W, N and H — which |
| 79 | // rules out both the O and the N of MoonBit — so the hot key lands on |
| 80 | // the first letter of the word, which is the reading that costs nobody |
| 81 | // a second glance. The menu is named after the language and not after |
| 82 | // moon, because it holds whatever the project put in its tools file, |
| 83 | // and the first tools file anybody writes outgrows the language's own |
| 84 | // toolchain. |
| 85 | ToolsMenu: "~M~oonBit", |
| 86 | // moon.mod is what `moon new` writes today; moon.mod.json is the |
| 87 | // deprecated JSON form, still found in every project written before |
| 88 | // the change and still understood by the toolchain. The nearest one |
| 89 | // going up is the directory the server is started in. |
| 90 | // |
| 91 | // moon.work — the multi-module workspace manifest — is deliberately |
| 92 | // absent, although moon itself looks for it. It only ever sits *above* |
| 93 | // a moon.mod, so it could only be reached for a file lying loose in a |
| 94 | // workspace root, and naming it would make that rare case look like |
| 95 | // part of the rule. |
| 96 | RootMarkers: []string{"moon.mod", "moon.mod.json"}, |
| 97 | Server: profile.Server{ |
| 98 | Command: ServerCommand, |
| 99 | Args: ServerArgs(), |
| 100 | InstallHint: InstallHint, |
| 101 | Dirs: ServerDirs(), |
| 102 | }, |
| 103 | Templates: profile.Templates{ |
| 104 | Settings: settingsTemplate, |
| 105 | Snippets: snippetsTemplate, |
| 106 | Tools: toolsTemplate, |
| 107 | Agents: agentsTemplate, |
| 108 | }, |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // Register teaches turbo-core to colour MoonBit. |
| 113 | // |
| 114 | // It is called explicitly at start-up rather than from an init function so that |
| 115 | // "which languages does this editor know?" is answered by reading main, not by |
| 116 | // working out which packages were imported. |
| 117 | // |
| 118 | // No shebang is claimed. MoonBit has no interpreter line: a file opening with |
| 119 | // #! would lex as an attribute named ! and fail, so a file with no extension is |
| 120 | // not MoonBit, and guessing otherwise would take a shell script away from the |
| 121 | // scanner that can actually colour it. |
| 122 | func Register() { |
| 123 | syntax.Register(syntax.Definition{ |
| 124 | Language: Language, |
| 125 | // .mbt is source; .mbti is a generated interface file, which is |
| 126 | // MoonBit and nothing else; .mbtx is a standalone script, which needs |
| 127 | // no module or package file at all. |
| 128 | // |
| 129 | // .mbt.md is deliberately not here, and could not be: it is a Markdown |
| 130 | // document with MoonBit in its fences, its extension is .md, and |
| 131 | // Markdown is what should colour it. |
| 132 | Extensions: []string{".mbt", ".mbti", ".mbtx"}, |
| 133 | Highlight: Highlight, |
| 134 | }) |
| 135 | } |
| 136 | |
| 137 | // ServerDirs returns the directories moon-lsp is looked for in after PATH. |
| 138 | // |
| 139 | // "Completion silently does nothing" is what a user sees when the editor cannot |
| 140 | // find a server they believe they installed, and the MoonBit installer's own |
| 141 | // last act is to append its bin directory to a shell profile — which does |
| 142 | // nothing for an editor started from a shell that was already open, or from a |
| 143 | // desktop launcher that reads no profile at all. |
| 144 | // |
| 145 | // Empty entries are skipped by the library, so a machine with MOON_HOME unset |
| 146 | // simply contributes the default alone. |
| 147 | func ServerDirs() []string { |
| 148 | dirs := []string{MoonHomeBinDir(), DefaultMoonBinDir()} |
| 149 | if dirs[0] == dirs[1] { |
| 150 | return dirs[:1] |
| 151 | } |
| 152 | return dirs |
| 153 | } |
| 154 | |
| 155 | // MoonHomeBinDir returns $MOON_HOME/bin, or "" when MOON_HOME is not set. |
| 156 | // |
| 157 | // MOON_HOME is what the installer itself honours when deciding where to put the |
| 158 | // toolchain, so a user who set it has the whole toolchain somewhere this is the |
| 159 | // only way to find. |
| 160 | func MoonHomeBinDir() string { |
| 161 | home := os.Getenv("MOON_HOME") |
| 162 | if home == "" { |
| 163 | return "" |
| 164 | } |
| 165 | return filepath.Join(home, "bin") |
| 166 | } |
| 167 | |
| 168 | // DefaultMoonBinDir returns ~/.moon/bin, where the installer puts the toolchain |
| 169 | // when MOON_HOME says nothing. |
| 170 | // |
| 171 | // It is the directory the install hint's command writes into, so it is the one |
| 172 | // that matters most to somebody who followed the hint and found nothing. |
| 173 | func DefaultMoonBinDir() string { |
| 174 | home, err := os.UserHomeDir() |
| 175 | if err != nil { |
| 176 | return "" |
| 177 | } |
| 178 | return filepath.Join(home, ".moon", "bin") |
| 179 | } |