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

Local #1

Open
wants to merge 2 commits into
base: local
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions book/src/lang-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ specific indent settings, etc see the default
{{#include ./generated/lang-support.md}}

[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml

# Local Configuration

A local `languages.toml` can be created within a `.helix` directory. Its settings will be merged with both the global and default configs.
153 changes: 117 additions & 36 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,51 +37,132 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}

/// Find project root.
///
/// Order of detection:
/// * Top-most folder containing a root marker in current git repository
/// * Git repostory root if no marker detected
/// * 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)
pub fn runtime_dir() -> std::path::PathBuf {
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
return dir.into();
}

const RT_DIR: &str = "runtime";
let conf_dir = config_dir().join(RT_DIR);
if conf_dir.exists() {
return conf_dir;
}

if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
}

// fallback to location of the executable being run
std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
.unwrap()
}

pub fn config_dir() -> std::path::PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.config_dir();
path.push("helix");
path
}

pub fn cache_dir() -> std::path::PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.cache_dir();
path.push("helix");
path
}

// right overrides left
pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
use toml::Value;

fn get_name(v: &Value) -> Option<&str> {
v.get("name").and_then(Value::as_str)
}

match (left, right) {
(Value::Array(mut left_items), Value::Array(right_items)) => {
left_items.reserve(right_items.len());
for rvalue in right_items {
let lvalue = get_name(&rvalue)
.and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname)))
.map(|lpos| left_items.remove(lpos));
let mvalue = match lvalue {
Some(lvalue) => merge_toml_values(lvalue, rvalue),
None => rvalue,
};
left_items.push(mvalue);
}
Value::Array(left_items)
}
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;
(Value::Table(mut left_map), Value::Table(right_map)) => {
for (rname, rvalue) in right_map {
match left_map.remove(&rname) {
Some(lvalue) => {
let merged_value = merge_toml_values(lvalue, rvalue);
left_map.insert(rname, merged_value);
}
None => {
left_map.insert(rname, rvalue);
}
}
}
Value::Table(left_map)
}
// 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());
}
// Catch everything else we didn't handle, and use the right value
(_, value) => value,
}
}

#[cfg(test)]
mod merge_toml_tests {
use super::merge_toml_values;

// 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)
#[test]
fn language_tomls() {
use toml::Value;

const USER: &str = "
[[language]]
name = \"nix\"
test = \"bbb\"
indent = { tab-width = 4, unit = \" \", test = \"aaa\" }
";

let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
.expect("Couldn't parse built-in languages config");
let user: Value = toml::from_str(USER).unwrap();

let merged = merge_toml_values(base, user);
let languages = merged.get("language").unwrap().as_array().unwrap();
let nix = languages
.iter()
.find(|v| v.get("name").unwrap().as_str().unwrap() == "nix")
.unwrap();
let nix_indent = nix.get("indent").unwrap();

// We changed tab-width and unit in indent so check them if they are the new values
assert_eq!(
nix_indent.get("tab-width").unwrap().as_integer().unwrap(),
4
);
assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " ");
// We added a new keys, so check them
assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb");
assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa");
// We didn't change comment-token so it should be same
assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#");
}
}

pub use etcetera::home_dir;

use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};

pub use ropey::{Rope, RopeBuilder, RopeSlice};

// pub use tendril::StrTendril as Tendril;
Expand Down
1 change: 1 addition & 0 deletions helix-loader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ etcetera = "0.3"
tree-sitter = "0.20"
libloading = "0.7"
once_cell = "1.9"
log = "0.4"

# cloning/compiling tree-sitter grammars
cc = { version = "1" }
Expand Down
78 changes: 69 additions & 9 deletions helix-loader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,80 @@ pub fn default_lang_config() -> toml::Value {

/// 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)
let config = 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| {
merge_toml_values(b, a)
});

Ok(config)
}

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
}

/// Find project root.
///
/// Order of detection:
/// * Top-most folder containing a root marker in current git repository
/// * Git repostory root if no marker detected
/// * 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> {
find_root_impl(root, root_markers).first().cloned()
}

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;
}

for marker in root_markers {
if ancestor.join(marker).exists() {
directories.push(ancestor.to_path_buf());
break;
}
}
}
directories
}

// right overrides left
/// right overrides left
pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
use toml::Value;

Expand Down
1 change: 1 addition & 0 deletions helix-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ homepage = "https://helix-editor.com"

[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }

anyhow = "1.0"
futures-executor = "0.3"
Expand Down
3 changes: 2 additions & 1 deletion helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use crate::{
Call, Error, OffsetEncoding, Result,
};

use helix_core::{find_root, ChangeSet, Rope};
use helix_core::{ChangeSet, Rope};
use helix_loader::find_root;
use jsonrpc_core as jsonrpc;
use lsp_types as lsp;
use serde_json::Value;
Expand Down
35 changes: 27 additions & 8 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, mut config: Config) -> Result<Self, Error> {
pub fn new(args: Args) -> Result<Self, Error> {
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 mut 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,8 +116,9 @@ impl Application {
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));

let mut compositor = Compositor::new()?;
let mut editor = Editor::new(
size,
compositor.size(),
theme_loader.clone(),
syn_loader.clone(),
config.editor.clone(),
Expand All @@ -114,7 +133,7 @@ impl Application {
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
} else if args.edit_config {
let path = conf_dir.join("config.toml");
let path = config_dir.join("config.toml");
editor.open(path, Action::VerticalSplit)?;
} else if !args.files.is_empty() {
let first = &args.files[0].0; // we know it's not empty
Expand Down
5 changes: 4 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub use lsp::*;
pub use typed::*;

use helix_core::{
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
comment, coords_at_pos, find_first_non_whitespace_char, graphemes,
history::UndoKind,
increment::date_time::DateTimeIncrementor,
increment::{number::NumberIncrementor, Increment},
Expand All @@ -24,6 +24,9 @@ use helix_core::{
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
Transaction,
};

use helix_loader::find_root;

use helix_view::{
clipboard::ClipboardType,
document::{Mode, SCRATCH_BUFFER_NAME},
Expand Down
Loading