1use std::collections::HashMap;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone)]
5pub struct LspServerConfig {
6 pub command: String,
7 pub args: Vec<String>,
8}
9
10#[derive(Debug, Clone)]
11pub struct LspServerInfo {
12 pub language: &'static str,
13 pub binary: &'static str,
14 pub install_hint: &'static str,
15}
16
17pub const KNOWN_SERVERS: &[LspServerInfo] = &[
18 LspServerInfo {
19 language: "rust",
20 binary: "rust-analyzer",
21 install_hint: "rustup component add rust-analyzer",
22 },
23 LspServerInfo {
24 language: "typescript",
25 binary: "typescript-language-server",
26 install_hint: "npm install -g typescript-language-server typescript",
27 },
28 LspServerInfo {
29 language: "python",
30 binary: "pylsp",
31 install_hint: "pip install python-lsp-server",
32 },
33 LspServerInfo {
34 language: "go",
35 binary: "gopls",
36 install_hint: "go install golang.org/x/tools/gopls@latest",
37 },
38];
39
40pub fn default_servers() -> HashMap<&'static str, LspServerConfig> {
41 let mut m = HashMap::new();
42 m.insert(
43 "rust",
44 LspServerConfig {
45 command: "rust-analyzer".into(),
46 args: vec![],
47 },
48 );
49 m.insert(
50 "typescript",
51 LspServerConfig {
52 command: "typescript-language-server".into(),
53 args: vec!["--stdio".into()],
54 },
55 );
56 m.insert(
57 "javascript",
58 LspServerConfig {
59 command: "typescript-language-server".into(),
60 args: vec!["--stdio".into()],
61 },
62 );
63 m.insert(
64 "python",
65 LspServerConfig {
66 command: "pylsp".into(),
67 args: vec![],
68 },
69 );
70 m.insert(
71 "go",
72 LspServerConfig {
73 command: "gopls".into(),
74 args: vec!["serve".into()],
75 },
76 );
77 m
78}
79
80pub fn language_for_extension(ext: &str) -> Option<&'static str> {
81 match ext {
82 "rs" => Some("rust"),
83 "ts" | "tsx" => Some("typescript"),
84 "js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
85 "py" | "pyi" => Some("python"),
86 "go" => Some("go"),
87 "java" => Some("java"),
88 "rb" => Some("ruby"),
89 "c" | "h" => Some("c"),
90 "cpp" | "cxx" | "cc" | "hpp" => Some("cpp"),
91 "cs" => Some("csharp"),
92 _ => None,
93 }
94}
95
96pub fn find_binary_in_path(binary: &str) -> Option<PathBuf> {
97 let path_var = std::env::var("PATH").ok()?;
98 for dir in std::env::split_paths(&path_var) {
99 let candidate = dir.join(binary);
100 if candidate.is_file() {
101 return Some(candidate);
102 }
103 if cfg!(windows) {
104 let exe = dir.join(format!("{binary}.exe"));
105 if exe.is_file() {
106 return Some(exe);
107 }
108 }
109 }
110 None
111}
112
113pub fn install_hint_for_language(language: &str) -> &'static str {
114 for info in KNOWN_SERVERS {
115 if info.language == language {
116 return info.install_hint;
117 }
118 }
119 "No install instructions available for this language server."
120}
121
122pub fn binary_for_language(language: &str) -> Option<&'static str> {
123 for info in KNOWN_SERVERS {
124 if info.language == language {
125 return Some(info.binary);
126 }
127 }
128 None
129}
130
131pub fn check_server_available(language: &str) -> Result<PathBuf, String> {
132 let servers = default_servers();
133 let config = servers
134 .get(language)
135 .ok_or_else(|| format!("No LSP server configured for '{language}'"))?;
136
137 find_binary_in_path(&config.command).ok_or_else(|| {
138 let hint = install_hint_for_language(language);
139 format!(
140 "Language server '{}' not found in PATH.\n\
141 \n\
142 ctx_refactor requires an external language server for '{}' files.\n\
143 Install it with:\n\
144 \n\
145 \x20 {}\n\
146 \n\
147 Then retry. This is optional — ctx_search and ctx_graph work without it.",
148 config.command, language, hint
149 )
150 })
151}