summaryrefslogtreecommitdiffhomepage
path: root/internal/lsp/discovery
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-29 15:10:44 -0500
committeradamdottv <[email protected]>2025-05-29 15:10:44 -0500
commit37c0c1f358cadbc918319500cd2b1b3fcbe41a9e (patch)
treef012aad2481ea47b062c630420ecc008287a65dc /internal/lsp/discovery
parent2a132f86d687be767df4a7657e9c4441b8a4058d (diff)
downloadopencode-37c0c1f358cadbc918319500cd2b1b3fcbe41a9e.tar.gz
opencode-37c0c1f358cadbc918319500cd2b1b3fcbe41a9e.zip
wip: refactoring tui
Diffstat (limited to 'internal/lsp/discovery')
-rw-r--r--internal/lsp/discovery/integration.go65
-rw-r--r--internal/lsp/discovery/language.go298
-rw-r--r--internal/lsp/discovery/server.go306
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
-}