Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for local language configuration #1249

Merged
merged 8 commits into from
Apr 18, 2022
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions book/src/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Language-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.

A local `languages.toml` can be created within a `.helix` directory. Its settings will be merged with both the global and default configs.

Changes 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:

```toml
Expand Down
4 changes: 2 additions & 2 deletions helix-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/// Syntax configuration loader based on built-in languages.toml.
pub fn default_syntax_loader() -> crate::syntax::Configuration {
helix_loader::default_lang_config()
helix_loader::config::default_lang_config()
.try_into()
.expect("Could not serialize built-in languages.toml")
}
/// Syntax configuration loader based on user configured languages.toml.
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
helix_loader::user_lang_config()?.try_into()
helix_loader::config::user_lang_config()?.try_into()
}
38 changes: 3 additions & 35 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,41 +46,9 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
/// * Top-most folder containing a root marker if not git repository detected
/// * Current working directory as fallback
pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");

let root = match root {
Some(root) => {
let root = std::path::Path::new(root);
if root.is_absolute() {
root.to_path_buf()
} else {
current_dir.join(root)
}
}
None => current_dir.clone(),
};

let mut top_marker = None;
for ancestor in root.ancestors() {
for marker in root_markers {
if ancestor.join(marker).exists() {
top_marker = Some(ancestor);
break;
}
}
// don't go higher than repo
if ancestor.join(".git").is_dir() {
// Use workspace if detected from marker
return Some(top_marker.unwrap_or(ancestor).to_path_buf());
}
}

// In absence of git repo, use workspace if detected
if top_marker.is_some() {
top_marker.map(|a| a.to_path_buf())
} else {
Some(current_dir)
}
helix_loader::find_root_impl(root, root_markers)
.first()
.cloned()
}

pub use ropey::{Rope, RopeBuilder, RopeSlice};
Expand Down
2 changes: 2 additions & 0 deletions helix-loader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ tree-sitter = "0.20"
libloading = "0.7"
once_cell = "1.9"

log = "0.4"

# cloning/compiling tree-sitter grammars
cc = { version = "1" }
threadpool = { version = "1.0" }
26 changes: 26 additions & 0 deletions helix-loader/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// Default bultin-in languages.toml.
pub fn default_lang_config() -> toml::Value {
toml::from_slice(include_bytes!("../../languages.toml"))
.expect("Could not parse bultin-in languages.toml to valid toml")
}

/// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let config = crate::local_config_dirs()
.into_iter()
.chain([crate::config_dir()].into_iter())
.map(|path| path.join("languages.toml"))
.filter_map(|file| {
std::fs::read(&file)
.map(|config| toml::from_slice(&config))
.ok()
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.chain([default_lang_config()].into_iter())
.fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
crate::merge_toml_values(b, a)
});

Ok(config)
}
2 changes: 1 addition & 1 deletion helix-loader/src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub fn build_grammars() -> Result<()> {
// merged. The `grammar_selection` key of the config is then used to filter
// down all grammars into a subset of the user's choosing.
fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
let config: Configuration = crate::user_lang_config()
let config: Configuration = crate::config::user_lang_config()
.context("Could not parse languages.toml")?
.try_into()?;

Expand Down
53 changes: 37 additions & 16 deletions helix-loader/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod config;
pub mod grammar;

use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
Expand Down Expand Up @@ -36,6 +37,15 @@ pub fn config_dir() -> std::path::PathBuf {
path
}

pub fn local_config_dirs() -> Vec<std::path::PathBuf> {
let directories = find_root_impl(None, &[".helix".to_string()])
.into_iter()
.map(|path| path.join(".helix"))
.collect();
log::debug!("Located configuration folders: {:?}", directories);
directories
}

pub fn cache_dir() -> std::path::PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
Expand All @@ -56,25 +66,36 @@ pub fn log_file() -> std::path::PathBuf {
cache_dir().join("helix.log")
}

/// Default bultin-in languages.toml.
pub fn default_lang_config() -> toml::Value {
toml::from_slice(include_bytes!("../../languages.toml"))
.expect("Could not parse bultin-in languages.toml to valid toml")
}

/// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let def_lang_conf = default_lang_config();
let data = std::fs::read(crate::config_dir().join("languages.toml"));
let user_lang_conf = match data {
Ok(raw) => {
let value = toml::from_slice(&raw)?;
merge_toml_values(def_lang_conf, value)
pub fn find_root_impl(root: Option<&str>, root_markers: &[String]) -> Vec<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let mut directories = Vec::new();

let root = match root {
Some(root) => {
let root = std::path::Path::new(root);
if root.is_absolute() {
root.to_path_buf()
} else {
current_dir.join(root)
}
}
Err(_) => def_lang_conf,
None => current_dir,
};

Ok(user_lang_conf)
for ancestor in root.ancestors() {
// don't go higher than repo
if ancestor.join(".git").is_dir() {
// Use workspace if detected from marker
directories.push(ancestor.to_path_buf());
break;
} else if root_markers
.iter()
.any(|marker| ancestor.join(marker).exists())
{
directories.push(ancestor.to_path_buf());
}
}
directories
}

// right overrides left
Expand Down
33 changes: 26 additions & 7 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,33 @@ pub struct Application {
}

impl Application {
pub fn new(args: Args, config: Config) -> Result<Self, Error> {
pub fn new(args: Args) -> Result<Self, Error> {
Copy link
Member

@dead10ck dead10ck Apr 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry I'm kind of late to the party here. Thanks for the change, @kirawi, this will be super useful!

But this particular part of the change actually presents a problem. I'm working on an integration testing branch, and this change makes it so that you can't pass in a pre-parsed Config object. This was very useful for testing, as it's super easy to just make the struct literal you want to describe the config option you want to test. Now it seems that the config can only come from a toml file. Could we please move this logic back into main?

use helix_view::editor::Action;
let mut compositor = Compositor::new()?;
let size = compositor.size();

let conf_dir = helix_loader::config_dir();
let config_dir = helix_loader::config_dir();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
}

let theme_loader =
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_loader::runtime_dir()));
let config = match std::fs::read_to_string(config_dir.join("config.toml")) {
Ok(config) => toml::from_str(&config)
.map(crate::keymap::merge_keys)
.unwrap_or_else(|err| {
eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
Config::default()
}),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
};

let theme_loader = std::sync::Arc::new(theme::Loader::new(
&config_dir,
&helix_loader::runtime_dir(),
));

let true_color = config.editor.true_color || crate::true_color();
let theme = config
Expand Down Expand Up @@ -98,9 +116,10 @@ impl Application {
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));

let mut compositor = Compositor::new()?;
let config = Arc::new(ArcSwap::from_pointee(config));
let mut editor = Editor::new(
size,
compositor.size(),
theme_loader.clone(),
syn_loader.clone(),
Box::new(Map::new(Arc::clone(&config), |config: &Config| {
Expand Down
30 changes: 2 additions & 28 deletions helix-term/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use anyhow::{Context, Error, Result};
use anyhow::{Context, Result};
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::{Config, ConfigLoadError};
use std::path::PathBuf;

fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
Expand Down Expand Up @@ -109,35 +108,10 @@ FLAGS:
return Ok(0);
}

let conf_dir = helix_loader::config_dir();
if !conf_dir.exists() {
std::fs::create_dir_all(&conf_dir).ok();
}

let config = match Config::load_default() {
Ok(config) => config,
Err(err) => {
match err {
ConfigLoadError::BadConfig(err) => {
eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
Config::default()
}
ConfigLoadError::Error(err) if err.kind() == std::io::ErrorKind::NotFound => {
Config::default()
}
ConfigLoadError::Error(err) => return Err(Error::new(err)),
}
}
};

setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;

// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config).context("unable to create new application")?;
let mut app = Application::new(args).context("unable to create new application")?;

let exit_code = app.run().await?;

Expand Down