Skip to content

Commit

Permalink
support project-specific language configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
kirawi committed Dec 11, 2021
1 parent a1e6481 commit d34a7d9
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 54 deletions.
8 changes: 8 additions & 0 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ pub fn config_dir() -> std::path::PathBuf {
path
}

/// Searches for the local `.helix` directory by searching for the root `.git` directory,
/// using the CWD if it can't find one.
pub fn local_config_dir() -> std::path::PathBuf {
let root = find_root(None)
.unwrap_or_else(|| std::env::current_dir().expect("unable to determine current directory"));
root.join(".helix")
}

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 Down
134 changes: 80 additions & 54 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,77 +52,110 @@ pub struct Application {
impl Application {
pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
use helix_view::editor::Action;
let mut compositor = Compositor::new()?;
let size = compositor.size();

let conf_dir = helix_core::config_dir();

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

// load default and user config, and merge both
let builtin_err_msg =
"Could not parse built-in languages.toml, something must be very wrong";
let def_lang_conf: toml::Value =
toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg);
let def_syn_loader_conf: helix_core::syntax::Configuration =
def_lang_conf.clone().try_into().expect(builtin_err_msg);
let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
.ok()
.map(|raw| toml::from_slice(&raw));
let lang_conf = match user_lang_conf {
Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
Some(err @ Err(_)) => err,
None => Ok(def_lang_conf),
};

let theme = if let Some(theme) = &config.theme {
match theme_loader.load(theme) {
Ok(theme) => theme,
Err(e) => {
log::warn!("failed to load theme `{}` - {}", theme, e);
theme_loader.default()
}
}
} else {
theme_loader.default()
};
// These configuration directories can contain `config.toml` and `languages.toml`.
// `local_config_dir` is a `.helix` folder within the projec directory.
let config_dir = helix_core::config_dir();
let local_config_dir = helix_core::local_config_dir();

// Config override order: local -> global -> default.
// Read and parse the `languages.toml` files as TOML objects.
let default_lang_config: toml::Value =
toml::from_slice(include_bytes!("../../languages.toml"))
.expect("failed to read the default `languages.toml`");
let lang_config =
{
let local_config = match std::fs::read(local_config_dir.join("languages.toml")) {
Ok(config) => toml::from_slice(&config)
.expect("failed to read the local `languages.toml`"),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
toml::from_str("").unwrap()
}
Err(err) => return Err(Error::new(err)),
};
let global_config = match std::fs::read(config_dir.join("languages.toml")) {
Ok(config) => toml::from_slice(&config)
.expect("failed to read the global `languages.toml`"),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
toml::from_str("").unwrap()
}
Err(err) => return Err(Error::new(err)),
};

let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.and_then(|conf| conf.try_into())
.unwrap_or_else(|err| {
merge_toml_values(
default_lang_config.clone(),
merge_toml_values(global_config, local_config),
)
};

// Convert previous `toml::Value`s into the config type.
let default_syn_loader_config: helix_core::syntax::Configuration = default_lang_config
.try_into()
.expect("failed to parse the default `languages.toml`");
let syn_loader_config: helix_core::syntax::Configuration =
lang_config.try_into().unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
eprintln!("Press <ENTER> to continue with default language config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
def_syn_loader_conf
default_syn_loader_config
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_config));

// Initialize rendering.
let theme_loader =
std::sync::Arc::new(theme::Loader::new(&config_dir, &helix_core::runtime_dir()));
let mut compositor = Compositor::new()?;
let mut editor = Editor::new(
size,
compositor.size(),
theme_loader.clone(),
syn_loader.clone(),
config.editor.clone(),
);

// Initialize the UI.
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
compositor.push(editor_view);

// Grab and load the user's default theme.
let theme = if let Some(theme) = &config.theme {
match theme_loader.load(theme) {
Ok(theme) => theme,
Err(e) => {
log::warn!("failed to load theme `{}` - {}", theme, e);
theme_loader.default()
}
}
} else {
theme_loader.default()
};
editor.set_theme(theme);

#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;

// Handle CLI arguments.
if args.load_tutor {
let path = helix_core::runtime_dir().join("tutor.txt");
editor.open(path, Action::VerticalSplit)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
} else if !args.files.is_empty() {
let first = &args.files[0]; // we know it's not empty
// File paths passed as e.g. `hx foo.rs bar.rs`
// SAFETY: The file count is already known to be non-zero.
let first = &args.files[0];

// If the first argument is a directory, then only the file picker at that
// path is opened. Otherwise, all files are opened in separate vertical splits.
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
} else {
let nr_of_files = args.files.len();
let file_count = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
for file in args.files {
if file.is_dir() {
Expand All @@ -133,9 +166,11 @@ impl Application {
editor.open(file.to_path_buf(), Action::Load)?;
}
}
editor.set_status(format!("Loaded {} files.", nr_of_files));
editor.set_status(format!("Loaded {} files.", file_count));
}
} else if stdin().is_tty() {
// If no arguments are passed and there is no stdin piping, then only a scratch
// buffer is opened.
editor.new_file(Action::VerticalSplit);
} else if cfg!(target_os = "macos") {
// On Linux and Windows, we allow the output of a command to be piped into the new buffer.
Expand All @@ -148,14 +183,7 @@ impl Application {
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
}

editor.set_theme(theme);

#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;

let app = Self {
Ok(Self {
compositor,
editor,

Expand All @@ -167,9 +195,7 @@ impl Application {
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
};

Ok(app)
})
}

fn render(&mut self) {
Expand Down

0 comments on commit d34a7d9

Please sign in to comment.