use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct Settings {
pub theme: Option<String>,
pub keymap: Option<String>,
pub tier: Option<String>,
pub smooth_scroll: Option<bool>,
pub minimap: Option<bool>,
pub spell_check: Option<bool>,
pub browser: Option<Vec<String>>,
pub language_servers: HashMap<String, Vec<String>>,
pub declined_languages: Vec<String>,
pub latex_engine: Option<Vec<String>>,
}
fn read(path: &Path) -> Option<Settings> {
serde_json::from_str(&fs::read_to_string(path).ok()?).ok()
}
impl Settings {
fn merge(&mut self, other: Self) {
self.theme = other.theme.or(self.theme.take());
self.smooth_scroll = other.smooth_scroll.or(self.smooth_scroll.take());
self.minimap = other.minimap.or(self.minimap.take());
self.keymap = other.keymap.or(self.keymap.take());
self.tier = other.tier.or(self.tier.take());
self.browser = other.browser.or(self.browser.take());
self.spell_check = other.spell_check.or(self.spell_check.take());
self.language_servers.extend(other.language_servers);
self.declined_languages.extend(other.declined_languages);
self.latex_engine = other.latex_engine.or(self.latex_engine.take());
}
pub fn words_file() -> Option<PathBuf> {
if cfg!(test) {
return None;
}
let home = PathBuf::from(std::env::var_os("HOME")?);
Some(home.join(".config/cellix/words.txt"))
}
fn user_file() -> Option<PathBuf> {
let home = PathBuf::from(std::env::var_os("HOME")?);
Some(home.join(".config/cellix/settings.json"))
}
pub fn store(key: &str, value: serde_json::Value) -> std::io::Result<()> {
if cfg!(test) {
return Ok(());
}
let file = Self::user_file().ok_or(std::io::ErrorKind::NotFound)?;
Self::store_in(&file, key, value)
}
fn store_in(file: &Path, key: &str, value: serde_json::Value) -> std::io::Result<()> {
let mut settings = crate::integrations::read_settings(file)?;
settings.insert(key.to_string(), value);
crate::integrations::write_settings(file, &settings)
}
pub fn load(root: &Path) -> Self {
let mut settings = Self::default();
if let Some(found) = Self::user_file().as_deref().and_then(read) {
settings.merge(found);
}
if let Some(found) = read(&root.join(".cellix/settings.json")) {
settings.merge(found);
}
settings
}
}
pub struct Server {
pub languages: &'static [&'static str],
pub command: &'static [&'static str],
pub install: &'static str,
}
const fn server(
languages: &'static [&'static str],
command: &'static [&'static str],
install: &'static str,
) -> Server {
Server {
languages,
command,
install,
}
}
pub const SERVERS: [Server; 27] = [
server(
&["Rust"],
&["rust-analyzer"],
"rustup component add rust-analyzer",
),
server(
&["Python"],
&["pyright-langserver", "--stdio"],
"npm install -g pyright",
),
server(
&["TypeScript", "TSX", "JavaScript"],
&["typescript-language-server", "--stdio"],
"npm install -g typescript typescript-language-server",
),
server(
&["Go"],
&["gopls"],
"go install golang.org/x/tools/gopls@latest",
),
server(&["Java"], &["jdtls"], "brew install jdtls"),
server(
&["C#"],
&["csharp-ls"],
"dotnet tool install --global csharp-ls",
),
server(&["C", "C++"], &["clangd"], "brew install llvm"),
server(
&["Kotlin"],
&["kotlin-language-server"],
"brew install kotlin-language-server",
),
server(&["Swift"], &["sourcekit-lsp"], "xcode-select --install"),
server(
&["PHP"],
&["intelephense", "--stdio"],
"npm install -g intelephense",
),
server(&["Ruby"], &["ruby-lsp"], "gem install ruby-lsp"),
server(
&["Lua"],
&["lua-language-server"],
"brew install lua-language-server",
),
server(&["Scala"], &["metals"], "brew install metals"),
server(&["Elixir"], &["elixir-ls"], "brew install elixir-ls"),
server(
&["Haskell"],
&["haskell-language-server-wrapper", "--lsp"],
"brew install haskell-language-server",
),
server(&["Zig"], &["zls"], "brew install zls"),
server(
&["Dart"],
&["dart", "language-server", "--protocol=lsp"],
"brew install dart",
),
server(
&["Shell"],
&["bash-language-server", "start"],
"npm install -g bash-language-server",
),
server(
&["Terraform"],
&["terraform-ls", "serve"],
"brew install hashicorp/tap/terraform-ls",
),
server(
&["YAML"],
&["yaml-language-server", "--stdio"],
"npm install -g yaml-language-server",
),
server(&["TOML"], &["taplo", "lsp", "stdio"], "brew install taplo"),
server(
&["HTML"],
&["vscode-html-language-server", "--stdio"],
"npm install -g vscode-langservers-extracted",
),
server(
&["CSS"],
&["vscode-css-language-server", "--stdio"],
"npm install -g vscode-langservers-extracted",
),
server(
&["JSON"],
&["vscode-json-language-server", "--stdio"],
"npm install -g vscode-langservers-extracted",
),
server(
&["Dockerfile"],
&["docker-langserver", "--stdio"],
"npm install -g dockerfile-language-server-nodejs",
),
server(&["LaTeX"], &["texlab"], "brew install texlab tectonic"),
server(
&["Markdown"],
&["marksman", "server"],
"brew install marksman",
),
];
pub fn default_server(language: &str) -> Option<Vec<String>> {
let found = SERVERS.iter().find(|s| s.languages.contains(&language))?;
Some(found.command.iter().map(|s| s.to_string()).collect())
}
pub fn formatter(language: &str) -> Option<&'static [&'static str]> {
Some(match language {
"Rust" => &["rustfmt", "--edition", "2024"],
"Go" => &["gofmt"],
"Python" => &["ruff", "format", "-"],
"C" | "C++" => &["clang-format"],
"Shell" => &["shfmt"],
"Terraform" => &["terraform", "fmt", "-"],
"Swift" => &["swift-format"],
"Lua" => &["stylua", "-"],
"Zig" => &["zig", "fmt", "--stdin"],
"JavaScript" | "TypeScript" | "TSX" | "JSON" | "HTML" | "CSS" | "YAML" | "Markdown" => {
&["prettier", "--stdin-filepath"]
}
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_change_keeps_the_rest_of_the_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("cellix/settings.json");
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(&file, r#"{"keymap":"vim"}"#).unwrap();
Settings::store_in(&file, "theme", "light".into()).unwrap();
let saved = read(&file).unwrap();
assert_eq!(saved.theme.as_deref(), Some("light"));
assert_eq!(saved.keymap.as_deref(), Some("vim"));
}
}