on master help sign in
Add support for local language configuration (#1249)
c2a40d9Kirawi
Cargo.lock+1
@@ -404,6 +404,7 @@ dependencies = [
404 "cc",
404 "cc",
405 "etcetera",
405 "etcetera",
406 "libloading",
406 "libloading",
407+ "log",
407 "once_cell",
408 "once_cell",
408 "serde",
409 "serde",
409 "threadpool",
410 "threadpool",
book/src/languages.md+2
@@ -2,6 +2,8 @@
2
2
3Language-specific settings and settings for particular language servers can be configured in a `languages.toml` file placed in your [configuration directory](./configuration.md). Helix actually uses two `languages.toml` files, the [first one](https://github.com/helix-editor/helix/blob/master/languages.toml) is in the main helix repository; it contains the default settings for each language and is included in the helix binary at compile time. Users who want to see the available settings and options can either reference the helix repo's `languages.toml` file, or consult the table in the [adding languages](./guides/adding_languages.md) section.
3Language-specific settings and settings for particular language servers can be configured in a `languages.toml` file placed in your [configuration directory](./configuration.md). Helix actually uses two `languages.toml` files, the [first one](https://github.com/helix-editor/helix/blob/master/languages.toml) is in the main helix repository; it contains the default settings for each language and is included in the helix binary at compile time. Users who want to see the available settings and options can either reference the helix repo's `languages.toml` file, or consult the table in the [adding languages](./guides/adding_languages.md) section.
4
4
5+A local `languages.toml` can be created within a `.helix` directory. Its settings will be merged with both the global and default configs.
6+
5Changes made to the `languages.toml` file in a user's [configuration directory](./configuration.md) are merged with helix's defaults on start-up, such that a user's settings will take precedence over defaults in the event of a collision. For example, the default `languages.toml` sets rust's `auto-format` to `true`. If a user wants to disable auto-format, they can change the `languages.toml` in their [configuration directory](./configuration.md) to make the rust entry read like the example below; the new key/value pair `auto-format = false` will override the default when the two sets of settings are merged on start-up:
7Changes made to the `languages.toml` file in a user's [configuration directory](./configuration.md) are merged with helix's defaults on start-up, such that a user's settings will take precedence over defaults in the event of a collision. For example, the default `languages.toml` sets rust's `auto-format` to `true`. If a user wants to disable auto-format, they can change the `languages.toml` in their [configuration directory](./configuration.md) to make the rust entry read like the example below; the new key/value pair `auto-format = false` will override the default when the two sets of settings are merged on start-up:
6
8
7```toml
9```toml
helix-core/src/config.rs+2−2
@@ -1,10 +1,10 @@
1/// Syntax configuration loader based on built-in languages.toml.
1/// Syntax configuration loader based on built-in languages.toml.
2pub fn default_syntax_loader() -> crate::syntax::Configuration {
2pub fn default_syntax_loader() -> crate::syntax::Configuration {
3− helix_loader::default_lang_config()
3+ helix_loader::config::default_lang_config()
4 .try_into()
4 .try_into()
5 .expect("Could not serialize built-in languages.toml")
5 .expect("Could not serialize built-in languages.toml")
6}
6}
7/// Syntax configuration loader based on user configured languages.toml.
7/// Syntax configuration loader based on user configured languages.toml.
8pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
8pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
9− helix_loader::user_lang_config()?.try_into()
9+ helix_loader::config::user_lang_config()?.try_into()
10}
10}
helix-core/src/lib.rs+3−35
@@ -46,41 +46,9 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
46/// * Top-most folder containing a root marker if not git repository detected
46/// * Top-most folder containing a root marker if not git repository detected
47/// * Current working directory as fallback
47/// * Current working directory as fallback
48pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::path::PathBuf> {
48pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::path::PathBuf> {
49− let current_dir = std::env::current_dir().expect("unable to determine current directory");
49+ helix_loader::find_root_impl(root, root_markers)
50−
50+ .first()
51− let root = match root {
51+ .cloned()
52− Some(root) => {
53− let root = std::path::Path::new(root);
54− if root.is_absolute() {
55− root.to_path_buf()
56− } else {
57− current_dir.join(root)
58− }
59− }
60− None => current_dir.clone(),
61− };
62−
63− let mut top_marker = None;
64− for ancestor in root.ancestors() {
65− for marker in root_markers {
66− if ancestor.join(marker).exists() {
67− top_marker = Some(ancestor);
68− break;
69− }
70− }
71− // don't go higher than repo
72− if ancestor.join(".git").is_dir() {
73− // Use workspace if detected from marker
74− return Some(top_marker.unwrap_or(ancestor).to_path_buf());
75− }
76− }
77−
78− // In absence of git repo, use workspace if detected
79− if top_marker.is_some() {
80− top_marker.map(|a| a.to_path_buf())
81− } else {
82− Some(current_dir)
83− }
84}
52}
85
53
86pub use ropey::{Rope, RopeBuilder, RopeSlice};
54pub use ropey::{Rope, RopeBuilder, RopeSlice};
helix-loader/Cargo.toml+2
@@ -18,6 +18,8 @@ tree-sitter = "0.20"
18libloading = "0.7"
18libloading = "0.7"
19once_cell = "1.9"
19once_cell = "1.9"
20
20
21+log = "0.4"
22+
21# cloning/compiling tree-sitter grammars
23# cloning/compiling tree-sitter grammars
22cc = { version = "1" }
24cc = { version = "1" }
23threadpool = { version = "1.0" }
25threadpool = { version = "1.0" }
helix-loader/src/config.rsnew file+26
@@ -0,0 +1,26 @@
1+/// Default bultin-in languages.toml.
2+pub fn default_lang_config() -> toml::Value {
3+ toml::from_slice(include_bytes!("../../languages.toml"))
4+ .expect("Could not parse bultin-in languages.toml to valid toml")
5+}
6+
7+/// User configured languages.toml file, merged with the default config.
8+pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
9+ let config = crate::local_config_dirs()
10+ .into_iter()
11+ .chain([crate::config_dir()].into_iter())
12+ .map(|path| path.join("languages.toml"))
13+ .filter_map(|file| {
14+ std::fs::read(&file)
15+ .map(|config| toml::from_slice(&config))
16+ .ok()
17+ })
18+ .collect::<Result<Vec<_>, _>>()?
19+ .into_iter()
20+ .chain([default_lang_config()].into_iter())
21+ .fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
22+ crate::merge_toml_values(b, a)
23+ });
24+
25+ Ok(config)
26+}
helix-loader/src/grammar.rs+1−1
@@ -92,7 +92,7 @@ pub fn build_grammars() -> Result<()> {
92// merged. The `grammar_selection` key of the config is then used to filter
92// merged. The `grammar_selection` key of the config is then used to filter
93// down all grammars into a subset of the user's choosing.
93// down all grammars into a subset of the user's choosing.
94fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
94fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
95− let config: Configuration = crate::user_lang_config()
95+ let config: Configuration = crate::config::user_lang_config()
96 .context("Could not parse languages.toml")?
96 .context("Could not parse languages.toml")?
97 .try_into()?;
97 .try_into()?;
98
98
helix-loader/src/lib.rs+37−16
@@ -1,3 +1,4 @@
1+pub mod config;
1pub mod grammar;
2pub mod grammar;
2
3
3use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
4use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
@@ -36,6 +37,15 @@ pub fn config_dir() -> std::path::PathBuf {
36 path
37 path
37}
38}
38
39
40+pub fn local_config_dirs() -> Vec<std::path::PathBuf> {
41+ let directories = find_root_impl(None, &[".helix".to_string()])
42+ .into_iter()
43+ .map(|path| path.join(".helix"))
44+ .collect();
45+ log::debug!("Located configuration folders: {:?}", directories);
46+ directories
47+}
48+
39pub fn cache_dir() -> std::path::PathBuf {
49pub fn cache_dir() -> std::path::PathBuf {
40 // TODO: allow env var override
50 // TODO: allow env var override
41 let strategy = choose_base_strategy().expect("Unable to find the config directory!");
51 let strategy = choose_base_strategy().expect("Unable to find the config directory!");
@@ -56,25 +66,36 @@ pub fn log_file() -> std::path::PathBuf {
56 cache_dir().join("helix.log")
66 cache_dir().join("helix.log")
57}
67}
58
68
59−/// Default bultin-in languages.toml.
69+pub fn find_root_impl(root: Option<&str>, root_markers: &[String]) -> Vec<std::path::PathBuf> {
60−pub fn default_lang_config() -> toml::Value {
70+ let current_dir = std::env::current_dir().expect("unable to determine current directory");
61− toml::from_slice(include_bytes!("../../languages.toml"))
71+ let mut directories = Vec::new();
62− .expect("Could not parse bultin-in languages.toml to valid toml")
72+
63−}
73+ let root = match root {
64−
74+ Some(root) => {
65−/// User configured languages.toml file, merged with the default config.
75+ let root = std::path::Path::new(root);
66−pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
76+ if root.is_absolute() {
67− let def_lang_conf = default_lang_config();
77+ root.to_path_buf()
68− let data = std::fs::read(crate::config_dir().join("languages.toml"));
78+ } else {
69− let user_lang_conf = match data {
79+ current_dir.join(root)
70− Ok(raw) => {
80+ }
71− let value = toml::from_slice(&raw)?;
72− merge_toml_values(def_lang_conf, value)
73 }
81 }
74− Err(_) => def_lang_conf,
82+ None => current_dir,
75 };
83 };
76
84
77− Ok(user_lang_conf)
85+ for ancestor in root.ancestors() {
86+ // don't go higher than repo
87+ if ancestor.join(".git").is_dir() {
88+ // Use workspace if detected from marker
89+ directories.push(ancestor.to_path_buf());
90+ break;
91+ } else if root_markers
92+ .iter()
93+ .any(|marker| ancestor.join(marker).exists())
94+ {
95+ directories.push(ancestor.to_path_buf());
96+ }
97+ }
98+ directories
78}
99}
79
100
80// right overrides left
101// right overrides left
helix-term/src/application.rs+26−7
@@ -56,15 +56,33 @@ pub struct Application {
56}
56}
57
57
58impl Application {
58impl Application {
59− pub fn new(args: Args, config: Config) -> Result<Self, Error> {
59+ pub fn new(args: Args) -> Result<Self, Error> {
60 use helix_view::editor::Action;
60 use helix_view::editor::Action;
61− let mut compositor = Compositor::new()?;
62− let size = compositor.size();
63
61
64− let conf_dir = helix_loader::config_dir();
62+ let config_dir = helix_loader::config_dir();
63+ if !config_dir.exists() {
64+ std::fs::create_dir_all(&config_dir).ok();
65+ }
65
66
67+ let config = match std::fs::read_to_string(config_dir.join("config.toml")) {
68+ Ok(config) => toml::from_str(&config)
69+ .map(crate::keymap::merge_keys)
70+ .unwrap_or_else(|err| {
71+ eprintln!("Bad config: {}", err);
72+ eprintln!("Press <ENTER> to continue with default config");
73+ use std::io::Read;
74+ // This waits for an enter press.
75+ let _ = std::io::stdin().read(&mut []);
76+ Config::default()
77+ }),
78+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
79+ Err(err) => return Err(Error::new(err)),
80+ };
81+
66− let theme_loader =
82+ let theme_loader = std::sync::Arc::new(theme::Loader::new(
83+ &config_dir,
67− std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_loader::runtime_dir()));
84+ &helix_loader::runtime_dir(),
85+ ));
68
86
69 let true_color = config.editor.true_color || crate::true_color();
87 let true_color = config.editor.true_color || crate::true_color();
70 let theme = config
88 let theme = config
@@ -98,9 +116,10 @@ impl Application {
98 });
116 });
99 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
117 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
100
118
119+ let mut compositor = Compositor::new()?;
101 let config = Arc::new(ArcSwap::from_pointee(config));
120 let config = Arc::new(ArcSwap::from_pointee(config));
102 let mut editor = Editor::new(
121 let mut editor = Editor::new(
103− size,
122+ compositor.size(),
104 theme_loader.clone(),
123 theme_loader.clone(),
105 syn_loader.clone(),
124 syn_loader.clone(),
106 Box::new(Map::new(Arc::clone(&config), |config: &Config| {
125 Box::new(Map::new(Arc::clone(&config), |config: &Config| {
helix-term/src/main.rs+2−28
@@ -1,7 +1,6 @@
1−use anyhow::{Context, Error, Result};
1+use anyhow::{Context, Result};
2use helix_term::application::Application;
2use helix_term::application::Application;
3use helix_term::args::Args;
3use helix_term::args::Args;
4−use helix_term::config::{Config, ConfigLoadError};
5use std::path::PathBuf;
4use std::path::PathBuf;
6
5
7fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
6fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@@ -109,35 +108,10 @@ FLAGS:
109 return Ok(0);
108 return Ok(0);
110 }
109 }
111
110
112− let conf_dir = helix_loader::config_dir();
113− if !conf_dir.exists() {
114− std::fs::create_dir_all(&conf_dir).ok();
115− }
116−
117− let config = match Config::load_default() {
118− Ok(config) => config,
119− Err(err) => {
120− match err {
121− ConfigLoadError::BadConfig(err) => {
122− eprintln!("Bad config: {}", err);
123− eprintln!("Press <ENTER> to continue with default config");
124− use std::io::Read;
125− // This waits for an enter press.
126− let _ = std::io::stdin().read(&mut []);
127− Config::default()
128− }
129− ConfigLoadError::Error(err) if err.kind() == std::io::ErrorKind::NotFound => {
130− Config::default()
131− }
132− ConfigLoadError::Error(err) => return Err(Error::new(err)),
133− }
134− }
135− };
136−
137 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
111 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
138
112
139 // TODO: use the thread local executor to spawn the application task separately from the work pool
113 // TODO: use the thread local executor to spawn the application task separately from the work pool
140− let mut app = Application::new(args, config).context("unable to create new application")?;
114+ let mut app = Application::new(args).context("unable to create new application")?;
141
115
142 let exit_code = app.run().await?;
116 let exit_code = app.run().await?;
143
117