| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- package discovery
- import (
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
- "log/slog"
- )
- // ServerInfo contains information about an LSP server
- type ServerInfo struct {
- // Command to run the server
- Command string
- // Arguments to pass to the command
- Args []string
- // Command to install the server (for user guidance)
- InstallCmd string
- // Whether this server is available
- Available bool
- // Full path to the executable (if found)
- Path string
- }
- // LanguageServerMap maps language IDs to their corresponding LSP servers
- var LanguageServerMap = map[string]ServerInfo{
- "go": {
- Command: "gopls",
- InstallCmd: "go install golang.org/x/tools/gopls@latest",
- },
- "typescript": {
- Command: "typescript-language-server",
- Args: []string{"--stdio"},
- InstallCmd: "npm install -g typescript-language-server typescript",
- },
- "javascript": {
- Command: "typescript-language-server",
- Args: []string{"--stdio"},
- InstallCmd: "npm install -g typescript-language-server typescript",
- },
- "python": {
- Command: "pylsp",
- InstallCmd: "pip install python-lsp-server",
- },
- "rust": {
- Command: "rust-analyzer",
- InstallCmd: "rustup component add rust-analyzer",
- },
- "java": {
- Command: "jdtls",
- InstallCmd: "Install Eclipse JDT Language Server",
- },
- "c": {
- Command: "clangd",
- InstallCmd: "Install clangd from your package manager",
- },
- "cpp": {
- Command: "clangd",
- InstallCmd: "Install clangd from your package manager",
- },
- "php": {
- Command: "intelephense",
- Args: []string{"--stdio"},
- InstallCmd: "npm install -g intelephense",
- },
- "ruby": {
- Command: "solargraph",
- Args: []string{"stdio"},
- InstallCmd: "gem install solargraph",
- },
- "lua": {
- Command: "lua-language-server",
- InstallCmd: "Install lua-language-server from your package manager",
- },
- "html": {
- Command: "vscode-html-language-server",
- Args: []string{"--stdio"},
- InstallCmd: "npm install -g vscode-langservers-extracted",
- },
- "css": {
- Command: "vscode-css-language-server",
- Args: []string{"--stdio"},
- InstallCmd: "npm install -g vscode-langservers-extracted",
- },
- "json": {
- Command: "vscode-json-language-server",
- Args: []string{"--stdio"},
- InstallCmd: "npm install -g vscode-langservers-extracted",
- },
- "yaml": {
- Command: "yaml-language-server",
- Args: []string{"--stdio"},
- InstallCmd: "npm install -g yaml-language-server",
- },
- }
- // FindLSPServer searches for an LSP server for the given language
- func FindLSPServer(languageID string) (ServerInfo, error) {
- // Get server info for the language
- serverInfo, exists := LanguageServerMap[languageID]
- if !exists {
- return ServerInfo{}, fmt.Errorf("no LSP server defined for language: %s", languageID)
- }
- // Check if the command is in PATH
- path, err := exec.LookPath(serverInfo.Command)
- if err == nil {
- serverInfo.Available = true
- serverInfo.Path = path
- slog.Debug("Found LSP server in PATH", "language", languageID, "command", serverInfo.Command, "path", path)
- return serverInfo, nil
- }
- // If not in PATH, search in common installation locations
- paths := getCommonLSPPaths(languageID, serverInfo.Command)
- for _, searchPath := range paths {
- if _, err := os.Stat(searchPath); err == nil {
- // Found the server
- serverInfo.Available = true
- serverInfo.Path = searchPath
- slog.Debug("Found LSP server in common location", "language", languageID, "command", serverInfo.Command, "path", searchPath)
- return serverInfo, nil
- }
- }
- // Server not found
- slog.Debug("LSP server not found", "language", languageID, "command", serverInfo.Command)
- return serverInfo, fmt.Errorf("LSP server for %s not found. Install with: %s", languageID, serverInfo.InstallCmd)
- }
- // getCommonLSPPaths returns common installation paths for LSP servers based on language and OS
- func getCommonLSPPaths(languageID, command string) []string {
- var paths []string
- homeDir, err := os.UserHomeDir()
- if err != nil {
- slog.Error("Failed to get user home directory", "error", err)
- return paths
- }
- // Add platform-specific paths
- switch runtime.GOOS {
- case "darwin":
- // macOS paths
- paths = append(paths,
- fmt.Sprintf("/usr/local/bin/%s", command),
- fmt.Sprintf("/opt/homebrew/bin/%s", command),
- fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
- )
- case "linux":
- // Linux paths
- paths = append(paths,
- fmt.Sprintf("/usr/bin/%s", command),
- fmt.Sprintf("/usr/local/bin/%s", command),
- fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
- )
- case "windows":
- // Windows paths
- paths = append(paths,
- fmt.Sprintf("%s\\AppData\\Local\\Programs\\%s.exe", homeDir, command),
- fmt.Sprintf("C:\\Program Files\\%s\\bin\\%s.exe", command, command),
- )
- }
- // Add language-specific paths
- switch languageID {
- case "go":
- gopath := os.Getenv("GOPATH")
- if gopath == "" {
- gopath = filepath.Join(homeDir, "go")
- }
- paths = append(paths, filepath.Join(gopath, "bin", command))
- if runtime.GOOS == "windows" {
- paths = append(paths, filepath.Join(gopath, "bin", command+".exe"))
- }
- case "typescript", "javascript", "html", "css", "json", "yaml", "php":
- // Node.js global packages
- if runtime.GOOS == "windows" {
- paths = append(paths,
- fmt.Sprintf("%s\\AppData\\Roaming\\npm\\%s.cmd", homeDir, command),
- fmt.Sprintf("%s\\AppData\\Roaming\\npm\\node_modules\\.bin\\%s.cmd", homeDir, command),
- )
- } else {
- paths = append(paths,
- fmt.Sprintf("%s/.npm-global/bin/%s", homeDir, command),
- fmt.Sprintf("%s/.nvm/versions/node/*/bin/%s", homeDir, command),
- fmt.Sprintf("/usr/local/lib/node_modules/.bin/%s", command),
- )
- }
- case "python":
- // Python paths
- if runtime.GOOS == "windows" {
- paths = append(paths,
- fmt.Sprintf("%s\\AppData\\Local\\Programs\\Python\\Python*\\Scripts\\%s.exe", homeDir, command),
- fmt.Sprintf("C:\\Python*\\Scripts\\%s.exe", command),
- )
- } else {
- paths = append(paths,
- fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
- fmt.Sprintf("%s/.pyenv/shims/%s", homeDir, command),
- fmt.Sprintf("/usr/local/bin/%s", command),
- )
- }
- case "rust":
- // Rust paths
- if runtime.GOOS == "windows" {
- paths = append(paths,
- fmt.Sprintf("%s\\.rustup\\toolchains\\*\\bin\\%s.exe", homeDir, command),
- fmt.Sprintf("%s\\.cargo\\bin\\%s.exe", homeDir, command),
- )
- } else {
- paths = append(paths,
- fmt.Sprintf("%s/.rustup/toolchains/*/bin/%s", homeDir, command),
- fmt.Sprintf("%s/.cargo/bin/%s", homeDir, command),
- )
- }
- }
- // Add VSCode extensions path
- vscodePath := getVSCodeExtensionsPath(homeDir)
- if vscodePath != "" {
- paths = append(paths, vscodePath)
- }
- // Expand any glob patterns in paths
- var expandedPaths []string
- for _, path := range paths {
- if strings.Contains(path, "*") {
- // This is a glob pattern, expand it
- matches, err := filepath.Glob(path)
- if err == nil {
- expandedPaths = append(expandedPaths, matches...)
- }
- } else {
- expandedPaths = append(expandedPaths, path)
- }
- }
- return expandedPaths
- }
- // getVSCodeExtensionsPath returns the path to VSCode extensions directory
- func getVSCodeExtensionsPath(homeDir string) string {
- var basePath string
- switch runtime.GOOS {
- case "darwin":
- basePath = filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage")
- case "linux":
- basePath = filepath.Join(homeDir, ".config", "Code", "User", "globalStorage")
- case "windows":
- basePath = filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage")
- default:
- return ""
- }
- // Check if the directory exists
- if _, err := os.Stat(basePath); err != nil {
- return ""
- }
- return basePath
- }
- // ConfigureLSPServers detects languages and configures LSP servers
- func ConfigureLSPServers(rootDir string) (map[string]ServerInfo, error) {
- // Detect languages in the project
- languages, err := DetectLanguages(rootDir)
- if err != nil {
- return nil, fmt.Errorf("failed to detect languages: %w", err)
- }
- // Find LSP servers for detected languages
- servers := make(map[string]ServerInfo)
- for langID, langInfo := range languages {
- // Prioritize primary languages but include all languages that have server definitions
- if !langInfo.IsPrimary && langInfo.FileCount < 3 {
- // Skip non-primary languages with very few files
- slog.Debug("Skipping non-primary language with few files", "language", langID, "files", langInfo.FileCount)
- continue
- }
- // Check if we have a server for this language
- serverInfo, err := FindLSPServer(langID)
- if err != nil {
- slog.Warn("LSP server not found", "language", langID, "error", err)
- continue
- }
- // Add to the map of configured servers
- servers[langID] = serverInfo
- if langInfo.IsPrimary {
- slog.Info("Configured LSP server for primary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
- } else {
- slog.Info("Configured LSP server for secondary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
- }
- }
- return servers, nil
- }
|