// Package moonbitlang is everything about Turbo MoonBit that is about // *MoonBit*: how the editor names itself, which language server it talks to, // what a project's starter files say, and how MoonBit source is coloured. // // Everything else the editor does lives in turbo-core, which knows nothing // about MoonBit. This package is the whole of the difference between Turbo // MoonBit and Turbo Python, which is what makes a fifth editor a matter of // writing one of these rather than forking anything. // // moonbitlang.Register() // teach the library to colour MoonBit // editor := app.New(screen, name, moonbitlang.Profile()) package moonbitlang import ( "os" "path/filepath" "rickub.com/turbo-editors/turbo-core/profile" "rickub.com/turbo-editors/turbo-core/syntax" ) // Name and Slug are what the editor calls itself. The slug is also its binary, // its project directory (as .turbo-moonbit) and the stem of its environment // variables (as TURBO_MOONBIT_…), so it is not free to change. const ( Name = "Turbo MoonBit" Slug = "turbo-moonbit" ) // Language is the name MoonBit is known by: the value LanguageOf returns for a // .mbt file, and what a snippets file writes in its languages key. // // It is lower case because every other name in the registry is — "moonbit" // beside "toml" and "dockerfile" — while the language's own spelling, the one // a person reads, is Profile().Language. const Language syntax.Language = "moonbit" // ServerCommand is the language server Turbo MoonBit talks to, and InstallHint // the single command that installs it. // // moon-lsp ships inside the MoonBit toolchain rather than being installed // separately, so the hint installs the whole toolchain: the same one command // puts moon, moonc and moon-lsp in place, and a machine that has moon but not // moon-lsp is not a machine anybody has. // // It answers seven of the nine questions turbo-core asks — completion, hover, // definition, references, the file's symbols and the project's symbols — and // publishes diagnostics unasked. It advertises neither typeDefinition nor // implementation, so those two items report nothing found; that is documented // rather than worked around, and a test asserts it so a future moon-lsp gaining // them is noticed. const ( ServerCommand = "moon-lsp" InstallHint = "curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash" ) // ServerArgs is what moon-lsp is started with. // // The --stdio is not optional: moon-lsp with no argument prints its usage and // exits, which the editor would see as a server that died at once. It is a // function rather than a variable so that no caller can append to the package's // own slice. func ServerArgs() []string { return []string{"--stdio"} } // Profile returns the editor Turbo MoonBit is. // // It is a function rather than a variable because Server.Dirs is worked out // from the environment, and a variable would freeze whatever MOON_HOME said // when the package was linked. func Profile() profile.Profile { return profile.Profile{ Name: Name, Slug: Slug, // The language's own spelling, camel case and all. This is what the // About box and the status bar read out, so it is written the way the // people who made it write it. Language: "MoonBit", // 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 — so the hot key lands on // the first letter of the word, which is the reading that costs nobody // a second glance. The menu is named after the language and not after // moon, because it holds whatever the project put in its tools file, // and the first tools file anybody writes outgrows the language's own // toolchain. ToolsMenu: "~M~oonBit", // moon.mod is what `moon new` writes today; moon.mod.json is the // deprecated JSON form, still found in every project written before // the change and still understood by the toolchain. The nearest one // going up is the directory the server is started in. // // moon.work — the multi-module workspace manifest — is deliberately // absent, although moon itself looks for it. It only ever sits *above* // a moon.mod, so it could only be reached for a file lying loose in a // workspace root, and naming it would make that rare case look like // part of the rule. RootMarkers: []string{"moon.mod", "moon.mod.json"}, Server: profile.Server{ Command: ServerCommand, Args: ServerArgs(), InstallHint: InstallHint, Dirs: ServerDirs(), }, Templates: profile.Templates{ Settings: settingsTemplate, Snippets: snippetsTemplate, Tools: toolsTemplate, Agents: agentsTemplate, }, } } // Register teaches turbo-core to colour MoonBit. // // It is called explicitly at start-up rather than from an init function so that // "which languages does this editor know?" is answered by reading main, not by // working out which packages were imported. // // No shebang is claimed. MoonBit has no interpreter line: a file opening with // #! would lex as an attribute named ! and fail, so a file with no extension is // not MoonBit, and guessing otherwise would take a shell script away from the // scanner that can actually colour it. func Register() { syntax.Register(syntax.Definition{ Language: Language, // .mbt is source; .mbti is a generated interface file, which is // MoonBit and nothing else; .mbtx is a standalone script, which needs // no module or package file at all. // // .mbt.md is deliberately not here, and could not be: it is a Markdown // document with MoonBit in its fences, its extension is .md, and // Markdown is what should colour it. Extensions: []string{".mbt", ".mbti", ".mbtx"}, Highlight: Highlight, }) } // ServerDirs returns the directories moon-lsp is looked for in after PATH. // // "Completion silently does nothing" is what a user sees when the editor cannot // find a server they believe they installed, and the MoonBit installer's own // last act is to append its bin directory to a shell profile — which does // nothing for an editor started from a shell that was already open, or from a // desktop launcher that reads no profile at all. // // Empty entries are skipped by the library, so a machine with MOON_HOME unset // simply contributes the default alone. func ServerDirs() []string { dirs := []string{MoonHomeBinDir(), DefaultMoonBinDir()} if dirs[0] == dirs[1] { return dirs[:1] } return dirs } // MoonHomeBinDir returns $MOON_HOME/bin, or "" when MOON_HOME is not set. // // MOON_HOME is what the installer itself honours when deciding where to put the // toolchain, so a user who set it has the whole toolchain somewhere this is the // only way to find. func MoonHomeBinDir() string { home := os.Getenv("MOON_HOME") if home == "" { return "" } return filepath.Join(home, "bin") } // DefaultMoonBinDir returns ~/.moon/bin, where the installer puts the toolchain // when MOON_HOME says nothing. // // It is the directory the install hint's command writes into, so it is the one // that matters most to somebody who followed the hint and found nothing. func DefaultMoonBinDir() string { home, err := os.UserHomeDir() if err != nil { return "" } return filepath.Join(home, ".moon", "bin") }