mcpls_core/config/
mod.rs

1//! Configuration types and loading.
2//!
3//! This module provides configuration structures for MCPLS,
4//! including LSP server definitions and workspace settings.
5
6mod server;
7
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11pub use server::LspServerConfig;
12
13use crate::error::{Error, Result};
14
15/// Main configuration for the MCPLS server.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct ServerConfig {
19    /// Workspace configuration.
20    #[serde(default)]
21    pub workspace: WorkspaceConfig,
22
23    /// LSP server configurations.
24    #[serde(default)]
25    pub lsp_servers: Vec<LspServerConfig>,
26}
27
28/// Workspace-level configuration.
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct WorkspaceConfig {
32    /// Root directories for the workspace.
33    #[serde(default)]
34    pub roots: Vec<PathBuf>,
35
36    /// Position encoding preference order.
37    /// Valid values: "utf-8", "utf-16", "utf-32"
38    #[serde(default = "default_position_encodings")]
39    pub position_encodings: Vec<String>,
40}
41
42fn default_position_encodings() -> Vec<String> {
43    vec!["utf-8".to_string(), "utf-16".to_string()]
44}
45
46impl ServerConfig {
47    /// Load configuration from the default path.
48    ///
49    /// Default paths checked in order:
50    /// 1. `$MCPLS_CONFIG` environment variable
51    /// 2. `./mcpls.toml` (current directory)
52    /// 3. `~/.config/mcpls/mcpls.toml` (Linux/macOS)
53    /// 4. `%APPDATA%\mcpls\mcpls.toml` (Windows)
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if no configuration file is found or if parsing fails.
58    pub fn load() -> Result<Self> {
59        if let Ok(path) = std::env::var("MCPLS_CONFIG") {
60            return Self::load_from(Path::new(&path));
61        }
62
63        let local_config = PathBuf::from("mcpls.toml");
64        if local_config.exists() {
65            return Self::load_from(&local_config);
66        }
67
68        if let Some(config_dir) = dirs::config_dir() {
69            let user_config = config_dir.join("mcpls").join("mcpls.toml");
70            if user_config.exists() {
71                return Self::load_from(&user_config);
72            }
73        }
74
75        // Return default configuration if no config file found
76        Ok(Self::default())
77    }
78
79    /// Load configuration from a specific path.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if the file doesn't exist or parsing fails.
84    pub fn load_from(path: &Path) -> Result<Self> {
85        let content = std::fs::read_to_string(path).map_err(|e| {
86            if e.kind() == std::io::ErrorKind::NotFound {
87                Error::ConfigNotFound(path.to_path_buf())
88            } else {
89                Error::Io(e)
90            }
91        })?;
92
93        let config: Self = toml::from_str(&content)?;
94        config.validate()?;
95        Ok(config)
96    }
97
98    /// Validate the configuration.
99    fn validate(&self) -> Result<()> {
100        for server in &self.lsp_servers {
101            if server.language_id.is_empty() {
102                return Err(Error::InvalidConfig(
103                    "language_id cannot be empty".to_string(),
104                ));
105            }
106            if server.command.is_empty() {
107                return Err(Error::InvalidConfig(format!(
108                    "command cannot be empty for language '{}'",
109                    server.language_id
110                )));
111            }
112        }
113        Ok(())
114    }
115}
116
117impl Default for ServerConfig {
118    fn default() -> Self {
119        Self {
120            workspace: WorkspaceConfig::default(),
121            lsp_servers: vec![LspServerConfig::rust_analyzer()],
122        }
123    }
124}