on aa00a47 help sign in
261 lines 9.2 kB history raw
1pub mod config;
2pub mod grammar;
4use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
5use std::path::PathBuf;
7pub static RUNTIME_DIR: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(runtime_dir);
9static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
11pub fn initialize_config_file(specified_file: Option<PathBuf>) {
12 let config_file = specified_file.unwrap_or_else(|| {
13 let config_dir = config_dir();
15 if !config_dir.exists() {
16 std::fs::create_dir_all(&config_dir).ok();
17 }
19 config_dir.join("config.toml")
20 });
22 // We should only initialize this value once.
23 CONFIG_FILE.set(config_file).ok();
26pub fn runtime_dir() -> PathBuf {
27 if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
28 return dir.into();
29 }
31 if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
32 // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
33 let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
34 log::debug!("runtime dir: {}", path.to_string_lossy());
35 return path;
36 }
38 const RT_DIR: &str = "runtime";
39 let conf_dir = config_dir().join(RT_DIR);
40 if conf_dir.exists() {
41 return conf_dir;
42 }
44 // fallback to location of the executable being run
45 // canonicalize the path in case the executable is symlinked
46 std::env::current_exe()
47 .ok()
48 .and_then(|path| std::fs::canonicalize(path).ok())
49 .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
50 .unwrap()
53pub fn config_dir() -> PathBuf {
54 // TODO: allow env var override
55 let strategy = choose_base_strategy().expect("Unable to find the config directory!");
56 let mut path = strategy.config_dir();
57 path.push("helix");
58 path
61pub fn local_config_dirs() -> Vec<PathBuf> {
62 let directories = find_root_impl(None, &[".helix".to_string()])
63 .into_iter()
64 .map(|path| path.join(".helix"))
65 .collect();
66 log::debug!("Located configuration folders: {:?}", directories);
67 directories
70pub fn cache_dir() -> PathBuf {
71 // TODO: allow env var override
72 let strategy = choose_base_strategy().expect("Unable to find the config directory!");
73 let mut path = strategy.cache_dir();
74 path.push("helix");
75 path
78pub fn config_file() -> PathBuf {
79 CONFIG_FILE
80 .get()
81 .map(|path| path.to_path_buf())
82 .unwrap_or_else(|| config_dir().join("config.toml"))
85pub fn lang_config_file() -> PathBuf {
86 config_dir().join("languages.toml")
89pub fn log_file() -> PathBuf {
90 cache_dir().join("helix.log")
93pub fn find_root_impl(root: Option<&str>, root_markers: &[String]) -> Vec<PathBuf> {
94 let current_dir = std::env::current_dir().expect("unable to determine current directory");
95 let mut directories = Vec::new();
97 let root = match root {
98 Some(root) => {
99 let root = std::path::Path::new(root);
100 if root.is_absolute() {
101 root.to_path_buf()
102 } else {
103 current_dir.join(root)
104 }
105 }
106 None => current_dir,
107 };
109 for ancestor in root.ancestors() {
110 // don't go higher than repo
111 if ancestor.join(".git").is_dir() {
112 // Use workspace if detected from marker
113 directories.push(ancestor.to_path_buf());
114 break;
115 } else if root_markers
116 .iter()
117 .any(|marker| ancestor.join(marker).exists())
118 {
119 directories.push(ancestor.to_path_buf());
120 }
121 }
122 directories
125/// Merge two TOML documents, merging values from `right` onto `left`
126///
127/// When an array exists in both `left` and `right`, `right`'s array is
128/// used. When a table exists in both `left` and `right`, the merged table
129/// consists of all keys in `left`'s table unioned with all keys in `right`
130/// with the values of `right` being merged recursively onto values of
131/// `left`.
132///
133/// `merge_toplevel_arrays` controls whether a top-level array in the TOML
134/// document is merged instead of overridden. This is useful for TOML
135/// documents that use a top-level array of values like the `languages.toml`,
136/// where one usually wants to override or add to the array instead of
137/// replacing it altogether.
138pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value {
139 use toml::Value;
141 fn get_name(v: &Value) -> Option<&str> {
142 v.get("name").and_then(Value::as_str)
143 }
145 match (left, right) {
146 (Value::Array(mut left_items), Value::Array(right_items)) => {
147 // The top-level arrays should be merged but nested arrays should
148 // act as overrides. For the `languages.toml` config, this means
149 // that you can specify a sub-set of languages in an overriding
150 // `languages.toml` but that nested arrays like Language Server
151 // arguments are replaced instead of merged.
152 if merge_depth > 0 {
153 left_items.reserve(right_items.len());
154 for rvalue in right_items {
155 let lvalue = get_name(&rvalue)
156 .and_then(|rname| {
157 left_items.iter().position(|v| get_name(v) == Some(rname))
158 })
159 .map(|lpos| left_items.remove(lpos));
160 let mvalue = match lvalue {
161 Some(lvalue) => merge_toml_values(lvalue, rvalue, merge_depth - 1),
162 None => rvalue,
163 };
164 left_items.push(mvalue);
165 }
166 Value::Array(left_items)
167 } else {
168 Value::Array(right_items)
169 }
170 }
171 (Value::Table(mut left_map), Value::Table(right_map)) => {
172 if merge_depth > 0 {
173 for (rname, rvalue) in right_map {
174 match left_map.remove(&rname) {
175 Some(lvalue) => {
176 let merged_value = merge_toml_values(lvalue, rvalue, merge_depth - 1);
177 left_map.insert(rname, merged_value);
178 }
179 None => {
180 left_map.insert(rname, rvalue);
181 }
182 }
183 }
184 Value::Table(left_map)
185 } else {
186 Value::Table(right_map)
187 }
188 }
189 // Catch everything else we didn't handle, and use the right value
190 (_, value) => value,
191 }
194#[cfg(test)]
195mod merge_toml_tests {
196 use super::merge_toml_values;
197 use toml::Value;
199 #[test]
200 fn language_toml_map_merges() {
201 const USER: &str = r#"
202 [[language]]
203 name = "nix"
204 test = "bbb"
205 indent = { tab-width = 4, unit = " ", test = "aaa" }
206 "#;
208 let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
209 .expect("Couldn't parse built-in languages config");
210 let user: Value = toml::from_str(USER).unwrap();
212 let merged = merge_toml_values(base, user, 3);
213 let languages = merged.get("language").unwrap().as_array().unwrap();
214 let nix = languages
215 .iter()
216 .find(|v| v.get("name").unwrap().as_str().unwrap() == "nix")
217 .unwrap();
218 let nix_indent = nix.get("indent").unwrap();
220 // We changed tab-width and unit in indent so check them if they are the new values
221 assert_eq!(
222 nix_indent.get("tab-width").unwrap().as_integer().unwrap(),
223 4
224 );
225 assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " ");
226 // We added a new keys, so check them
227 assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb");
228 assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa");
229 // We didn't change comment-token so it should be same
230 assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#");
231 }
233 #[test]
234 fn language_toml_nested_array_merges() {
235 const USER: &str = r#"
236 [[language]]
237 name = "typescript"
238 language-server = { command = "deno", args = ["lsp"] }
239 "#;
241 let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
242 .expect("Couldn't parse built-in languages config");
243 let user: Value = toml::from_str(USER).unwrap();
245 let merged = merge_toml_values(base, user, 3);
246 let languages = merged.get("language").unwrap().as_array().unwrap();
247 let ts = languages
248 .iter()
249 .find(|v| v.get("name").unwrap().as_str().unwrap() == "typescript")
250 .unwrap();
251 assert_eq!(
252 ts.get("language-server")
253 .unwrap()
254 .get("args")
255 .unwrap()
256 .as_array()
257 .unwrap(),
258 &vec![Value::String("lsp".into())]
259 )
260 }