diff options
| author | adamdottv <[email protected]> | 2025-05-29 15:10:44 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-05-29 15:10:44 -0500 |
| commit | 37c0c1f358cadbc918319500cd2b1b3fcbe41a9e (patch) | |
| tree | f012aad2481ea47b062c630420ecc008287a65dc /internal/lsp/discovery | |
| parent | 2a132f86d687be767df4a7657e9c4441b8a4058d (diff) | |
| download | opencode-37c0c1f358cadbc918319500cd2b1b3fcbe41a9e.tar.gz opencode-37c0c1f358cadbc918319500cd2b1b3fcbe41a9e.zip | |
wip: refactoring tui
Diffstat (limited to 'internal/lsp/discovery')
| -rw-r--r-- | internal/lsp/discovery/integration.go | 65 | ||||
| -rw-r--r-- | internal/lsp/discovery/language.go | 298 | ||||
| -rw-r--r-- | internal/lsp/discovery/server.go | 306 |
3 files changed, 0 insertions, 669 deletions
diff --git a/internal/lsp/discovery/integration.go b/internal/lsp/discovery/integration.go deleted file mode 100644 index a2043b53e..000000000 --- a/internal/lsp/discovery/integration.go +++ /dev/null @@ -1,65 +0,0 @@ -package discovery - -import ( - "fmt" - - "github.com/sst/opencode/internal/config" - "log/slog" -) - -// IntegrateLSPServers discovers languages and LSP servers and integrates them into the application configuration -func IntegrateLSPServers(workingDir string) error { - // Get the current configuration - cfg := config.Get() - if cfg == nil { - return fmt.Errorf("config not loaded") - } - - // Check if this is the first run - shouldInit, err := config.ShouldShowInitDialog() - if err != nil { - return fmt.Errorf("failed to check initialization status: %w", err) - } - - // Always run language detection, but log differently for first run vs. subsequent runs - if shouldInit || len(cfg.LSP) == 0 { - slog.Info("Running initial LSP auto-discovery...") - } else { - slog.Debug("Running LSP auto-discovery to detect new languages...") - } - - // Configure LSP servers - servers, err := ConfigureLSPServers(workingDir) - if err != nil { - return fmt.Errorf("failed to configure LSP servers: %w", err) - } - - // Update the configuration with discovered servers - for langID, serverInfo := range servers { - // Skip languages that already have a configured server - if _, exists := cfg.LSP[langID]; exists { - slog.Debug("LSP server already configured for language", "language", langID) - continue - } - - if serverInfo.Available { - // Only add servers that were found - cfg.LSP[langID] = config.LSPConfig{ - Disabled: false, - Command: serverInfo.Path, - Args: serverInfo.Args, - } - slog.Info("Added LSP server to configuration", - "language", langID, - "command", serverInfo.Command, - "path", serverInfo.Path) - } else { - slog.Warn("LSP server not available", - "language", langID, - "command", serverInfo.Command, - "installCmd", serverInfo.InstallCmd) - } - } - - return nil -} diff --git a/internal/lsp/discovery/language.go b/internal/lsp/discovery/language.go deleted file mode 100644 index 25fe17d55..000000000 --- a/internal/lsp/discovery/language.go +++ /dev/null @@ -1,298 +0,0 @@ -package discovery - -import ( - "os" - "path/filepath" - "strings" - "sync" - - "github.com/sst/opencode/internal/lsp" - "log/slog" -) - -// LanguageInfo stores information about a detected language -type LanguageInfo struct { - // Language identifier (e.g., "go", "typescript", "python") - ID string - - // Number of files detected for this language - FileCount int - - // Project files associated with this language (e.g., go.mod, package.json) - ProjectFiles []string - - // Whether this is likely a primary language in the project - IsPrimary bool -} - -// ProjectFile represents a project configuration file -type ProjectFile struct { - // File name or pattern to match - Name string - - // Associated language ID - LanguageID string - - // Whether this file strongly indicates the language is primary - IsPrimary bool -} - -// Common project files that indicate specific languages -var projectFilePatterns = []ProjectFile{ - {Name: "go.mod", LanguageID: "go", IsPrimary: true}, - {Name: "go.sum", LanguageID: "go", IsPrimary: false}, - {Name: "package.json", LanguageID: "javascript", IsPrimary: true}, // Could be TypeScript too - {Name: "tsconfig.json", LanguageID: "typescript", IsPrimary: true}, - {Name: "jsconfig.json", LanguageID: "javascript", IsPrimary: true}, - {Name: "pyproject.toml", LanguageID: "python", IsPrimary: true}, - {Name: "setup.py", LanguageID: "python", IsPrimary: true}, - {Name: "requirements.txt", LanguageID: "python", IsPrimary: true}, - {Name: "Cargo.toml", LanguageID: "rust", IsPrimary: true}, - {Name: "Cargo.lock", LanguageID: "rust", IsPrimary: false}, - {Name: "CMakeLists.txt", LanguageID: "cmake", IsPrimary: true}, - {Name: "pom.xml", LanguageID: "java", IsPrimary: true}, - {Name: "build.gradle", LanguageID: "java", IsPrimary: true}, - {Name: "build.gradle.kts", LanguageID: "kotlin", IsPrimary: true}, - {Name: "composer.json", LanguageID: "php", IsPrimary: true}, - {Name: "Gemfile", LanguageID: "ruby", IsPrimary: true}, - {Name: "Rakefile", LanguageID: "ruby", IsPrimary: true}, - {Name: "mix.exs", LanguageID: "elixir", IsPrimary: true}, - {Name: "rebar.config", LanguageID: "erlang", IsPrimary: true}, - {Name: "dune-project", LanguageID: "ocaml", IsPrimary: true}, - {Name: "stack.yaml", LanguageID: "haskell", IsPrimary: true}, - {Name: "cabal.project", LanguageID: "haskell", IsPrimary: true}, - {Name: "Makefile", LanguageID: "make", IsPrimary: false}, - {Name: "Dockerfile", LanguageID: "dockerfile", IsPrimary: false}, -} - -// Map of file extensions to language IDs -var extensionToLanguage = map[string]string{ - ".go": "go", - ".js": "javascript", - ".jsx": "javascript", - ".ts": "typescript", - ".tsx": "typescript", - ".py": "python", - ".rs": "rust", - ".java": "java", - ".c": "c", - ".cpp": "cpp", - ".h": "c", - ".hpp": "cpp", - ".rb": "ruby", - ".php": "php", - ".cs": "csharp", - ".fs": "fsharp", - ".swift": "swift", - ".kt": "kotlin", - ".scala": "scala", - ".hs": "haskell", - ".ml": "ocaml", - ".ex": "elixir", - ".exs": "elixir", - ".erl": "erlang", - ".lua": "lua", - ".r": "r", - ".sh": "shell", - ".bash": "shell", - ".zsh": "shell", - ".html": "html", - ".css": "css", - ".scss": "scss", - ".sass": "sass", - ".less": "less", - ".json": "json", - ".xml": "xml", - ".yaml": "yaml", - ".yml": "yaml", - ".md": "markdown", - ".dart": "dart", -} - -// Directories to exclude from scanning -var excludedDirs = map[string]bool{ - ".git": true, - "node_modules": true, - "vendor": true, - "dist": true, - "build": true, - "target": true, - ".idea": true, - ".vscode": true, - ".github": true, - ".gitlab": true, - "__pycache__": true, - ".next": true, - ".nuxt": true, - "venv": true, - "env": true, - ".env": true, -} - -// DetectLanguages scans a directory to identify programming languages used in the project -func DetectLanguages(rootDir string) (map[string]LanguageInfo, error) { - languages := make(map[string]LanguageInfo) - var mutex sync.Mutex - - // Walk the directory tree - err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil // Skip files that can't be accessed - } - - // Skip excluded directories - if info.IsDir() { - if excludedDirs[info.Name()] || strings.HasPrefix(info.Name(), ".") { - return filepath.SkipDir - } - return nil - } - - // Skip hidden files - if strings.HasPrefix(info.Name(), ".") { - return nil - } - - // Check for project files - for _, pattern := range projectFilePatterns { - if info.Name() == pattern.Name { - mutex.Lock() - lang, exists := languages[pattern.LanguageID] - if !exists { - lang = LanguageInfo{ - ID: pattern.LanguageID, - FileCount: 0, - ProjectFiles: []string{}, - IsPrimary: pattern.IsPrimary, - } - } - lang.ProjectFiles = append(lang.ProjectFiles, path) - if pattern.IsPrimary { - lang.IsPrimary = true - } - languages[pattern.LanguageID] = lang - mutex.Unlock() - break - } - } - - // Check file extension - ext := strings.ToLower(filepath.Ext(path)) - if langID, ok := extensionToLanguage[ext]; ok { - mutex.Lock() - lang, exists := languages[langID] - if !exists { - lang = LanguageInfo{ - ID: langID, - FileCount: 0, - ProjectFiles: []string{}, - } - } - lang.FileCount++ - languages[langID] = lang - mutex.Unlock() - } - - return nil - }) - - if err != nil { - return nil, err - } - - // Determine primary languages based on file count if not already marked - determinePrimaryLanguages(languages) - - // Log detected languages - for id, info := range languages { - if info.IsPrimary { - slog.Debug("Detected primary language", "language", id, "files", info.FileCount, "projectFiles", len(info.ProjectFiles)) - } else { - slog.Debug("Detected secondary language", "language", id, "files", info.FileCount) - } - } - - return languages, nil -} - -// determinePrimaryLanguages marks languages as primary based on file count -func determinePrimaryLanguages(languages map[string]LanguageInfo) { - // Find the language with the most files - var maxFiles int - for _, info := range languages { - if info.FileCount > maxFiles { - maxFiles = info.FileCount - } - } - - // Mark languages with at least 20% of the max files as primary - threshold := max(maxFiles/5, 5) // At least 5 files to be considered primary - - for id, info := range languages { - if !info.IsPrimary && info.FileCount >= threshold { - info.IsPrimary = true - languages[id] = info - } - } -} - -// GetLanguageIDFromExtension returns the language ID for a given file extension -func GetLanguageIDFromExtension(ext string) string { - ext = strings.ToLower(ext) - if langID, ok := extensionToLanguage[ext]; ok { - return langID - } - return "" -} - -// GetLanguageIDFromProtocol converts a protocol.LanguageKind to our language ID string -func GetLanguageIDFromProtocol(langKind string) string { - // Convert protocol language kind to our language ID - switch langKind { - case "go": - return "go" - case "typescript": - return "typescript" - case "typescriptreact": - return "typescript" - case "javascript": - return "javascript" - case "javascriptreact": - return "javascript" - case "python": - return "python" - case "rust": - return "rust" - case "java": - return "java" - case "c": - return "c" - case "cpp": - return "cpp" - default: - // Try to normalize the language kind - return strings.ToLower(langKind) - } -} - -// GetLanguageIDFromPath determines the language ID from a file path -func GetLanguageIDFromPath(path string) string { - // Check file extension first - ext := filepath.Ext(path) - if langID := GetLanguageIDFromExtension(ext); langID != "" { - return langID - } - - // Check if it's a known project file - filename := filepath.Base(path) - for _, pattern := range projectFilePatterns { - if filename == pattern.Name { - return pattern.LanguageID - } - } - - // Use LSP's detection as a fallback - uri := "file://" + path - langKind := lsp.DetectLanguageID(uri) - return GetLanguageIDFromProtocol(string(langKind)) -} diff --git a/internal/lsp/discovery/server.go b/internal/lsp/discovery/server.go deleted file mode 100644 index 26ed94e18..000000000 --- a/internal/lsp/discovery/server.go +++ /dev/null @@ -1,306 +0,0 @@ -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 -} |
