|
@@ -0,0 +1,306 @@
|
|
|
|
|
+package discovery
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "os/exec"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "runtime"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/opencode-ai/opencode/internal/logging"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// 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
|
|
|
|
|
+ logging.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
|
|
|
|
|
+ logging.Debug("Found LSP server in common location", "language", languageID, "command", serverInfo.Command, "path", searchPath)
|
|
|
|
|
+ return serverInfo, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Server not found
|
|
|
|
|
+ logging.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 {
|
|
|
|
|
+ logging.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
|
|
|
|
|
+ logging.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 {
|
|
|
|
|
+ logging.Warn("LSP server not found", "language", langID, "error", err)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Add to the map of configured servers
|
|
|
|
|
+ servers[langID] = serverInfo
|
|
|
|
|
+ if langInfo.IsPrimary {
|
|
|
|
|
+ logging.Info("Configured LSP server for primary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ logging.Info("Configured LSP server for secondary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return servers, nil
|
|
|
|
|
+}
|