Files
MuyueWorkspace/internal/lsp/lsp.go
Augustin 44691225e7
All checks were successful
CI / build (push) Successful in 2m41s
refactor: modularize TUI, improve error handling, add CI caching and tests
Split monolithic app.go into focused modules (dashboard, chat, workflow,
config, agents, terminal, commands, handlers). Add proper error handling
for installer commands, proxy pipes, and MCP config parsing. Fix daemon
channel buffer, cap orchestrator history, compile think regex once, and
set HTTP timeouts on preview server. Improve CI with Go module caching,
dependency download step, and test stage with race detection.

😘 Generated with Crush

Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
2026-04-20 19:13:48 +02:00

196 lines
6.9 KiB
Go

package lsp
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/muyue/muyue/internal/config"
)
type LSPServer struct {
Name string `json:"name"`
Language string `json:"language"`
Command string `json:"command"`
InstallCmd string `json:"install_cmd"`
ConfigFile string `json:"config_file"`
Installed bool `json:"installed"`
}
type LSPConfig struct {
Servers []LSPServer `json:"servers"`
}
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
}
return servers
}
func InstallServer(name string) error {
for _, s := range knownServers {
if s.Name == name {
if s.InstallCmd == "" {
return fmt.Errorf("%s has no auto-install command, install manually", 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", name, string(output), err)
}
return nil
}
}
return fmt.Errorf("unknown LSP server: %s", name)
}
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 GenerateCrushConfig(cfg *config.MuyueConfig) error {
if cfg == nil {
return fmt.Errorf("config is nil")
}
configDir, err := config.ConfigDir()
if err != nil {
return err
}
type lspEntry struct {
Command []string `json:"command"`
}
lspConfig := map[string]lspEntry{}
for _, lang := range cfg.Profile.Languages {
switch lang {
case "go":
lspConfig["go"] = lspEntry{Command: []string{"gopls"}}
case "python":
lspConfig["python"] = lspEntry{Command: []string{"pyright-langserver", "--stdio"}}
case "typescript", "javascript":
lspConfig["typescript"] = lspEntry{Command: []string{"typescript-language-server", "--stdio"}}
case "rust":
lspConfig["rust"] = lspEntry{Command: []string{"rust-analyzer"}}
case "c", "cpp":
lspConfig["c"] = lspEntry{Command: []string{"clangd"}}
case "lua":
lspConfig["lua"] = lspEntry{Command: []string{"lua-language-server"}}
}
}
if len(lspConfig) == 0 {
return nil
}
data, err := json.MarshalIndent(lspConfig, "", " ")
if err != nil {
return err
}
lspPath := filepath.Join(configDir, "crush.json")
existing, err := os.ReadFile(lspPath)
if err == nil {
var existingConfig map[string]interface{}
if unmarshalErr := json.Unmarshal(existing, &existingConfig); unmarshalErr == nil {
var newConfig map[string]interface{}
if unmarshalErr2 := json.Unmarshal(data, &newConfig); unmarshalErr2 == nil {
for k, v := range newConfig {
existingConfig[k] = v
}
data, _ = json.MarshalIndent(existingConfig, "", " ")
}
}
}
return os.WriteFile(lspPath, data, 0644)
}
func EnsureCrushConfig(cfg *config.MuyueConfig) error {
configDir, _ := config.ConfigDir()
crusherPath := filepath.Join(configDir, "crush.json")
if _, err := os.Stat(crusherPath); err != nil {
home, _ := os.UserHomeDir()
homeCrush := filepath.Join(home, ".config", "crush", "crush.json")
if _, err := os.Stat(homeCrush); err == nil {
return nil
}
defaultConfig := map[string]interface{}{
"version": "1",
}
data, _ := json.MarshalIndent(defaultConfig, "", " ")
os.MkdirAll(filepath.Dir(crusherPath), 0755)
return os.WriteFile(crusherPath, data, 0644)
}
return nil
}