the-dipsy 2 weeks ago committed by GitHub
commit 45629b846b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -30,6 +30,7 @@ You can use a custom configuration file by specifying it with the `-c` or
Additionally, you can reload the configuration file by sending the USR1
signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository and adding `load-workspace-config = "always"` to the top of your configuration directory `config.toml`.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
Enabling workspace configs is a potential security risk. A project from an untrusted sources may e.g. contain a `.helix/config.toml` that overrides the `editor.shell` parameter to execute malicious code on your machine.

@ -28,8 +28,13 @@ There are three possible locations for a `languages.toml` file:
3. In a `.helix` folder in your project. Language configuration may also be
overridden local to a project by creating a `languages.toml` file in a
`.helix` folder. Its settings will be merged with the language configuration
in the configuration directory and the built-in configuration.
`.helix` folder and adding `workspace-config = true` to the top of your
configuration directory `languages.toml`. Its settings will be merged with
the language configuration in the configuration directory and the built-in
configuration. Enabling workspace configs is a potential security risk.
A project from an untrusted sources may e.g. contain a
`.helix/languages.toml` that modifies a language server command to execute
malicious code on your machine.
## Language configuration

@ -1,4 +1,5 @@
use std::str::from_utf8;
use std::path::PathBuf;
/// Default built-in languages.toml.
pub fn default_lang_config() -> toml::Value {
@ -7,40 +8,28 @@ pub fn default_lang_config() -> toml::Value {
.expect("Could not parse built-in languages.toml to valid toml")
}
fn merge_language_config(
left: toml::Value, file: PathBuf,
) -> Result<toml::Value, toml::de::Error> {
let right = std::fs::read_to_string(file).ok()
.map(|c| toml::from_str(&c)).transpose()?;
let config = match right {
Some(right) => crate::merge_toml_values(left, right, 3),
None => left,
};
Ok(config)
}
/// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let config = [
crate::config_dir(),
crate::find_workspace().0.join(".helix"),
]
.into_iter()
.map(|path| path.join("languages.toml"))
.filter_map(|file| {
std::fs::read_to_string(file)
.map(|config| toml::from_str(&config))
.ok()
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.fold(default_lang_config(), |a, b| {
// combines for example
// b:
// [[language]]
// name = "toml"
// language-server = { command = "taplo", args = ["lsp", "stdio"] }
//
// a:
// [[language]]
// language-server = { command = "/usr/bin/taplo" }
//
// into:
// [[language]]
// name = "toml"
// language-server = { command = "/usr/bin/taplo" }
//
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(a, b, 3)
});
let global = merge_language_config(default_lang_config(), crate::lang_config_file())?;
let config = match global.get("workspace-config").and_then(|v| v.as_bool()) {
Some(true) => merge_language_config(global, crate::workspace_lang_config_file())?,
_ => global,
};
Ok(config)
}

@ -148,6 +148,10 @@ pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml")
}
pub fn workspace_lang_config_file() -> PathBuf {
find_workspace().0.join(".helix").join("languages.toml")
}
pub fn default_log_file() -> PathBuf {
cache_dir().join("helix.log")
}

@ -431,7 +431,7 @@ impl Application {
fn refresh_config(&mut self) {
let mut refresh_config = || -> Result<(), Error> {
let default_config = Config::load_default()
let default_config = Config::load()
.map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?;
self.refresh_language_config()?;
self.refresh_theme(&default_config)?;

@ -4,45 +4,97 @@ use helix_loader::merge_toml_values;
use helix_view::document::Mode;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use std::fmt::Display;
use std::fs;
use std::io::Error as IOError;
use toml::de::Error as TomlError;
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub theme: Option<String>,
pub keys: HashMap<Mode, KeyTrie>,
pub editor: helix_view::editor::Config,
// Config loading error
#[derive(Debug)]
pub enum ConfigLoadError {
BadConfig(TomlError),
Error(IOError),
}
impl Default for ConfigLoadError {
fn default() -> Self {
ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder"))
}
}
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LoadWorkspaceConfig {
#[default]
Never,
Always,
}
// Deserializable raw config struct
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ConfigRaw {
pub load_workspace_config: Option<LoadWorkspaceConfig>,
pub theme: Option<String>,
pub keys: Option<HashMap<Mode, KeyTrie>>,
pub editor: Option<toml::Value>,
}
impl Default for Config {
fn default() -> Config {
Config {
impl Default for ConfigRaw {
fn default() -> ConfigRaw {
Self {
load_workspace_config: Some(LoadWorkspaceConfig::default()),
theme: None,
keys: keymap::default(),
editor: helix_view::editor::Config::default(),
keys: Some(keymap::default()),
editor: None,
}
}
}
#[derive(Debug)]
pub enum ConfigLoadError {
BadConfig(TomlError),
Error(IOError),
impl ConfigRaw {
fn load(file: PathBuf) -> Result<Self, ConfigLoadError> {
let source = fs::read_to_string(file).map_err(ConfigLoadError::Error)?;
toml::from_str(&source).map_err(ConfigLoadError::BadConfig)
}
fn merge(self, other: ConfigRaw, trust: bool) -> Self {
ConfigRaw {
load_workspace_config: match trust {
true => other.load_workspace_config.or(self.load_workspace_config),
false => self.load_workspace_config,
},
theme: other.theme.or(self.theme),
keys: match (self.keys, other.keys) {
(Some(a), Some(b)) => Some(merge_keys(a, b)),
(opt_a, opt_b) => opt_a.or(opt_b),
},
editor: match (self.editor, other.editor) {
(Some(a), Some(b)) => Some(merge_toml_values(a, b, 3)),
(opt_a, opt_b) => opt_a.or(opt_b),
}
}
}
}
impl Default for ConfigLoadError {
fn default() -> Self {
ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder"))
// Final config struct
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub load_workspace_config: LoadWorkspaceConfig,
pub theme: Option<String>,
pub keys: HashMap<Mode, KeyTrie>,
pub editor: helix_view::editor::Config,
}
impl Default for Config {
fn default() -> Config {
let raw = ConfigRaw::default();
Self {
load_workspace_config: raw.load_workspace_config.unwrap_or_default(),
theme: raw.theme,
keys: raw.keys.unwrap_or_else(|| keymap::default()),
editor: helix_view::editor::Config::default(),
}
}
}
@ -55,74 +107,37 @@ impl Display for ConfigLoadError {
}
}
impl Config {
pub fn load(
global: Result<String, ConfigLoadError>,
local: Result<String, ConfigLoadError>,
) -> Result<Config, ConfigLoadError> {
let global_config: Result<ConfigRaw, ConfigLoadError> =
global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
let local_config: Result<ConfigRaw, ConfigLoadError> =
local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
let res = match (global_config, local_config) {
(Ok(global), Ok(local)) => {
let mut keys = keymap::default();
if let Some(global_keys) = global.keys {
merge_keys(&mut keys, global_keys)
}
if let Some(local_keys) = local.keys {
merge_keys(&mut keys, local_keys)
}
let editor = match (global.editor, local.editor) {
(None, None) => helix_view::editor::Config::default(),
(None, Some(val)) | (Some(val), None) => {
val.try_into().map_err(ConfigLoadError::BadConfig)?
}
(Some(global), Some(local)) => merge_toml_values(global, local, 3)
.try_into()
.map_err(ConfigLoadError::BadConfig)?,
};
Config {
theme: local.theme.or(global.theme),
keys,
editor,
}
}
// if any configs are invalid return that first
(_, Err(ConfigLoadError::BadConfig(err)))
| (Err(ConfigLoadError::BadConfig(err)), _) => {
return Err(ConfigLoadError::BadConfig(err))
}
(Ok(config), Err(_)) | (Err(_), Ok(config)) => {
let mut keys = keymap::default();
if let Some(keymap) = config.keys {
merge_keys(&mut keys, keymap);
}
Config {
theme: config.theme,
keys,
editor: config.editor.map_or_else(
|| Ok(helix_view::editor::Config::default()),
|val| val.try_into().map_err(ConfigLoadError::BadConfig),
)?,
}
}
// these are just two io errors return the one for the global config
(Err(err), Err(_)) => return Err(err),
};
Ok(res)
impl TryFrom<ConfigRaw> for Config {
type Error = ConfigLoadError;
fn try_from(config: ConfigRaw) -> Result<Self, Self::Error> {
Ok(Self {
load_workspace_config: config.load_workspace_config.unwrap_or_default(),
theme: config.theme,
keys: config.keys.unwrap_or_else(|| keymap::default()),
editor: config.editor
.map(|e| e.try_into()).transpose()
.map_err(ConfigLoadError::BadConfig)?
.unwrap_or_default(),
})
}
}
pub fn load_default() -> Result<Config, ConfigLoadError> {
let global_config =
fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error);
let local_config = fs::read_to_string(helix_loader::workspace_config_file())
.map_err(ConfigLoadError::Error);
Config::load(global_config, local_config)
impl Config {
pub fn load() -> Result<Config, ConfigLoadError> {
let default = ConfigRaw::default();
let global = default.merge(ConfigRaw::load(helix_loader::config_file())?, true);
match global.load_workspace_config {
Some(LoadWorkspaceConfig::Always) => {
match ConfigRaw::load(helix_loader::workspace_config_file()) {
Ok(workspace) => Ok(global.merge(workspace, false)),
Err(ConfigLoadError::Error(_)) => Ok(global),
error => error,
}?
},
_ => global,
}.try_into()
}
}
@ -131,8 +146,9 @@ mod tests {
use super::*;
impl Config {
fn load_test(config: &str) -> Config {
Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap()
fn load_test(file: &str) -> Config {
let raw: ConfigRaw = toml::from_str(file).unwrap();
ConfigRaw::default().merge(raw, true).try_into().unwrap()
}
}
@ -151,9 +167,8 @@ mod tests {
A-F12 = "move_next_word_end"
"#;
let mut keys = keymap::default();
merge_keys(
&mut keys,
let keys = merge_keys(
keymap::default(),
hashmap! {
Mode::Insert => keymap!({ "Insert mode"
"y" => move_line_down,

@ -374,14 +374,17 @@ impl Default for Keymaps {
}
/// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(dst: &mut HashMap<Mode, KeyTrie>, mut delta: HashMap<Mode, KeyTrie>) {
for (mode, keys) in dst {
pub fn merge_keys(
mut left: HashMap<Mode, KeyTrie>, mut right: HashMap<Mode, KeyTrie>,
) -> HashMap<Mode, KeyTrie> {
for (mode, keys) in &mut left {
keys.merge_nodes(
delta
right
.remove(mode)
.unwrap_or_else(|| KeyTrie::Node(KeyTrieNode::default())),
)
}
left
}
#[cfg(test)]
@ -419,11 +422,10 @@ mod tests {
},
})
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
assert_ne!(keymap, merged_keyamp);
let mut merged_keymap = merge_keys(default(), keymap.clone());
assert_ne!(keymap, merged_keymap);
let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone())));
let mut keymap = Keymaps::new(Box::new(Constant(merged_keymap.clone())));
assert_eq!(
keymap.get(Mode::Normal, key!('i')),
KeymapResult::Matched(MappableCommand::normal_mode),
@ -441,7 +443,7 @@ mod tests {
"Leaf should replace node"
);
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
let keymap = merged_keymap.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.search(&[key!('g'), key!('$')]).unwrap(),
@ -462,7 +464,7 @@ mod tests {
);
assert!(
merged_keyamp
merged_keymap
.get(&Mode::Normal)
.and_then(|key_trie| key_trie.node())
.unwrap()
@ -470,7 +472,7 @@ mod tests {
> 1
);
assert!(
merged_keyamp
merged_keymap
.get(&Mode::Insert)
.and_then(|key_trie| key_trie.node())
.unwrap()
@ -491,10 +493,9 @@ mod tests {
},
})
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
assert_ne!(keymap, merged_keyamp);
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
let mut merged_keymap = merge_keys(default(), keymap.clone());
assert_ne!(keymap, merged_keymap);
let keymap = merged_keymap.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works
assert_eq!(
keymap.search(&[key!(' '), key!('s'), key!('v')]).unwrap(),

@ -130,7 +130,7 @@ FLAGS:
helix_stdx::env::set_current_working_dir(path)?;
}
let config = match Config::load_default() {
let config = match Config::load() {
Ok(config) => config,
Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
Config::default()

@ -1,6 +1,8 @@
# Language support configuration.
# See the languages documentation: https://docs.helix-editor.com/master/languages.html
workspace-config = false
use-grammars = { except = [ "hare", "wren", "gemini" ] }
[language-server]

Loading…
Cancel
Save