Skip to content

Commit f6627f5

Browse files
committed
support project-specific language configuration
1 parent a1e6481 commit f6627f5

File tree

2 files changed

+88
-54
lines changed

2 files changed

+88
-54
lines changed

helix-core/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ pub fn config_dir() -> std::path::PathBuf {
9292
path
9393
}
9494

95+
/// Searches for the local `.helix` directory by searching for the root `.git` directory,
96+
/// using the CWD if it can't find one.
97+
pub fn local_config_dir() -> std::path::PathBuf {
98+
let root = find_root(None)
99+
.unwrap_or_else(|| std::env::current_dir().expect("unable to determine current directory"));
100+
root.join(".helix")
101+
}
102+
95103
pub fn cache_dir() -> std::path::PathBuf {
96104
// TODO: allow env var override
97105
let strategy = choose_base_strategy().expect("Unable to find the config directory!");

helix-term/src/application.rs

Lines changed: 80 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -52,77 +52,110 @@ pub struct Application {
5252
impl Application {
5353
pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
5454
use helix_view::editor::Action;
55-
let mut compositor = Compositor::new()?;
56-
let size = compositor.size();
57-
58-
let conf_dir = helix_core::config_dir();
5955

60-
let theme_loader =
61-
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
62-
63-
// load default and user config, and merge both
64-
let builtin_err_msg =
65-
"Could not parse built-in languages.toml, something must be very wrong";
66-
let def_lang_conf: toml::Value =
67-
toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg);
68-
let def_syn_loader_conf: helix_core::syntax::Configuration =
69-
def_lang_conf.clone().try_into().expect(builtin_err_msg);
70-
let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
71-
.ok()
72-
.map(|raw| toml::from_slice(&raw));
73-
let lang_conf = match user_lang_conf {
74-
Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
75-
Some(err @ Err(_)) => err,
76-
None => Ok(def_lang_conf),
77-
};
78-
79-
let theme = if let Some(theme) = &config.theme {
80-
match theme_loader.load(theme) {
81-
Ok(theme) => theme,
82-
Err(e) => {
83-
log::warn!("failed to load theme `{}` - {}", theme, e);
84-
theme_loader.default()
85-
}
86-
}
87-
} else {
88-
theme_loader.default()
89-
};
56+
// These configuration directories can contain `config.toml` and `languages.toml`.
57+
// `local_config_dir` is a `.helix` folder within the projec directory.
58+
let config_dir = helix_core::config_dir();
59+
let local_config_dir = helix_core::local_config_dir();
60+
61+
// Config override order: local -> global -> default.
62+
// Read and parse the `languages.toml` files as TOML objects.
63+
let default_lang_config: toml::Value =
64+
toml::from_slice(include_bytes!("../../languages.toml"))
65+
.expect("failed to read the default `languages.toml`");
66+
let lang_config =
67+
{
68+
let local_config = match std::fs::read(local_config_dir.join("languages.toml")) {
69+
Ok(config) => toml::from_slice(&config)
70+
.expect("failed to read the local `languages.toml`"),
71+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
72+
toml::Value::Table(toml::value::Table::default())
73+
}
74+
Err(err) => return Err(Error::new(err)),
75+
};
76+
let global_config = match std::fs::read(config_dir.join("languages.toml")) {
77+
Ok(config) => toml::from_slice(&config)
78+
.expect("failed to read the global `languages.toml`"),
79+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
80+
toml::Value::Table(toml::value::Table::default())
81+
}
82+
Err(err) => return Err(Error::new(err)),
83+
};
9084

91-
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
92-
.and_then(|conf| conf.try_into())
93-
.unwrap_or_else(|err| {
85+
merge_toml_values(
86+
default_lang_config.clone(),
87+
merge_toml_values(global_config, local_config),
88+
)
89+
};
90+
91+
// Convert previous `toml::Value`s into the config type.
92+
let default_syn_loader_config: helix_core::syntax::Configuration = default_lang_config
93+
.try_into()
94+
.expect("failed to parse the default `languages.toml`");
95+
let syn_loader_config: helix_core::syntax::Configuration =
96+
lang_config.try_into().unwrap_or_else(|err| {
9497
eprintln!("Bad language config: {}", err);
9598
eprintln!("Press <ENTER> to continue with default language config");
9699
use std::io::Read;
97100
// This waits for an enter press.
98101
let _ = std::io::stdin().read(&mut []);
99-
def_syn_loader_conf
102+
default_syn_loader_config
100103
});
101-
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
104+
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_config));
102105

106+
// Initialize rendering.
107+
let theme_loader =
108+
std::sync::Arc::new(theme::Loader::new(&config_dir, &helix_core::runtime_dir()));
109+
let mut compositor = Compositor::new()?;
103110
let mut editor = Editor::new(
104-
size,
111+
compositor.size(),
105112
theme_loader.clone(),
106113
syn_loader.clone(),
107114
config.editor.clone(),
108115
);
109116

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

121+
// Grab and load the user's default theme.
122+
let theme = if let Some(theme) = &config.theme {
123+
match theme_loader.load(theme) {
124+
Ok(theme) => theme,
125+
Err(e) => {
126+
log::warn!("failed to load theme `{}` - {}", theme, e);
127+
theme_loader.default()
128+
}
129+
}
130+
} else {
131+
theme_loader.default()
132+
};
133+
editor.set_theme(theme);
134+
135+
#[cfg(windows)]
136+
let signals = futures_util::stream::empty();
137+
#[cfg(not(windows))]
138+
let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
139+
140+
// Handle CLI arguments.
113141
if args.load_tutor {
114142
let path = helix_core::runtime_dir().join("tutor.txt");
115143
editor.open(path, Action::VerticalSplit)?;
116144
// Unset path to prevent accidentally saving to the original tutor file.
117145
doc_mut!(editor).set_path(None)?;
118146
} else if !args.files.is_empty() {
119-
let first = &args.files[0]; // we know it's not empty
147+
// File paths passed as e.g. `hx foo.rs bar.rs`
148+
// SAFETY: The file count is already known to be non-zero.
149+
let first = &args.files[0];
150+
151+
// If the first argument is a directory, then only the file picker at that
152+
// path is opened. Otherwise, all files are opened in separate vertical splits.
120153
if first.is_dir() {
121154
std::env::set_current_dir(&first)?;
122155
editor.new_file(Action::VerticalSplit);
123156
compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
124157
} else {
125-
let nr_of_files = args.files.len();
158+
let file_count = args.files.len();
126159
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
127160
for file in args.files {
128161
if file.is_dir() {
@@ -133,9 +166,11 @@ impl Application {
133166
editor.open(file.to_path_buf(), Action::Load)?;
134167
}
135168
}
136-
editor.set_status(format!("Loaded {} files.", nr_of_files));
169+
editor.set_status(format!("Loaded {} files.", file_count));
137170
}
138171
} else if stdin().is_tty() {
172+
// If no arguments are passed and there is no stdin piping, then only a scratch
173+
// buffer is opened.
139174
editor.new_file(Action::VerticalSplit);
140175
} else if cfg!(target_os = "macos") {
141176
// On Linux and Windows, we allow the output of a command to be piped into the new buffer.
@@ -148,14 +183,7 @@ impl Application {
148183
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
149184
}
150185

151-
editor.set_theme(theme);
152-
153-
#[cfg(windows)]
154-
let signals = futures_util::stream::empty();
155-
#[cfg(not(windows))]
156-
let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
157-
158-
let app = Self {
186+
Ok(Self {
159187
compositor,
160188
editor,
161189

@@ -167,9 +195,7 @@ impl Application {
167195
signals,
168196
jobs: Jobs::new(),
169197
lsp_progress: LspProgressMap::new(),
170-
};
171-
172-
Ok(app)
198+
})
173199
}
174200

175201
fn render(&mut self) {

0 commit comments

Comments
 (0)