leta_config/
config.rs

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 // 256MB
77}
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}