server.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. package discovery
  2. import (
  3. "fmt"
  4. "os"
  5. "os/exec"
  6. "path/filepath"
  7. "runtime"
  8. "strings"
  9. "log/slog"
  10. )
  11. // ServerInfo contains information about an LSP server
  12. type ServerInfo struct {
  13. // Command to run the server
  14. Command string
  15. // Arguments to pass to the command
  16. Args []string
  17. // Command to install the server (for user guidance)
  18. InstallCmd string
  19. // Whether this server is available
  20. Available bool
  21. // Full path to the executable (if found)
  22. Path string
  23. }
  24. // LanguageServerMap maps language IDs to their corresponding LSP servers
  25. var LanguageServerMap = map[string]ServerInfo{
  26. "go": {
  27. Command: "gopls",
  28. InstallCmd: "go install golang.org/x/tools/gopls@latest",
  29. },
  30. "typescript": {
  31. Command: "typescript-language-server",
  32. Args: []string{"--stdio"},
  33. InstallCmd: "npm install -g typescript-language-server typescript",
  34. },
  35. "javascript": {
  36. Command: "typescript-language-server",
  37. Args: []string{"--stdio"},
  38. InstallCmd: "npm install -g typescript-language-server typescript",
  39. },
  40. "python": {
  41. Command: "pylsp",
  42. InstallCmd: "pip install python-lsp-server",
  43. },
  44. "rust": {
  45. Command: "rust-analyzer",
  46. InstallCmd: "rustup component add rust-analyzer",
  47. },
  48. "java": {
  49. Command: "jdtls",
  50. InstallCmd: "Install Eclipse JDT Language Server",
  51. },
  52. "c": {
  53. Command: "clangd",
  54. InstallCmd: "Install clangd from your package manager",
  55. },
  56. "cpp": {
  57. Command: "clangd",
  58. InstallCmd: "Install clangd from your package manager",
  59. },
  60. "php": {
  61. Command: "intelephense",
  62. Args: []string{"--stdio"},
  63. InstallCmd: "npm install -g intelephense",
  64. },
  65. "ruby": {
  66. Command: "solargraph",
  67. Args: []string{"stdio"},
  68. InstallCmd: "gem install solargraph",
  69. },
  70. "lua": {
  71. Command: "lua-language-server",
  72. InstallCmd: "Install lua-language-server from your package manager",
  73. },
  74. "html": {
  75. Command: "vscode-html-language-server",
  76. Args: []string{"--stdio"},
  77. InstallCmd: "npm install -g vscode-langservers-extracted",
  78. },
  79. "css": {
  80. Command: "vscode-css-language-server",
  81. Args: []string{"--stdio"},
  82. InstallCmd: "npm install -g vscode-langservers-extracted",
  83. },
  84. "json": {
  85. Command: "vscode-json-language-server",
  86. Args: []string{"--stdio"},
  87. InstallCmd: "npm install -g vscode-langservers-extracted",
  88. },
  89. "yaml": {
  90. Command: "yaml-language-server",
  91. Args: []string{"--stdio"},
  92. InstallCmd: "npm install -g yaml-language-server",
  93. },
  94. }
  95. // FindLSPServer searches for an LSP server for the given language
  96. func FindLSPServer(languageID string) (ServerInfo, error) {
  97. // Get server info for the language
  98. serverInfo, exists := LanguageServerMap[languageID]
  99. if !exists {
  100. return ServerInfo{}, fmt.Errorf("no LSP server defined for language: %s", languageID)
  101. }
  102. // Check if the command is in PATH
  103. path, err := exec.LookPath(serverInfo.Command)
  104. if err == nil {
  105. serverInfo.Available = true
  106. serverInfo.Path = path
  107. slog.Debug("Found LSP server in PATH", "language", languageID, "command", serverInfo.Command, "path", path)
  108. return serverInfo, nil
  109. }
  110. // If not in PATH, search in common installation locations
  111. paths := getCommonLSPPaths(languageID, serverInfo.Command)
  112. for _, searchPath := range paths {
  113. if _, err := os.Stat(searchPath); err == nil {
  114. // Found the server
  115. serverInfo.Available = true
  116. serverInfo.Path = searchPath
  117. slog.Debug("Found LSP server in common location", "language", languageID, "command", serverInfo.Command, "path", searchPath)
  118. return serverInfo, nil
  119. }
  120. }
  121. // Server not found
  122. slog.Debug("LSP server not found", "language", languageID, "command", serverInfo.Command)
  123. return serverInfo, fmt.Errorf("LSP server for %s not found. Install with: %s", languageID, serverInfo.InstallCmd)
  124. }
  125. // getCommonLSPPaths returns common installation paths for LSP servers based on language and OS
  126. func getCommonLSPPaths(languageID, command string) []string {
  127. var paths []string
  128. homeDir, err := os.UserHomeDir()
  129. if err != nil {
  130. slog.Error("Failed to get user home directory", "error", err)
  131. return paths
  132. }
  133. // Add platform-specific paths
  134. switch runtime.GOOS {
  135. case "darwin":
  136. // macOS paths
  137. paths = append(paths,
  138. fmt.Sprintf("/usr/local/bin/%s", command),
  139. fmt.Sprintf("/opt/homebrew/bin/%s", command),
  140. fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
  141. )
  142. case "linux":
  143. // Linux paths
  144. paths = append(paths,
  145. fmt.Sprintf("/usr/bin/%s", command),
  146. fmt.Sprintf("/usr/local/bin/%s", command),
  147. fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
  148. )
  149. case "windows":
  150. // Windows paths
  151. paths = append(paths,
  152. fmt.Sprintf("%s\\AppData\\Local\\Programs\\%s.exe", homeDir, command),
  153. fmt.Sprintf("C:\\Program Files\\%s\\bin\\%s.exe", command, command),
  154. )
  155. }
  156. // Add language-specific paths
  157. switch languageID {
  158. case "go":
  159. gopath := os.Getenv("GOPATH")
  160. if gopath == "" {
  161. gopath = filepath.Join(homeDir, "go")
  162. }
  163. paths = append(paths, filepath.Join(gopath, "bin", command))
  164. if runtime.GOOS == "windows" {
  165. paths = append(paths, filepath.Join(gopath, "bin", command+".exe"))
  166. }
  167. case "typescript", "javascript", "html", "css", "json", "yaml", "php":
  168. // Node.js global packages
  169. if runtime.GOOS == "windows" {
  170. paths = append(paths,
  171. fmt.Sprintf("%s\\AppData\\Roaming\\npm\\%s.cmd", homeDir, command),
  172. fmt.Sprintf("%s\\AppData\\Roaming\\npm\\node_modules\\.bin\\%s.cmd", homeDir, command),
  173. )
  174. } else {
  175. paths = append(paths,
  176. fmt.Sprintf("%s/.npm-global/bin/%s", homeDir, command),
  177. fmt.Sprintf("%s/.nvm/versions/node/*/bin/%s", homeDir, command),
  178. fmt.Sprintf("/usr/local/lib/node_modules/.bin/%s", command),
  179. )
  180. }
  181. case "python":
  182. // Python paths
  183. if runtime.GOOS == "windows" {
  184. paths = append(paths,
  185. fmt.Sprintf("%s\\AppData\\Local\\Programs\\Python\\Python*\\Scripts\\%s.exe", homeDir, command),
  186. fmt.Sprintf("C:\\Python*\\Scripts\\%s.exe", command),
  187. )
  188. } else {
  189. paths = append(paths,
  190. fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
  191. fmt.Sprintf("%s/.pyenv/shims/%s", homeDir, command),
  192. fmt.Sprintf("/usr/local/bin/%s", command),
  193. )
  194. }
  195. case "rust":
  196. // Rust paths
  197. if runtime.GOOS == "windows" {
  198. paths = append(paths,
  199. fmt.Sprintf("%s\\.rustup\\toolchains\\*\\bin\\%s.exe", homeDir, command),
  200. fmt.Sprintf("%s\\.cargo\\bin\\%s.exe", homeDir, command),
  201. )
  202. } else {
  203. paths = append(paths,
  204. fmt.Sprintf("%s/.rustup/toolchains/*/bin/%s", homeDir, command),
  205. fmt.Sprintf("%s/.cargo/bin/%s", homeDir, command),
  206. )
  207. }
  208. }
  209. // Add VSCode extensions path
  210. vscodePath := getVSCodeExtensionsPath(homeDir)
  211. if vscodePath != "" {
  212. paths = append(paths, vscodePath)
  213. }
  214. // Expand any glob patterns in paths
  215. var expandedPaths []string
  216. for _, path := range paths {
  217. if strings.Contains(path, "*") {
  218. // This is a glob pattern, expand it
  219. matches, err := filepath.Glob(path)
  220. if err == nil {
  221. expandedPaths = append(expandedPaths, matches...)
  222. }
  223. } else {
  224. expandedPaths = append(expandedPaths, path)
  225. }
  226. }
  227. return expandedPaths
  228. }
  229. // getVSCodeExtensionsPath returns the path to VSCode extensions directory
  230. func getVSCodeExtensionsPath(homeDir string) string {
  231. var basePath string
  232. switch runtime.GOOS {
  233. case "darwin":
  234. basePath = filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage")
  235. case "linux":
  236. basePath = filepath.Join(homeDir, ".config", "Code", "User", "globalStorage")
  237. case "windows":
  238. basePath = filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage")
  239. default:
  240. return ""
  241. }
  242. // Check if the directory exists
  243. if _, err := os.Stat(basePath); err != nil {
  244. return ""
  245. }
  246. return basePath
  247. }
  248. // ConfigureLSPServers detects languages and configures LSP servers
  249. func ConfigureLSPServers(rootDir string) (map[string]ServerInfo, error) {
  250. // Detect languages in the project
  251. languages, err := DetectLanguages(rootDir)
  252. if err != nil {
  253. return nil, fmt.Errorf("failed to detect languages: %w", err)
  254. }
  255. // Find LSP servers for detected languages
  256. servers := make(map[string]ServerInfo)
  257. for langID, langInfo := range languages {
  258. // Prioritize primary languages but include all languages that have server definitions
  259. if !langInfo.IsPrimary && langInfo.FileCount < 3 {
  260. // Skip non-primary languages with very few files
  261. slog.Debug("Skipping non-primary language with few files", "language", langID, "files", langInfo.FileCount)
  262. continue
  263. }
  264. // Check if we have a server for this language
  265. serverInfo, err := FindLSPServer(langID)
  266. if err != nil {
  267. slog.Warn("LSP server not found", "language", langID, "error", err)
  268. continue
  269. }
  270. // Add to the map of configured servers
  271. servers[langID] = serverInfo
  272. if langInfo.IsPrimary {
  273. slog.Info("Configured LSP server for primary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
  274. } else {
  275. slog.Info("Configured LSP server for secondary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
  276. }
  277. }
  278. return servers, nil
  279. }