Unnamed repository; edit this file 'description' to name the repository.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 IT_HASH: &str = env!("VERSION_AND_GIT_HASH"); static CWD: RwLock<Option<PathBuf>> = RwLock::new(None); static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> = once_cell::sync::Lazy::new(prioritize_runtime_dirs); static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new(); static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new(); // Get the current working directory. // This information is managed internally as the call to std::env::current_dir // might fail if the cwd has been deleted. pub fn current_working_dir() -> PathBuf { if let Some(path) = &*CWD.read().unwrap() { return path.clone(); } let path = std::env::current_dir() .and_then(dunce::canonicalize) .expect("Couldn't determine current working directory"); let mut cwd = CWD.write().unwrap(); *cwd = Some(path.clone()); path } pub fn set_current_working_dir(path: PathBuf) -> std::io::Result<()> { let path = dunce::canonicalize(path)?; std::env::set_current_dir(path.clone())?; let mut cwd = CWD.write().unwrap(); *cwd = Some(path); Ok(()) } pub fn initialize_config_file(specified_file: Option<PathBuf>) { let config_file = specified_file.unwrap_or_else(default_config_file); ensure_parent_dir(&config_file); CONFIG_FILE.set(config_file).ok(); } pub fn initialize_log_file(specified_file: Option<PathBuf>) { let log_file = specified_file.unwrap_or_else(default_log_file); ensure_parent_dir(&log_file); LOG_FILE.set(log_file).ok(); } /// A list of runtime directories from highest to lowest priority /// /// The priority is: /// /// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set) /// 2. subdirectory of user config directory (always included) /// 3. `HELIX_RUNTIME` (if environment variable is set) /// 4. subdirectory of path to helix executable (always included) /// /// Postcondition: returns at least two paths (they might not exist). fn prioritize_runtime_dirs() -> Vec<PathBuf> { const RT_DIR: &str = "runtime"; // Adding higher priority first let mut rt_dirs = Vec::new(); 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 let path = PathBuf::from(dir).parent().unwrap().join(RT_DIR); log::debug!("runtime dir: {}", path.to_string_lossy()); rt_dirs.push(path); } let conf_rt_dir = config_dir().join(RT_DIR); rt_dirs.push(conf_rt_dir); if let Ok(dir) = std::env::var("HELIX_RUNTIME") { rt_dirs.push(dir.into()); } // fallback to location of the executable being run // canonicalize the path in case the executable is symlinked let exe_rt_dir = std::env::current_exe() .ok() .and_then(|path| std::fs::canonicalize(path).ok()) .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR))) .unwrap(); rt_dirs.push(exe_rt_dir); rt_dirs } /// Runtime directories ordered from highest to lowest priority /// /// All directories should be checked when looking for files. /// /// Postcondition: returns at least one path (it might not exist). pub fn runtime_dirs() -> &'static [PathBuf] { &RUNTIME_DIRS } /// Find file with path relative to runtime directory /// /// `rel_path` should be the relative path from within the `runtime/` directory. /// The valid runtime directories are searched in priority order and the first /// file found to exist is returned, otherwise None. fn find_runtime_file(rel_path: &Path) -> Option<PathBuf> { RUNTIME_DIRS.iter().find_map(|rt_dir| { let path = rt_dir.join(rel_path); if path.exists() { Some(path) } else { None } }) } /// Find file with path relative to runtime directory /// /// `rel_path` should be the relative path from within the `runtime/` directory. /// The valid runtime directories are searched in priority order and the first /// file found to exist is returned, otherwise the path to the final attempt /// that failed. pub fn runtime_file(rel_path: &Path) -> PathBuf { find_runtime_file(rel_path).unwrap_or_else(|| { RUNTIME_DIRS .last() .map(|dir| dir.join(rel_path)) .unwrap_or_default() }) } pub fn config_dir() -> 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() -> 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 } pub fn config_file() -> PathBuf { CONFIG_FILE.get().map(|path| path.to_path_buf()).unwrap() } pub fn log_file() -> PathBuf { LOG_FILE.get().map(|path| path.to_path_buf()).unwrap() } pub fn workspace_config_file() -> PathBuf { find_workspace().0.join(".helix").join("config.toml") } pub fn lang_config_file() -> PathBuf { config_dir().join("languages.toml") } pub fn default_log_file() -> PathBuf { cache_dir().join("helix.log") } /// Merge two TOML documents, merging values from `right` onto `left` /// /// When an array exists in both `left` and `right`, `right`'s array is /// used. When a table exists in both `left` and `right`, the merged table /// consists of all keys in `left`'s table unioned with all keys in `right` /// with the values of `right` being merged recursively onto values of /// `left`. /// /// `merge_toplevel_arrays` controls whether a top-level array in the TOML /// document is merged instead of overridden. This is useful for TOML /// documents that use a top-level array of values like the `languages.toml`, /// where one usually wants to override or add to the array instead of /// replacing it altogether. pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> 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)) => { // The top-level arrays should be merged but nested arrays should // act as overrides. For the `languages.toml` config, this means // that you can specify a sub-set of languages in an overriding // `languages.toml` but that nested arrays like Language Server // arguments are replaced instead of merged. if merge_depth > 0 { 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, merge_depth - 1), None => rvalue, }; left_items.push(mvalue); } Value::Array(left_items) } else { Value::Array(right_items) } } (Value::Table(mut left_map), Value::Table(right_map)) => { if merge_depth > 0 { for (rname, rvalue) in right_map { match left_map.remove(&rname) { Some(lvalue) => { let merged_value = merge_toml_values(lvalue, rvalue, merge_depth - 1); left_map.insert(rname, merged_value); } None => { left_map.insert(rname, rvalue); } } } Value::Table(left_map) } else { Value::Table(right_map) } } // Catch everything else we didn't handle, and use the right value (_, value) => value, } } /// Finds the current workspace folder. /// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root /// /// This function starts searching the FS upward from the CWD /// and returns the first directory that contains either `.git` or `.helix`. /// If no workspace was found returns (CWD, true). /// Otherwise (workspace, false) is returned pub fn find_workspace() -> (PathBuf, bool) { let current_dir = current_working_dir(); for ancestor in current_dir.ancestors() { if ancestor.join(".git").exists() || ancestor.join(".helix").exists() { return (ancestor.to_owned(), false); } } (current_dir, true) } fn default_config_file() -> PathBuf { config_dir().join("config.toml") } fn ensure_parent_dir(path: &Path) { if let Some(parent) = path.parent() { if !parent.exists() { std::fs::create_dir_all(parent).ok(); } } } #[cfg(test)] mod merge_toml_tests { use std::str; use super::{current_working_dir, merge_toml_values, set_current_working_dir}; use toml::Value; #[test] fn current_dir_is_set() { let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap(); let cwd = current_working_dir(); assert_ne!(cwd, new_path); set_current_working_dir(new_path.clone()).expect("Couldn't set new path"); let cwd = current_working_dir(); assert_eq!(cwd, new_path); } #[test] fn language_toml_map_merges() { const USER: &str = r#" [[language]] name = "nix" test = "bbb" indent = { tab-width = 4, unit = " ", test = "aaa" } "#; let base = include_bytes!("../../languages.toml"); let base = str::from_utf8(base).expect("Couldn't parse built-in languages config"); let base: Value = toml::from_str(base).expect("Couldn't parse built-in languages config"); let user: Value = toml::from_str(USER).unwrap(); let merged = merge_toml_values(base, user, 3); 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(), "#"); } #[test] fn language_toml_nested_array_merges() { const USER: &str = r#" [[language]] name = "typescript" language-server = { command = "deno", args = ["lsp"] } "#; let base = include_bytes!("../../languages.toml"); let base = str::from_utf8(base).expect("Couldn't parse built-in languages config"); let base: Value = toml::from_str(base).expect("Couldn't parse built-in languages config"); let user: Value = toml::from_str(USER).unwrap(); let merged = merge_toml_values(base, user, 3); let languages = merged.get("language").unwrap().as_array().unwrap(); let ts = languages .iter() .find(|v| v.get("name").unwrap().as_str().unwrap() == "typescript") .unwrap(); assert_eq!( ts.get("language-server") .unwrap() .get("args") .unwrap() .as_array() .unwrap(), &vec![Value::String("lsp".into())] ) } } |