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

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

📦 Turbo MoonBit cc1f595 · on main · k33g · 11h ago
moonbitlang.go · 179 lines · 7.2 KBGo Blame HistoryRaw
  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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// 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")
}