1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::fs::File;
4use std::os::unix::io::AsRawFd;
5use std::path::{Path, PathBuf};
6use thiserror::Error;
7
8use crate::paths::{get_config_dir, get_config_path};
9
10struct ConfigLock {
11 _file: File,
12}
13
14impl ConfigLock {
15 fn acquire_exclusive() -> Result<Self, std::io::Error> {
16 let lock_path = get_config_path().with_extension("lock");
17 if let Some(parent) = lock_path.parent() {
18 std::fs::create_dir_all(parent)?;
19 }
20 let file = File::options()
21 .write(true)
22 .create(true)
23 .truncate(true)
24 .open(&lock_path)?;
25 let fd = file.as_raw_fd();
26 let result = unsafe { libc::flock(fd, libc::LOCK_EX) };
27 if result != 0 {
28 return Err(std::io::Error::last_os_error());
29 }
30 Ok(ConfigLock { _file: file })
31 }
32}
33
34#[derive(Error, Debug)]
35pub enum ConfigError {
36 #[error("IO error: {0}")]
37 Io(#[from] std::io::Error),
38 #[error("TOML parse error: {0}")]
39 TomlParse(#[from] toml::de::Error),
40 #[error("TOML serialize error: {0}")]
41 TomlSerialize(#[from] toml::ser::Error),
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct DaemonConfig {
46 #[serde(default = "default_log_level")]
47 pub log_level: String,
48 #[serde(default = "default_request_timeout")]
49 pub request_timeout: u64,
50 #[serde(default = "default_cache_size")]
51 pub hover_cache_size: u64,
52 #[serde(default = "default_cache_size")]
53 pub symbol_cache_size: u64,
54}
55
56impl Default for DaemonConfig {
57 fn default() -> Self {
58 Self {
59 log_level: default_log_level(),
60 request_timeout: default_request_timeout(),
61 hover_cache_size: default_cache_size(),
62 symbol_cache_size: default_cache_size(),
63 }
64 }
65}
66
67fn default_log_level() -> String {
68 "info".to_string()
69}
70
71fn default_request_timeout() -> u64 {
72 30
73}
74
75fn default_cache_size() -> u64 {
76 256 * 1024 * 1024 }
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct WorkspacesConfig {
81 #[serde(default)]
82 pub roots: Vec<String>,
83 #[serde(default)]
84 pub excluded_languages: Vec<String>,
85}
86
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct FormattingConfig {
89 #[serde(default = "default_tab_size")]
90 pub tab_size: u32,
91 #[serde(default = "default_insert_spaces")]
92 pub insert_spaces: bool,
93}
94
95fn default_tab_size() -> u32 {
96 4
97}
98
99fn default_insert_spaces() -> bool {
100 true
101}
102
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct ServerLanguageConfig {
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub preferred: Option<String>,
107}
108
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
110pub struct Config {
111 #[serde(default)]
112 pub daemon: DaemonConfig,
113 #[serde(default)]
114 pub workspaces: WorkspacesConfig,
115 #[serde(default)]
116 pub formatting: FormattingConfig,
117 #[serde(default)]
118 pub servers: HashMap<String, ServerLanguageConfig>,
119}
120
121impl Config {
122 pub fn load() -> Result<Self, ConfigError> {
123 let _lock = ConfigLock::acquire_exclusive()?;
124 Self::load_unlocked()
125 }
126
127 fn load_unlocked() -> Result<Self, ConfigError> {
128 let config_path = get_config_path();
129 if !config_path.exists() {
130 return Ok(Config::default());
131 }
132 let content = std::fs::read_to_string(&config_path)?;
133 let config: Config = toml::from_str(&content)?;
134 Ok(config)
135 }
136
137 pub fn save(&self) -> Result<(), ConfigError> {
138 let _lock = ConfigLock::acquire_exclusive()?;
139 self.save_unlocked()
140 }
141
142 fn save_unlocked(&self) -> Result<(), ConfigError> {
143 let config_path = get_config_path();
144 let config_dir = get_config_dir();
145 std::fs::create_dir_all(&config_dir)?;
146 let content = toml::to_string_pretty(self)?;
147 std::fs::write(&config_path, content)?;
148 Ok(())
149 }
150
151 pub fn add_workspace_root(root: &Path) -> Result<bool, ConfigError> {
152 let _lock = ConfigLock::acquire_exclusive()?;
153 let mut config = Config::load_unlocked()?;
154 let root_str = root.to_string_lossy().to_string();
155 if !config.workspaces.roots.contains(&root_str) {
156 config.workspaces.roots.push(root_str);
157 config.save_unlocked()?;
158 Ok(true)
159 } else {
160 Ok(false)
161 }
162 }
163
164 pub fn remove_workspace_root(root: &Path) -> Result<bool, ConfigError> {
165 let _lock = ConfigLock::acquire_exclusive()?;
166 let mut config = Config::load_unlocked()?;
167 let root_str = root.to_string_lossy().to_string();
168 let initial_len = config.workspaces.roots.len();
169 config.workspaces.roots.retain(|r| r != &root_str);
170 if config.workspaces.roots.len() < initial_len {
171 config.save_unlocked()?;
172 Ok(true)
173 } else {
174 Ok(false)
175 }
176 }
177
178 pub fn get_best_workspace_root(&self, path: &Path, cwd: Option<&Path>) -> Option<PathBuf> {
179 let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
180
181 let mut best: Option<PathBuf> = None;
182 let mut best_len = 0;
183
184 for root_str in &self.workspaces.roots {
185 let root = PathBuf::from(root_str);
186 let root = root.canonicalize().unwrap_or(root);
187
188 if path.starts_with(&root) {
189 let len = root.as_os_str().len();
190 if len > best_len {
191 best = Some(root);
192 best_len = len;
193 }
194 }
195 }
196
197 if best.is_some() {
198 return best;
199 }
200
201 if let Some(cwd) = cwd {
202 let cwd = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
203 for root_str in &self.workspaces.roots {
204 let root = PathBuf::from(root_str);
205 let root = root.canonicalize().unwrap_or(root);
206
207 if cwd.starts_with(&root) {
208 let len = root.as_os_str().len();
209 if len > best_len {
210 best = Some(root);
211 best_len = len;
212 }
213 }
214 }
215 }
216
217 best
218 }
219
220 pub fn cleanup_stale_workspace_roots(&mut self) -> Vec<String> {
221 let mut removed = Vec::new();
222 let original_roots = self.workspaces.roots.clone();
223
224 self.workspaces.roots.retain(|root| {
225 let path = PathBuf::from(root);
226 if path.exists() {
227 true
228 } else {
229 removed.push(root.clone());
230 false
231 }
232 });
233
234 if self.workspaces.roots.len() < original_roots.len() {
235 let _ = self.save();
236 }
237
238 removed
239 }
240}