package moonbitlang_test import ( "os" "path/filepath" "slices" "strings" "testing" "rickub.com/turbo-editors/turbo-core/profile" "rickub.com/turbo-editors/turbo-core/syntax" "rickub.com/turbo-editors/turbo-moonbit/internal/moonbitlang" ) // fixedMenuHotKeys are the hot keys turbo-core's own menus take. The toolchain // menu may not claim one of them, or one of the two would be unreachable from // the keyboard and nothing would say so. var fixedMenuHotKeys = []rune{'F', 'E', 'S', 'R', 'C', 'O', 'W', 'N', 'H'} func TestProfileNamesTheEditor(t *testing.T) { p := moonbitlang.Profile() if p.Name != "Turbo MoonBit" { t.Errorf("Name = %q, want %q", p.Name, "Turbo MoonBit") } if p.Slug != "turbo-moonbit" { t.Errorf("Slug = %q, want %q", p.Slug, "turbo-moonbit") } // The About box and the status bar read this out, so it is the language's // own spelling rather than the registry's lower-case name. if p.Language != "MoonBit" { t.Errorf("Language = %q, want %q", p.Language, "MoonBit") } } func TestSlugDerivesEveryPath(t *testing.T) { p := moonbitlang.Profile() if got := p.ProjectDir(); got != ".turbo-moonbit" { t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-moonbit") } for name, got := range map[string]string{ "DirEnvVar": p.DirEnvVar(), "ThemeDirEnvVar": p.ThemeDirEnvVar(), "SnippetDirEnvVar": p.SnippetDirEnvVar(), } { if !strings.HasPrefix(got, "TURBO_MOONBIT_") { t.Errorf("%s() = %q, want a TURBO_MOONBIT_ prefix", name, got) } } } func TestThemeDirFollowsItsEnvironmentVariable(t *testing.T) { p := moonbitlang.Profile() want := t.TempDir() t.Setenv(p.ThemeDirEnvVar(), want) if got := p.ThemeDir(); got != want { t.Errorf("ThemeDir() = %q, want %q", got, want) } } func TestToolsMenuHotKeyClashesWithNoFixedMenu(t *testing.T) { label := moonbitlang.Profile().ToolsMenu hotKey, ok := hotKeyOf(label) if !ok { t.Fatalf("ToolsMenu = %q, which marks no hot key between tildes", label) } if slices.Contains(fixedMenuHotKeys, hotKey) { t.Errorf("ToolsMenu hot key %q is already taken by a fixed menu", hotKey) } if got := strings.ReplaceAll(label, "~", ""); got != "MoonBit" { t.Errorf("ToolsMenu reads %q once the tildes are removed, want %q", got, "MoonBit") } } // hotKeyOf returns the upper-case letter a menu label marks between tildes. func hotKeyOf(label string) (rune, bool) { open := strings.Index(label, "~") if open < 0 { return 0, false } rest := label[open+1:] shut := strings.Index(rest, "~") if shut != 1 { return 0, false } return []rune(strings.ToUpper(rest))[0], true } func TestRootMarkersAreMoonModules(t *testing.T) { want := []string{"moon.mod", "moon.mod.json"} if got := moonbitlang.Profile().RootMarkers; !slices.Equal(got, want) { t.Errorf("RootMarkers = %v, want %v", got, want) } } func TestServerIsMoonLSPOverStdio(t *testing.T) { server := moonbitlang.Profile().Server if server.Command != "moon-lsp" { t.Errorf("Server.Command = %q, want %q", server.Command, "moon-lsp") } // moon-lsp with no argument prints its usage and exits, which the editor // would see as a server that died at once. if !slices.Contains(server.Args, "--stdio") { t.Errorf("Server.Args = %v, want it to contain --stdio", server.Args) } if server.InstallHint == "" { t.Error("Server.InstallHint is empty; a missing server would say nothing useful") } // It is shown on the status bar, so it has to fit on a narrow line. if len(server.InstallHint) > 72 { t.Errorf("InstallHint is %d characters, too long for a status bar", len(server.InstallHint)) } } func TestServerArgsCannotBeAppendedToByACaller(t *testing.T) { first := moonbitlang.ServerArgs() first = append(first, "--nonsense") if second := moonbitlang.ServerArgs(); slices.Contains(second, "--nonsense") { t.Errorf("ServerArgs() = %v after a caller appended to an earlier result", second) } } func TestServerIsLookedForInMoonHome(t *testing.T) { home := t.TempDir() t.Setenv("MOON_HOME", home) dirs := moonbitlang.Profile().Server.Dirs want := filepath.Join(home, "bin") if !slices.Contains(dirs, want) { t.Errorf("Server.Dirs = %v, want it to contain %q", dirs, want) } } func TestServerDirsFallBackToTheDefaultInstallDirectory(t *testing.T) { t.Setenv("MOON_HOME", "") home, err := os.UserHomeDir() if err != nil { t.Skip("no home directory on this machine") } want := filepath.Join(home, ".moon", "bin") if dirs := moonbitlang.ServerDirs(); !slices.Contains(dirs, want) { t.Errorf("ServerDirs() = %v, want it to contain %q", dirs, want) } } func TestServerDirsDoNotRepeatOneDirectory(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip("no home directory on this machine") } t.Setenv("MOON_HOME", filepath.Join(home, ".moon")) dirs := moonbitlang.ServerDirs() if len(dirs) != 1 { t.Errorf("ServerDirs() = %v with MOON_HOME at the default, want one entry", dirs) } } func TestProfileIsReadAfreshEveryTime(t *testing.T) { // Server.Dirs comes out of the environment, so a package-level variable // would freeze whatever MOON_HOME said when the binary was linked. first := t.TempDir() t.Setenv("MOON_HOME", first) before := moonbitlang.Profile().Server.Dirs second := t.TempDir() t.Setenv("MOON_HOME", second) after := moonbitlang.Profile().Server.Dirs if slices.Equal(before, after) { t.Errorf("Profile().Server.Dirs = %v both times; it did not follow MOON_HOME", after) } } func TestTemplatesAreAllFilledIn(t *testing.T) { templates := moonbitlang.Profile().Templates for name, template := range map[string]string{ "Settings": templates.Settings, "Snippets": templates.Snippets, "Tools": templates.Tools, } { if template == "" { t.Errorf("Templates.%s is empty; the editor would offer to write nothing", name) } } } func TestRegisterTeachesTheLibraryMoonBit(t *testing.T) { moonbitlang.Register() if !slices.Contains(syntax.Registered(), moonbitlang.Language) { t.Fatalf("Registered() = %v, want it to contain %q", syntax.Registered(), moonbitlang.Language) } for _, path := range []string{"main.mbt", "pkg.mbti", "script.mbtx", "DEEP/nested/x.MBT"} { if got := syntax.LanguageOf(path, ""); got != moonbitlang.Language { t.Errorf("LanguageOf(%q) = %q, want %q", path, got, moonbitlang.Language) } } } func TestRegisterLeavesOtherFilesAlone(t *testing.T) { moonbitlang.Register() cases := map[string]syntax.Language{ // A .mbt.md is a Markdown document with MoonBit in its fences. Its // extension is .md, and Markdown is what should colour it. "README.mbt.md": syntax.LanguageMarkdown, "moon.mod.json": syntax.LanguageNone, "Dockerfile": syntax.LanguageDockerfile, "main.py": syntax.LanguageNone, } for path, want := range cases { if got := syntax.LanguageOf(path, ""); got != want { t.Errorf("LanguageOf(%q) = %q, want %q", path, got, want) } } } func TestNoShebangIsClaimed(t *testing.T) { moonbitlang.Register() // MoonBit has no interpreter line: a file opening with #! would lex as an // attribute named ! and fail. Claiming one would take a shell script away // from the scanner that can actually colour it. if got := syntax.LanguageOf("script", "#!/usr/bin/env moon"); got == moonbitlang.Language { t.Errorf("LanguageOf with a moon shebang = %q, want anything but MoonBit", got) } } // A profile is a literal, so this example is the whole of how one is used. func ExampleProfile() { moonbitlang.Register() var p profile.Profile = moonbitlang.Profile() _ = p.ProjectDir() // ".turbo-moonbit" }