1package setup
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "runtime"
9 "strings"
10
11 "github.com/opencode-ai/opencode/internal/lsp/protocol"
12)
13
14// LSPServerInfo contains information about an LSP server
15type LSPServerInfo struct {
16 Name string // Display name of the server
17 Command string // Command to execute
18 Args []string // Arguments to pass to the command
19 InstallCmd string // Command to install the server
20 Description string // Description of the server
21 Recommended bool // Whether this is the recommended server for the language
22 Options any // Additional options for the server
23}
24
25// LSPServerMap maps languages to their available LSP servers
26type LSPServerMap map[protocol.LanguageKind][]LSPServerInfo
27
28// ServerDefinition defines an LSP server configuration
29type ServerDefinition struct {
30 Name string
31 Args []string
32 InstallCmd string
33 Languages []protocol.LanguageKind
34}
35
36// Common paths where LSP servers might be installed
37var (
38 // Common editor-specific paths
39 vscodePath = getVSCodeExtensionsPath()
40 neovimPath = getNeovimPluginsPath()
41
42 // Common package manager paths
43 npmBinPath = getNpmGlobalBinPath()
44 pipBinPath = getPipBinPath()
45 goBinPath = getGoBinPath()
46 cargoInstallPath = getCargoInstallPath()
47
48 // Server definitions
49 serverDefinitions = []ServerDefinition{
50 {
51 Name: "typescript-language-server",
52 Args: []string{"--stdio"},
53 InstallCmd: "npm install -g typescript-language-server typescript",
54 Languages: []protocol.LanguageKind{protocol.LangJavaScript, protocol.LangTypeScript, protocol.LangJavaScriptReact, protocol.LangTypeScriptReact},
55 },
56 {
57 Name: "deno",
58 Args: []string{"lsp"},
59 InstallCmd: "https://deno.com/#installation",
60 Languages: []protocol.LanguageKind{protocol.LangJavaScript, protocol.LangTypeScript},
61 },
62 {
63 Name: "pylsp",
64 Args: []string{},
65 InstallCmd: "pip install python-lsp-server",
66 Languages: []protocol.LanguageKind{protocol.LangPython},
67 },
68 {
69 Name: "pyright",
70 Args: []string{"--stdio"},
71 InstallCmd: "npm install -g pyright",
72 Languages: []protocol.LanguageKind{protocol.LangPython},
73 },
74 {
75 Name: "jedi-language-server",
76 Args: []string{},
77 InstallCmd: "pip install jedi-language-server",
78 Languages: []protocol.LanguageKind{protocol.LangPython},
79 },
80 {
81 Name: "gopls",
82 Args: []string{},
83 InstallCmd: "go install golang.org/x/tools/gopls@latest",
84 Languages: []protocol.LanguageKind{protocol.LangGo},
85 },
86 {
87 Name: "rust-analyzer",
88 Args: []string{},
89 InstallCmd: "rustup component add rust-analyzer",
90 Languages: []protocol.LanguageKind{protocol.LangRust},
91 },
92 {
93 Name: "jdtls",
94 Args: []string{},
95 InstallCmd: "Manual installation required: https://github.com/eclipse/eclipse.jdt.ls",
96 Languages: []protocol.LanguageKind{protocol.LangJava},
97 },
98 {
99 Name: "clangd",
100 Args: []string{},
101 InstallCmd: "Manual installation required: Install via package manager or https://clangd.llvm.org/installation.html",
102 Languages: []protocol.LanguageKind{protocol.LangC, protocol.LangCPP},
103 },
104 {
105 Name: "omnisharp",
106 Args: []string{"--languageserver"},
107 InstallCmd: "npm install -g omnisharp-language-server",
108 Languages: []protocol.LanguageKind{protocol.LangCSharp},
109 },
110 {
111 Name: "intelephense",
112 Args: []string{"--stdio"},
113 InstallCmd: "npm install -g intelephense",
114 Languages: []protocol.LanguageKind{protocol.LangPHP},
115 },
116 {
117 Name: "solargraph",
118 Args: []string{"stdio"},
119 InstallCmd: "gem install solargraph",
120 Languages: []protocol.LanguageKind{protocol.LangRuby},
121 },
122 {
123 Name: "vscode-html-language-server",
124 Args: []string{"--stdio"},
125 InstallCmd: "npm install -g vscode-langservers-extracted",
126 Languages: []protocol.LanguageKind{protocol.LangHTML},
127 },
128 {
129 Name: "vscode-css-language-server",
130 Args: []string{"--stdio"},
131 InstallCmd: "npm install -g vscode-langservers-extracted",
132 Languages: []protocol.LanguageKind{protocol.LangCSS},
133 },
134 {
135 Name: "vscode-json-language-server",
136 Args: []string{"--stdio"},
137 InstallCmd: "npm install -g vscode-langservers-extracted",
138 Languages: []protocol.LanguageKind{protocol.LangJSON},
139 },
140 {
141 Name: "yaml-language-server",
142 Args: []string{"--stdio"},
143 InstallCmd: "npm install -g yaml-language-server",
144 Languages: []protocol.LanguageKind{protocol.LangYAML},
145 },
146 {
147 Name: "lua-language-server",
148 Args: []string{},
149 InstallCmd: "https://github.com/LuaLS/lua-language-server/wiki/Getting-Started",
150 Languages: []protocol.LanguageKind{protocol.LangLua},
151 },
152 {
153 Name: "docker-langserver",
154 Args: []string{"--stdio"},
155 InstallCmd: "npm install -g dockerfile-language-server-nodejs",
156 Languages: []protocol.LanguageKind{protocol.LangDockerfile},
157 },
158 {
159 Name: "bash-language-server",
160 Args: []string{"start"},
161 InstallCmd: "npm install -g bash-language-server",
162 Languages: []protocol.LanguageKind{protocol.LangShellScript},
163 },
164 {
165 Name: "vls",
166 Args: []string{},
167 InstallCmd: "npm install -g @volar/vue-language-server",
168 Languages: []protocol.LanguageKind{"vue"},
169 },
170 {
171 Name: "svelteserver",
172 Args: []string{"--stdio"},
173 InstallCmd: "npm install -g svelte-language-server",
174 Languages: []protocol.LanguageKind{"svelte"},
175 },
176 {
177 Name: "dart",
178 Args: []string{"language-server"},
179 InstallCmd: "https://dart.dev/get-dart",
180 Languages: []protocol.LanguageKind{protocol.LangDart},
181 },
182 {
183 Name: "elixir-ls",
184 Args: []string{},
185 InstallCmd: "https://github.com/elixir-lsp/elixir-ls#installation",
186 Languages: []protocol.LanguageKind{protocol.LangElixir},
187 },
188 }
189
190 // Recommended servers by language
191 recommendedServers = map[protocol.LanguageKind]string{
192 protocol.LangJavaScript: "typescript-language-server",
193 protocol.LangTypeScript: "typescript-language-server",
194 protocol.LangJavaScriptReact: "typescript-language-server",
195 protocol.LangTypeScriptReact: "typescript-language-server",
196 protocol.LangPython: "pylsp",
197 protocol.LangGo: "gopls",
198 protocol.LangRust: "rust-analyzer",
199 protocol.LangJava: "jdtls",
200 protocol.LangC: "clangd",
201 protocol.LangCPP: "clangd",
202 protocol.LangCSharp: "omnisharp",
203 protocol.LangPHP: "intelephense",
204 protocol.LangRuby: "solargraph",
205 protocol.LangHTML: "vscode-html-language-server",
206 protocol.LangCSS: "vscode-css-language-server",
207 protocol.LangJSON: "vscode-json-language-server",
208 protocol.LangYAML: "yaml-language-server",
209 protocol.LangLua: "lua-language-server",
210 protocol.LangDockerfile: "docker-langserver",
211 protocol.LangShellScript: "bash-language-server",
212 "vue": "vls",
213 "svelte": "svelteserver",
214 protocol.LangDart: "dart",
215 protocol.LangElixir: "elixir-ls",
216 }
217)
218
219// DiscoverInstalledLSPs checks common locations for installed LSP servers
220func DiscoverInstalledLSPs() LSPServerMap {
221 result := make(LSPServerMap)
222
223 for _, def := range serverDefinitions {
224 for _, lang := range def.Languages {
225 checkAndAddServer(result, lang, def.Name, def.Args, def.InstallCmd)
226 }
227 }
228
229 return result
230}
231
232// checkAndAddServer checks if an LSP server is installed and adds it to the result map
233func checkAndAddServer(result LSPServerMap, lang protocol.LanguageKind, command string, args []string, installCmd string) {
234 // Check if the command exists in PATH
235 if path, err := exec.LookPath(command); err == nil {
236 server := LSPServerInfo{
237 Name: command,
238 Command: path,
239 Args: args,
240 InstallCmd: installCmd,
241 Description: fmt.Sprintf("%s language server", lang),
242 Recommended: isRecommendedServer(lang, command),
243 }
244
245 result[lang] = append(result[lang], server)
246 } else {
247 // Check in common editor-specific paths
248 if path := findInEditorPaths(command); path != "" {
249 server := LSPServerInfo{
250 Name: command,
251 Command: path,
252 Args: args,
253 InstallCmd: installCmd,
254 Description: fmt.Sprintf("%s language server", lang),
255 Recommended: isRecommendedServer(lang, command),
256 }
257 result[lang] = append(result[lang], server)
258 }
259 }
260}
261
262// findInEditorPaths checks for an LSP server in common editor-specific paths
263func findInEditorPaths(command string) string {
264 // Check in VSCode extensions
265 if vscodePath != "" {
266 // VSCode extensions can have different structures, so we need to search for the binary
267 matches, err := filepath.Glob(filepath.Join(vscodePath, "*", "**", command))
268 if err == nil && len(matches) > 0 {
269 for _, match := range matches {
270 if isExecutable(match) {
271 return match
272 }
273 }
274 }
275
276 // Check for node_modules/.bin in VSCode extensions
277 matches, err = filepath.Glob(filepath.Join(vscodePath, "*", "node_modules", ".bin", command))
278 if err == nil && len(matches) > 0 {
279 for _, match := range matches {
280 if isExecutable(match) {
281 return match
282 }
283 }
284 }
285 }
286
287 // Check in Neovim plugins
288 if neovimPath != "" {
289 matches, err := filepath.Glob(filepath.Join(neovimPath, "*", "**", command))
290 if err == nil && len(matches) > 0 {
291 for _, match := range matches {
292 if isExecutable(match) {
293 return match
294 }
295 }
296 }
297 }
298
299 // Check in npm global bin
300 if npmBinPath != "" {
301 path := filepath.Join(npmBinPath, command)
302 if isExecutable(path) {
303 return path
304 }
305 }
306
307 // Check in pip bin
308 if pipBinPath != "" {
309 path := filepath.Join(pipBinPath, command)
310 if isExecutable(path) {
311 return path
312 }
313 }
314
315 // Check in Go bin
316 if goBinPath != "" {
317 path := filepath.Join(goBinPath, command)
318 if isExecutable(path) {
319 return path
320 }
321 }
322
323 // Check in Cargo install
324 if cargoInstallPath != "" {
325 path := filepath.Join(cargoInstallPath, command)
326 if isExecutable(path) {
327 return path
328 }
329 }
330
331 return ""
332}
333
334// isExecutable checks if a file is executable
335func isExecutable(path string) bool {
336 info, err := os.Stat(path)
337 if err != nil {
338 return false
339 }
340
341 // On Windows, all files are "executable"
342 if runtime.GOOS == "windows" {
343 return !info.IsDir()
344 }
345
346 // On Unix-like systems, check the executable bit
347 return !info.IsDir() && (info.Mode()&0111 != 0)
348}
349
350// isRecommendedServer checks if a server is the recommended one for a language
351func isRecommendedServer(lang protocol.LanguageKind, command string) bool {
352 recommended, ok := recommendedServers[lang]
353 return ok && recommended == command
354}
355
356// GetRecommendedLSPServers returns the recommended LSP servers for the given languages
357func GetRecommendedLSPServers(languages []LanguageScore) LSPServerMap {
358 result := make(LSPServerMap)
359
360 for _, lang := range languages {
361 // Find the server definition for this language
362 for _, def := range serverDefinitions {
363 for _, defLang := range def.Languages {
364 if defLang == lang.Language && isRecommendedServer(lang.Language, def.Name) {
365 server := LSPServerInfo{
366 Name: def.Name,
367 Command: def.Name,
368 Args: def.Args,
369 InstallCmd: def.InstallCmd,
370 Description: fmt.Sprintf("%s Language Server", lang.Language),
371 Recommended: true,
372 }
373 result[lang.Language] = []LSPServerInfo{server}
374 break
375 }
376 }
377 }
378 }
379
380 return result
381}
382
383// Helper functions to get common paths
384
385func getVSCodeExtensionsPath() string {
386 var path string
387
388 switch runtime.GOOS {
389 case "windows":
390 path = filepath.Join(os.Getenv("USERPROFILE"), ".vscode", "extensions")
391 case "darwin":
392 path = filepath.Join(os.Getenv("HOME"), ".vscode", "extensions")
393 default: // Linux and others
394 path = filepath.Join(os.Getenv("HOME"), ".vscode", "extensions")
395 }
396
397 if _, err := os.Stat(path); err != nil {
398 // Try alternative locations
399 switch runtime.GOOS {
400 case "darwin":
401 altPath := filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Code", "User", "extensions")
402 if _, err := os.Stat(altPath); err == nil {
403 return altPath
404 }
405 case "linux":
406 altPath := filepath.Join(os.Getenv("HOME"), ".config", "Code", "User", "extensions")
407 if _, err := os.Stat(altPath); err == nil {
408 return altPath
409 }
410 }
411 return ""
412 }
413
414 return path
415}
416
417func getNeovimPluginsPath() string {
418 var paths []string
419
420 switch runtime.GOOS {
421 case "windows":
422 paths = []string{
423 filepath.Join(os.Getenv("LOCALAPPDATA"), "nvim", "plugged"),
424 filepath.Join(os.Getenv("LOCALAPPDATA"), "nvim", "site", "pack"),
425 }
426 default: // Linux, macOS, and others
427 paths = []string{
428 filepath.Join(os.Getenv("HOME"), ".local", "share", "nvim", "plugged"),
429 filepath.Join(os.Getenv("HOME"), ".local", "share", "nvim", "site", "pack"),
430 filepath.Join(os.Getenv("HOME"), ".config", "nvim", "plugged"),
431 }
432 }
433
434 for _, path := range paths {
435 if _, err := os.Stat(path); err == nil {
436 return path
437 }
438 }
439
440 return ""
441}
442
443func getNpmGlobalBinPath() string {
444 // Try to get the npm global bin path
445 cmd := exec.Command("npm", "config", "get", "prefix")
446 output, err := cmd.Output()
447 if err == nil {
448 prefix := strings.TrimSpace(string(output))
449 if prefix != "" {
450 if runtime.GOOS == "windows" {
451 return filepath.Join(prefix, "node_modules", ".bin")
452 }
453 return filepath.Join(prefix, "bin")
454 }
455 }
456
457 // Fallback to common locations
458 switch runtime.GOOS {
459 case "windows":
460 return filepath.Join(os.Getenv("APPDATA"), "npm")
461 default:
462 return filepath.Join(os.Getenv("HOME"), ".npm-global", "bin")
463 }
464}
465
466func getPipBinPath() string {
467 // Try to get the pip user bin path
468 var cmd *exec.Cmd
469 if runtime.GOOS == "windows" {
470 cmd = exec.Command("python", "-m", "site", "--user-base")
471 } else {
472 cmd = exec.Command("python3", "-m", "site", "--user-base")
473 }
474
475 output, err := cmd.Output()
476 if err == nil {
477 userBase := strings.TrimSpace(string(output))
478 if userBase != "" {
479 return filepath.Join(userBase, "bin")
480 }
481 }
482
483 // Fallback to common locations
484 switch runtime.GOOS {
485 case "windows":
486 return filepath.Join(os.Getenv("APPDATA"), "Python", "Scripts")
487 default:
488 return filepath.Join(os.Getenv("HOME"), ".local", "bin")
489 }
490}
491
492func getGoBinPath() string {
493 // Try to get the GOPATH
494 gopath := os.Getenv("GOPATH")
495 if gopath == "" {
496 // Fallback to default GOPATH
497 switch runtime.GOOS {
498 case "windows":
499 gopath = filepath.Join(os.Getenv("USERPROFILE"), "go")
500 default:
501 gopath = filepath.Join(os.Getenv("HOME"), "go")
502 }
503 }
504
505 return filepath.Join(gopath, "bin")
506}
507
508func getCargoInstallPath() string {
509 // Try to get the Cargo install path
510 cargoHome := os.Getenv("CARGO_HOME")
511 if cargoHome == "" {
512 // Fallback to default Cargo home
513 switch runtime.GOOS {
514 case "windows":
515 cargoHome = filepath.Join(os.Getenv("USERPROFILE"), ".cargo")
516 default:
517 cargoHome = filepath.Join(os.Getenv("HOME"), ".cargo")
518 }
519 }
520
521 return filepath.Join(cargoHome, "bin")
522}