Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Add Windows ARM64 release artifacts
- Move dictionary cache directory to platform-specific data directories instead of /tmp
- Allow overriding the global `codebook.toml` path via LSP initialization option `globalConfigPath`

[0.3.18]

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ If quickfix code actions are not showing up for specific languages, ensure your
},
```

To enable DEBUG logs, add this to your settings.json:
To enable DEBUG logs or change the global config path, add this to your settings.json:

```json
"lsp": {
"codebook": {
"initialization_options": {
"logLevel": "debug"
"logLevel": "debug",
"globalConfigPath": "~/.config/codebook/codebook.toml"
}
}
},
Expand Down Expand Up @@ -212,6 +213,8 @@ The global configuration applies to all projects by default. Location depends on
- **Linux/macOS**: `$XDG_CONFIG_HOME/codebook/codebook.toml` or `~/.config/codebook/codebook.toml`
- **Windows**: `%APPDATA%\codebook\codebook.toml` or `%APPDATA%\Roaming\codebook\codebook.toml`

You can override this location if you sync your config elsewhere by providing `initializationOptions.globalConfigPath` from your LSP client. When no override is provided, the OS-specific default above is used.

### Project Configuration

Project-specific configuration is loaded from either `codebook.toml` or `.codebook.toml` in the project root. Codebook searches for this file starting from the current directory and moving up to parent directories.
Expand Down
2 changes: 2 additions & 0 deletions crates/codebook-config/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ The data model for configuration settings:
- Linux/macOS: `$XDG_CONFIG_HOME/codebook/codebook.toml` if XDG_CONFIG_HOME is set
- Linux/macOS fallback: `~/.config/codebook/codebook.toml`
- Windows: `%APPDATA%\codebook\codebook.toml`
- **Custom Overrides**:
- Consumers may call `CodebookConfigFile::load_with_global_config` to supply an explicit global config path (used by `codebook-lsp` when an LSP client provides `initializationOptions.globalConfigPath`).

- **Configuration Precedence**:
- Project configuration overrides global configuration
Expand Down
20 changes: 20 additions & 0 deletions crates/codebook-config/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ pub(crate) fn min_word_length(settings: &ConfigSettings) -> usize {
settings.min_word_length
}

pub(crate) fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> {
let p = path_user_input.as_ref();
if !p.starts_with("~") {
return Some(p.to_path_buf());
}
if p == Path::new("~") {
return dirs::home_dir();
}
dirs::home_dir().map(|mut h| {
if h == Path::new("/") {
// Corner case: `h` root directory;
// don't prepend extra `/`, just drop the tilde.
p.strip_prefix("~").unwrap().to_path_buf()
} else {
h.push(p.strip_prefix("~/").unwrap());
h
}
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
57 changes: 52 additions & 5 deletions crates/codebook-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod helpers;
mod settings;
mod watched_file;
use crate::helpers::expand_tilde;
use crate::settings::ConfigSettings;
use crate::watched_file::WatchedFile;
use log::debug;
Expand Down Expand Up @@ -72,24 +73,40 @@ impl Default for CodebookConfigFile {
impl CodebookConfigFile {
/// Load configuration by searching for both global and project-specific configs
pub fn load(current_dir: Option<&Path>) -> Result<Self, io::Error> {
Self::load_with_global_config(current_dir, None)
}

/// Load configuration with an explicit global config override.
pub fn load_with_global_config(
current_dir: Option<&Path>,
global_config_path: Option<PathBuf>,
) -> Result<Self, io::Error> {
debug!("Initializing CodebookConfig");

if let Some(current_dir) = current_dir {
let current_dir = Path::new(current_dir);
Self::load_configs(current_dir)
Self::load_configs(current_dir, global_config_path)
} else {
let current_dir = env::current_dir()?;
Self::load_configs(&current_dir)
Self::load_configs(&current_dir, global_config_path)
}
}

/// Load both global and project configuration
fn load_configs(start_dir: &Path) -> Result<Self, io::Error> {
fn load_configs(
start_dir: &Path,
global_config_override: Option<PathBuf>,
) -> Result<Self, io::Error> {
let config = Self::default();
let mut inner = config.inner.write().unwrap();

// First, try to load global config
if let Some(global_path) = Self::find_global_config_path() {
let global_config_path = match global_config_override {
Some(path) => Some(path.to_path_buf()),
None => Self::find_global_config_path(),
};

if let Some(global_path) = global_config_path {
let global_config = WatchedFile::new(Some(global_path.clone()));

if global_path.exists() {
Expand Down Expand Up @@ -321,6 +338,10 @@ impl CodebookConfigFile {
None => return Ok(()),
};

#[cfg(not(windows))]
let global_config_path = expand_tilde(global_config_path)
.expect("Failed to expand tilde in: {global_config_path}");

let settings = match inner.global_config.content() {
Some(settings) => settings,
None => return Ok(()),
Expand Down Expand Up @@ -806,14 +827,40 @@ mod tests {
"#
)?;

let config = CodebookConfigFile::load_configs(&sub_sub_dir)?;
let config = CodebookConfigFile::load_configs(&sub_sub_dir, None)?;
assert!(config.snapshot().words.contains(&"testword".to_string()));

// Check that the config file path is stored
assert_eq!(config.project_config_path(), Some(config_path));
Ok(())
}

#[test]
fn test_global_config_override_is_used() -> Result<(), io::Error> {
let temp_dir = TempDir::new().unwrap();
let workspace_dir = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_dir)?;
let custom_global_dir = temp_dir.path().join("global");
fs::create_dir_all(&custom_global_dir)?;
let override_path = custom_global_dir.join("codebook.toml");

fs::write(
&override_path,
r#"
words = ["customword"]
"#,
)?;

let config = CodebookConfigFile::load_with_global_config(
Some(workspace_dir.as_path()),
Some(override_path.clone()),
)?;

assert_eq!(config.global_config_path(), Some(override_path));
assert!(config.is_allowed_word("customword"));
Ok(())
}

#[test]
fn test_should_ignore_path() {
let config = CodebookConfigFile::default();
Expand Down
106 changes: 106 additions & 0 deletions crates/codebook-lsp/src/init_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use log::LevelFilter;
use serde::Deserialize;
use serde::de::Deserializer;
use serde_json::Value;
use std::path::PathBuf;

fn default_log_level() -> LevelFilter {
LevelFilter::Info
}

fn deserialize_log_level<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s.as_deref() {
Some("trace") => Ok(LevelFilter::Trace),
Some("debug") => Ok(LevelFilter::Debug),
Some("warn") => Ok(LevelFilter::Warn),
Some("error") => Ok(LevelFilter::Error),
_ => Ok(LevelFilter::Info),
}
}

fn deserialize_global_config_path<'de, D>(deserializer: D) -> Result<Option<PathBuf>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(path) => {
let trimmed = path.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(PathBuf::from(trimmed)))
}
}
None => Ok(None),
}
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ClientInitializationOptions {
#[serde(
default = "default_log_level",
deserialize_with = "deserialize_log_level"
)]
pub(crate) log_level: LevelFilter,
#[serde(default, deserialize_with = "deserialize_global_config_path")]
pub(crate) global_config_path: Option<PathBuf>,
}

impl Default for ClientInitializationOptions {
fn default() -> Self {
ClientInitializationOptions {
log_level: default_log_level(),
global_config_path: None,
}
}
}

impl ClientInitializationOptions {
pub(crate) fn from_value(options_value: Option<Value>) -> Self {
match options_value {
None => ClientInitializationOptions::default(),
Some(value) => match serde_json::from_value(value) {
Ok(options) => options,
Err(err) => {
log::error!(
"Failed to deserialize client initialization options. Using default: {}",
err
);
ClientInitializationOptions::default()
}
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default() {
let default_options = ClientInitializationOptions::default();
assert_eq!(default_options.log_level, LevelFilter::Info);
}

#[test]
fn test_custom() {
let custom_options = ClientInitializationOptions {
log_level: LevelFilter::Debug,
..Default::default()
};
assert_eq!(custom_options.log_level, LevelFilter::Debug);
}

#[test]
fn test_json() {
let json = r#"{"logLevel": "debug"}"#;
let options: ClientInitializationOptions = serde_json::from_str(json).unwrap();
assert_eq!(options.log_level, LevelFilter::Debug);
}
}
1 change: 1 addition & 0 deletions crates/codebook-lsp/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod file_cache;
mod init_options;
pub mod lsp;
pub mod lsp_logger;
Loading