package lsp import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) type LSPServer struct { Name string `json:"name"` Language string `json:"language"` Command string `json:"command"` InstallCmd string `json:"install_cmd"` Installed bool `json:"installed"` Version string `json:"version,omitempty"` Healthy bool `json:"healthy,omitempty"` Description string `json:"description,omitempty"` Category string `json:"category,omitempty"` } var knownServers = []LSPServer{ {Name: "gopls", Language: "go", Command: "gopls", InstallCmd: "go install golang.org/x/tools/gopls@latest"}, {Name: "pyright", Language: "python", Command: "pyright", InstallCmd: "npm install -g pyright"}, {Name: "typescript-language-server", Language: "typescript", Command: "typescript-language-server", InstallCmd: "npm install -g typescript-language-server typescript"}, {Name: "vscode-json-language-server", Language: "json", Command: "vscode-json-language-server", InstallCmd: "npm install -g vscode-langservers-extracted"}, {Name: "vscode-html-language-server", Language: "html", Command: "vscode-html-language-server", InstallCmd: "npm install -g vscode-langservers-extracted"}, {Name: "vscode-css-language-server", Language: "css", Command: "vscode-css-language-server", InstallCmd: "npm install -g vscode-langservers-extracted"}, {Name: "yaml-language-server", Language: "yaml", Command: "yaml-language-server", InstallCmd: "npm install -g yaml-language-server"}, {Name: "bash-language-server", Language: "bash", Command: "bash-language-server", InstallCmd: "npm install -g bash-language-server"}, {Name: "rust-analyzer", Language: "rust", Command: "rust-analyzer", InstallCmd: "rustup component add rust-analyzer"}, {Name: "clangd", Language: "c/c++", Command: "clangd", InstallCmd: ""}, {Name: "lua-language-server", Language: "lua", Command: "lua-language-server", InstallCmd: "npm install -g lua-language-server"}, {Name: "dockerfile-language-server", Language: "dockerfile", Command: "docker-langserver", InstallCmd: "npm install -g dockerfile-language-server-nodejs"}, {Name: "tailwindcss-language-server", Language: "tailwind", Command: "tailwindcss-language-server", InstallCmd: "npm install -g @tailwindcss/language-server"}, {Name: "svelte-language-server", Language: "svelte", Command: "svelteserver", InstallCmd: "npm install -g svelte-language-server"}, {Name: "vue-language-server", Language: "vue", Command: "vue-language-server", InstallCmd: "npm install -g @vue/language-server"}, {Name: "golangci-lint-langserver", Language: "go-lint", Command: "golangci-lint-langserver", InstallCmd: "go install github.com/nametake/golangci-lint-langserver@latest"}, } func ScanServers() []LSPServer { servers := make([]LSPServer, len(knownServers)) for i, s := range knownServers { servers[i] = s _, err := exec.LookPath(s.Command) servers[i].Installed = err == nil servers[i].Version = getInstalledLSPVersion(s.Name) } regServers, err := scanLSPRegistryServers() if err == nil { servers = append(servers, regServers...) } return servers } func scanLSPRegistryServers() ([]LSPServer, error) { reg, err := LoadLSPRegistry() if err != nil { return nil, err } knownNames := map[string]bool{} for _, s := range knownServers { knownNames[s.Name] = true } var servers []LSPServer for _, rs := range reg.Servers { if knownNames[rs.Name] { continue } servers = append(servers, LSPServer{ Name: rs.Name, Language: rs.Language, Command: rs.Command, InstallCmd: rs.InstallCmd, Installed: isLSPCommandAvailable(rs.Command), Description: rs.Description, Category: rs.Category, Version: getInstalledLSPVersion(rs.Name), }) } return servers, nil } func isLSPCommandAvailable(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil } func getInstalledLSPVersion(name string) string { home, _ := os.UserHomeDir() if home == "" { return "" } receiptPath := filepath.Join(home, ".muyue", "receipts", "lsp", name+".json") data, err := os.ReadFile(receiptPath) if err != nil { return "" } var receipt struct { Version string `json:"version"` } if json.Unmarshal(data, &receipt) == nil { return receipt.Version } return "" } func saveLSPReceipt(name, version string) error { home, _ := os.UserHomeDir() if home == "" { return nil } receiptDir := filepath.Join(home, ".muyue", "receipts", "lsp") os.MkdirAll(receiptDir, 0755) receipt := struct { Name string `json:"name"` Version string `json:"version"` UpdatedAt string `json:"updated_at"` }{ Name: name, Version: version, UpdatedAt: time.Now().Format(time.RFC3339), } data, _ := json.MarshalIndent(receipt, "", " ") return os.WriteFile(filepath.Join(receiptDir, name+".json"), data, 0644) } func InstallServer(name string) error { for _, s := range knownServers { if s.Name == name { return doInstallLSP(s) } } reg, err := LoadLSPRegistry() if err == nil { for _, s := range reg.Servers { if s.Name == name { return doInstallLSP(LSPServer{ Name: s.Name, Language: s.Language, Command: s.Command, InstallCmd: s.InstallCmd, }) } } } return fmt.Errorf("unknown LSP server: %s", name) } func doInstallLSP(s LSPServer) error { if s.InstallCmd == "" { return fmt.Errorf("%s has no auto-install command, install manually", s.Name) } cmd := exec.Command("bash", "-c", s.InstallCmd) cmd.Env = os.Environ() if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("install %s: %s: %w", s.Name, string(output), err) } saveLSPReceipt(s.Name, "latest") return nil } func InstallForLanguages(languages []string) []LSPServer { langMap := map[string][]string{ "go": {"gopls"}, "typescript": {"typescript-language-server", "vscode-json-language-server", "vscode-html-language-server", "vscode-css-language-server"}, "javascript": {"typescript-language-server", "vscode-json-language-server", "vscode-html-language-server", "vscode-css-language-server"}, "python": {"pyright"}, "rust": {"rust-analyzer"}, "c": {"clangd"}, "cpp": {"clangd"}, "json": {"vscode-json-language-server"}, "yaml": {"yaml-language-server"}, "bash": {"bash-language-server"}, "html": {"vscode-html-language-server"}, "css": {"vscode-css-language-server"}, "lua": {"lua-language-server"}, "docker": {"dockerfile-language-server"}, "svelte": {"svelte-language-server"}, "vue": {"vue-language-server"}, } installed := map[string]bool{} var results []LSPServer for _, lang := range languages { if servers, ok := langMap[lang]; ok { for _, srv := range servers { if installed[srv] { continue } installed[srv] = true if err := InstallServer(srv); err != nil { results = append(results, LSPServer{Name: srv, Language: lang, Installed: false}) } else { results = append(results, LSPServer{Name: srv, Language: lang, Installed: true}) } } } } return results } func AutoInstallForProject(projectDir string) ([]LSPServer, error) { languages := DetectProjectLanguages(projectDir) if len(languages) == 0 { return nil, nil } results := InstallForLanguages(languages) return results, nil } func HealthCheck(name string) (bool, string) { for _, s := range knownServers { if s.Name == name { return healthCheckServer(s) } } return false, "unknown server" } func healthCheckServer(s LSPServer) (bool, string) { path, err := exec.LookPath(s.Command) if err != nil { return false, fmt.Sprintf("command %q not found in PATH", s.Command) } versionArgs := map[string][]string{ "gopls": {"version"}, "pyright": {"--version"}, "typescript-language-server": {"--version"}, "rust-analyzer": {"--version"}, "clangd": {"--version"}, "lua-language-server": {"--version"}, "bash-language-server": {"--version"}, "yaml-language-server": {"--version"}, } if args, ok := versionArgs[s.Command]; ok { cmd := exec.Command(path, args...) output, err := cmd.CombinedOutput() if err != nil { return true, fmt.Sprintf("installed at %s but version check failed", path) } version := strings.TrimSpace(string(output)) if idx := strings.Index(version, "\n"); idx > 0 { version = version[:idx] } saveLSPReceipt(s.Name, version) return true, version } return true, fmt.Sprintf("installed at %s", path) } func GenerateEditorConfigs(servers []LSPServer, editor string, homeDir string) (string, error) { if homeDir == "" { home, _ := os.UserHomeDir() homeDir = home } reg, err := LoadLSPRegistry() if err != nil { return "", err } regMap := map[string]RegistryEntry{} for _, s := range reg.Servers { regMap[s.Name] = s } var regEntries []RegistryEntry for _, s := range servers { if re, ok := regMap[s.Name]; ok { regEntries = append(regEntries, re) } } switch editor { case "neovim", "nvim": return GenerateNeovimConfig(regEntries), nil case "helix", "hx": return GenerateHelixConfig(regEntries), nil case "vscode", "code", "cursor": exts := GenerateVSCodeRecommendations(regEntries) var b strings.Builder b.WriteString("{\n \"recommendations\": [\n") for i, ext := range exts { if i > 0 { b.WriteString(",\n") } b.WriteString(" \"" + ext + "\"") } b.WriteString("\n ]\n}") return b.String(), nil default: return "", fmt.Errorf("unsupported editor: %s", editor) } }