diff options
author | Matan Kushner <hello@matchai.me> | 2019-10-04 22:30:46 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-04 22:30:46 +0900 |
commit | 05210b9510b797f7738d5b2d51e8a6877f2d5283 (patch) | |
tree | 7399401dba9373f61035dbbd055f4137cd20f705 /src/modules | |
parent | e90a3768da7882db092b38d141cf8e19fabbee56 (diff) |
refactor: Go from Rust workspaces to a package with nested packages (#480)
Diffstat (limited to 'src/modules')
-rw-r--r-- | src/modules/aws.rs | 27 | ||||
-rw-r--r-- | src/modules/battery.rs | 102 | ||||
-rw-r--r-- | src/modules/character.rs | 59 | ||||
-rw-r--r-- | src/modules/cmd_duration.rs | 101 | ||||
-rw-r--r-- | src/modules/directory.rs | 312 | ||||
-rw-r--r-- | src/modules/dotnet.rs | 314 | ||||
-rw-r--r-- | src/modules/env_var.rs | 43 | ||||
-rw-r--r-- | src/modules/git_branch.rs | 65 | ||||
-rw-r--r-- | src/modules/git_state.rs | 162 | ||||
-rw-r--r-- | src/modules/git_status.rs | 196 | ||||
-rw-r--r-- | src/modules/golang.rs | 82 | ||||
-rw-r--r-- | src/modules/hostname.rs | 41 | ||||
-rw-r--r-- | src/modules/java.rs | 105 | ||||
-rw-r--r-- | src/modules/jobs.rs | 34 | ||||
-rw-r--r-- | src/modules/kubernetes.rs | 182 | ||||
-rw-r--r-- | src/modules/line_break.rs | 15 | ||||
-rw-r--r-- | src/modules/memory_usage.rs | 91 | ||||
-rw-r--r-- | src/modules/mod.rs | 68 | ||||
-rw-r--r-- | src/modules/nix_shell.rs | 56 | ||||
-rw-r--r-- | src/modules/nodejs.rs | 49 | ||||
-rw-r--r-- | src/modules/package.rs | 166 | ||||
-rw-r--r-- | src/modules/python.rs | 122 | ||||
-rw-r--r-- | src/modules/ruby.rs | 61 | ||||
-rw-r--r-- | src/modules/rust.rs | 289 | ||||
-rw-r--r-- | src/modules/time.rs | 105 | ||||
-rw-r--r-- | src/modules/username.rs | 53 |
26 files changed, 2900 insertions, 0 deletions
diff --git a/src/modules/aws.rs b/src/modules/aws.rs new file mode 100644 index 000000000..fb8fabfa7 --- /dev/null +++ b/src/modules/aws.rs @@ -0,0 +1,27 @@ +use std::env; + +use super::{Context, Module}; + +use crate::config::RootModuleConfig; +use crate::configs::aws::AwsConfig; + +pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { + const AWS_PREFIX: &str = "on "; + + let aws_profile = env::var("AWS_PROFILE").ok()?; + if aws_profile.is_empty() { + return None; + } + + let mut module = context.new_module("aws"); + let config: AwsConfig = AwsConfig::try_load(module.config); + + module.set_style(config.style); + + module.get_prefix().set_value(AWS_PREFIX); + + module.create_segment("symbol", &config.symbol); + module.create_segment("profile", &config.profile.with_value(&aws_profile)); + + Some(module) +} diff --git a/src/modules/battery.rs b/src/modules/battery.rs new file mode 100644 index 000000000..e67280297 --- /dev/null +++ b/src/modules/battery.rs @@ -0,0 +1,102 @@ +use super::{Context, Module}; +use crate::config::RootModuleConfig; +use crate::configs::battery::BatteryConfig; + +/// Creates a module for the battery percentage and charging state +pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { + // TODO: Update when v1.0 printing refactor is implemented to only + // print escapes in a prompt context. + let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); + let percentage_char = match shell.as_str() { + "zsh" => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc` + _ => "%", + }; + + let battery_status = get_battery_status()?; + let BatteryStatus { state, percentage } = battery_status; + + let mut module = context.new_module("battery"); + let battery_config: BatteryConfig = BatteryConfig::try_load(module.config); + + // Parse config under `display` + let display_styles = &battery_config.display; + let display_style = display_styles + .iter() + .find(|display_style| percentage <= display_style.threshold as f32); + + if let Some(display_style) = display_style { + // Set style based on percentage + module.set_style(display_style.style); + module.get_prefix().set_value(""); + + match state { + battery::State::Full => { + module.create_segment("full_symbol", &battery_config.full_symbol); + } + battery::State::Charging => { + module.create_segment("charging_symbol", &battery_config.charging_symbol); + } + battery::State::Discharging => { + module.create_segment("discharging_symbol", &battery_config.discharging_symbol); + } + battery::State::Unknown => { + log::debug!("Unknown detected"); + if let Some(unknown_symbol) = battery_config.unknown_symbol { + module.create_segment("unknown_symbol", &unknown_symbol); + } + } + battery::State::Empty => { + if let Some(empty_symbol) = battery_config.empty_symbol { + module.create_segment("empty_symbol", &empty_symbol); + } + } + _ => { + log::debug!("Unhandled battery state `{}`", state); + return None; + } + } + + let mut percent_string = Vec::<String>::with_capacity(2); + // Round the percentage to a whole number + percent_string.push(percentage.round().to_string()); + percent_string.push(percentage_char.to_string()); + module.create_segment( + "percentage", + &battery_config + .percentage + .with_value(percent_string.join("").as_ref()), + ); + + Some(module) + } else { + None + } +} + +fn get_battery_status() -> Option<BatteryStatus> { + let battery_manager = battery::Manager::new().ok()?; + match battery_manager.batteries().ok()?.next() { + Some(Ok(battery)) => { + log::debug!("Battery found: {:?}", battery); + let battery_status = BatteryStatus { + percentage: battery.state_of_charge().value * 100.0, + state: battery.state(), + }; + + Some(battery_status) + } + Some(Err(e)) => { + log::debug!("Unable to access battery information:\n{}", &e); + None + } + None => { + log::debug!("No batteries found"); + None + } + } +} + +struct BatteryStatus { + percentage: f32, + state: battery::State, +} diff --git a/src/modules/character.rs b/src/modules/character.rs new file mode 100644 index 000000000..4925147cb --- /dev/null +++ b/src/modules/character.rs @@ -0,0 +1,59 @@ +use super::{Context, Module}; + +use crate::config::RootModuleConfig; +use crate::configs::character::CharacterConfig; + +/// Creates a module for the prompt character +/// +/// The character segment prints an arrow character in a color dependant on the exit- +/// code of the last executed command: +/// - If the exit-code was "0", the arrow will be formatted with `style_success` +/// (green by default) +/// - If the exit-code was anything else, the arrow will be formatted with +/// `style_failure` (red by default) +pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { + enum ShellEditMode { + Normal, + Insert, + }; + const ASSUMED_MODE: ShellEditMode = ShellEditMode::Insert; + // TODO: extend config to more modes + + let mut module = context.new_module("character"); + let config: CharacterConfig = CharacterConfig::try_load(module.config); + module.get_prefix().set_value(""); + + let arguments = &context.arguments; + let exit_success = arguments.value_of("status_code").unwrap_or("0") == "0"; + let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); + let keymap = arguments.value_of("keymap").unwrap_or("viins"); + + // Match shell "keymap" names to normalized vi modes + // NOTE: in vi mode, fish reports normal mode as "default". + // Unfortunately, this is also the name of the non-vi default mode. + // We do some environment detection in src/init.rs to translate. + // The result: in non-vi fish, keymap is always reported as "insert" + let mode = match (shell.as_str(), keymap) { + ("fish", "default") | ("zsh", "vicmd") => ShellEditMode::Normal, + _ => ASSUMED_MODE, + }; + + if exit_success { + module.set_style(config.style_success); + } else { + module.set_style(config.style_failure); + }; + + /* If an error symbol is set in the config, use symbols to indicate + success/failure, in addition to color */ + if config.use_symbol_for_status && !exit_success { + module.create_segment("error_symbol", &config.error_symbol) + } else { + match mode { + ShellEditMode::Normal => module.create_segment("vicmd_symbol", &config.vicmd_symbol), + ShellEditMode::Insert => module.create_segment("symbol", &config.symbol), + } + }; + + Some(module) +} diff --git a/src/modules/cmd_duration.rs b/src/modules/cmd_duration.rs new file mode 100644 index 000000000..8d4816392 --- /dev/null +++ b/src/modules/cmd_duration.rs @@ -0,0 +1,101 @@ +use ansi_term::Color; + +use super::{Context, Module}; + +/// Outputs the time it took the last command to execute +/// +/// Will only print if last command took more than a certain amount of time to +/// execute. Default is two seconds, but can be set by config option `min_time`. +pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { + let mut module = context.new_module("cmd_duration"); + + let arguments = &context.arguments; + let elapsed = arguments + .value_of("cmd_duration") + .unwrap_or("invalid_time") + .parse::<u64>() + .ok()?; + + let prefix = module + .config_value_str("prefix") + .unwrap_or("took ") + .to_owned(); + + let signed_config_min = module.config_value_i64("min_time").unwrap_or(2); + + /* TODO: Once error handling is implemented, warn the user if their config + min time is nonsensical */ + if signed_config_min < 0 { + log::debug!( + "[WARN]: min_time in [cmd_duration] ({}) was less than zero", + signed_config_min + ); + return None; + } + + let config_min = signed_config_min as u64; + + let module_color = match elapsed { + time if time < config_min => return None, + _ => module + .config_value_style("style") + .unwrap_or_else(|| Color::Yellow.bold()), + }; + + module.set_style(module_color); + module.new_segment( + "cmd_duration", + &format!("{}{}", prefix, render_time(elapsed)), + ); + module.get_prefix().set_value(""); + + Some(module) +} + +// Render the time into a nice human-readable string +fn render_time(raw_seconds: u64) -> String { + // Calculate a simple breakdown into days/hours/minutes/seconds + let (seconds, raw_minutes) = (raw_seconds % 60, raw_seconds / 60); + let (minutes, raw_hours) = (raw_minutes % 60, raw_minutes / 60); + let (hours, days) = (raw_hours % 24, raw_hours / 24); + + let components = [days, hours, minutes, seconds]; + let suffixes = ["d", "h", "m", "s"]; + + let rendered_components: Vec<String> = components + .iter() + .zip(&suffixes) + .map(render_time_component) + .collect(); + rendered_components.join("") +} + +/// Render a single component of the time string, giving an empty string if component is zero +fn render_time_component((component, suffix): (&u64, &&str)) -> String { + match component { + 0 => String::new(), + n => format!("{}{}", n, suffix), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_10s() { + assert_eq!(render_time(10 as u64), "10s") + } + #[test] + fn test_90s() { + assert_eq!(render_time(90 as u64), "1m30s") + } + #[test] + fn test_10110s() { + assert_eq!(render_time(10110 as u64), "2h48m30s") + } + #[test] + fn test_1d() { + assert_eq!(render_time(86400 as u64), "1d") + } +} diff --git a/src/modules/directory.rs b/src/modules/directory.rs new file mode 100644 index 000000000..fd9e74fd1 --- /dev/null +++ b/src/modules/directory.rs @@ -0,0 +1,312 @@ +use ansi_term::Color; +use path_slash::PathExt; +use std::path::Path; + +use super::{Context, Module}; + +/// Creates a module with the current directory +/// +/// Will perform path contraction and truncation. +/// **Contraction** +/// - Paths beginning with the home directory or with a git repo right +/// inside the home directory will be contracted to `~` +/// - Paths containing a git repo will contract to begin at the repo root +/// +/// **Truncation** +/// Paths will be limited in length to `3` path components by default. +pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { + const HOME_SYMBOL: &str = "~"; + const DIR_TRUNCATION_LENGTH: i64 = 3; + const FISH_STYLE_PWD_DIR_LENGTH: i64 = 0; + + let mut module = context.new_module("directory"); + let module_color = module + .config_value_style("style") + .unwrap_or_else(|| Color::Cyan.bold()); + module.set_style(module_color); + + let truncation_length = module + .config_value_i64("truncation_length") + .unwrap_or(DIR_TRUNCATION_LENGTH); + let truncate_to_repo = module.config_value_bool("truncate_to_repo").unwrap_or(true); + let fish_style_pwd_dir_length = module + .config_value_i64("fish_style_pwd_dir_length") + .unwrap_or(FISH_STYLE_PWD_DIR_LENGTH); + + // Using environment PWD is the standard approach for determining logical path + let use_logical_path = module.config_value_bool("use_logical_path").unwrap_or(true); + // If this is None for any reason, we fall back to reading the os-provided path + let logical_current_dir = if use_logical_path { + match std::env::var("PWD") { + Ok(x) => Some(x), + Err(_) => { + log::debug!("Asked for logical path, but PWD was invalid."); + None + } + } + } else { + None + }; + let current_dir = logical_current_dir + .as_ref() + .map(|d| Path::new(d)) + .unwrap_or_else(|| context.current_dir.as_ref()); + + let home_dir = dirs::home_dir().unwrap(); + log::debug!("Current directory: {:?}", current_dir); + + let repo = &context.get_repo().ok()?; + + let dir_string = match &repo.root { + Some(repo_root) if truncate_to_repo && (repo_root != &home_dir) => { + let repo_folder_name = repo_root.file_name().unwrap().to_str().unwrap(); + + // Contract the path to the git repo root + contract_path(current_dir, repo_root, repo_folder_name) + } + // Contract the path to the home directory + _ => contract_path(current_dir, &home_dir, HOME_SYMBOL), + }; + + // Truncate the dir string to the maximum number of path components + let truncated_dir_string = truncate(dir_string, truncation_length as usize); + + if fish_style_pwd_dir_length > 0 { + // If user is using fish style path, we need to add the segment first + let contracted_home_dir = contract_path(¤t_dir, &home_dir, HOME_SYMBOL); + let fish_style_dir = to_fish_style( + fish_style_pwd_dir_length as usize, + contracted_home_dir, + &truncated_dir_string, + ); + + module.new_segment("path", &fish_style_dir); + } + + module.new_segment("path", &truncated_dir_string); + + module.get_prefix().set_value("in "); + + Some(module) +} + +/// Contract the root component of a path +/// +/// Replaces the `top_level_path` in a given `full_path` with the provided +/// `top_level_replacement`. +fn contract_path(full_path: &Path, top_level_path: &Path, top_level_replacement: &str) -> String { + if !full_path.starts_with(top_level_path) { + return replace_c_dir(full_path.to_slash().unwrap()); + } + + if full_path == top_level_path { + return replace_c_dir(top_level_replacement.to_string()); + } + + format!( + "{replacement}{separator}{path}", + replacement = top_level_replacement, + separator = "/", + path = replace_c_dir( + full_path + .strip_prefix(top_level_path) + .unwrap() + .to_slash() + .unwrap() + ) + ) +} + +/// Replaces "C://" with "/c/" within a Windows path +/// +/// On non-Windows OS, does nothing +#[cfg(target_os = "windows")] +fn replace_c_dir(path: String) -> String { + path.replace("C:/", "/c") +} + +/// Replaces "C://" with "/c/" within a Windows path +/// +/// On non-Windows OS, does nothing +#[cfg(not(target_os = "windows"))] +const fn replace_c_dir(path: String) -> String { + path +} + +/// Truncate a path to only have a set number of path components +/// +/// Will truncate a path to only show the last `length` components in a path. +/// If a length of `0` is provided, the path will not be truncated. +fn truncate(dir_string: String, length: usize) -> String { + if length == 0 { + return dir_string; + } + + let components = dir_string.split('/').collect::<Vec<&str>>(); + if components.len() <= length { + return dir_string; + } + + let truncated_components = &components[components.len() - length..]; + truncated_components.join("/") +} + +/// Takes part before contracted path and replaces it with fish style path +/// +/// Will take the first letter of each directory before the contracted path and +/// use that in the path instead. See the following example. +/// +/// Absolute Path: `/Users/Bob/Projects/work/a_repo` +/// Contracted Path: `a_repo` +/// With Fish Style: `~/P/w/a_repo` +/// +/// Absolute Path: `/some/Path/not/in_a/repo/but_nested` +/// Contracted Path: `in_a/repo/but_nested` +/// With Fish Style: `/s/P/n/in_a/repo/but_nested` +fn to_fish_style(pwd_dir_length: usize, dir_string: String, truncated_dir_string: &str) -> String { + let replaced_dir_string = dir_string.trim_end_matches(truncated_dir_string).to_owned(); + let components = replaced_dir_string.split('/').collect::<Vec<&str>>(); + + if components.is_empty() { + return replaced_dir_string; + } + + components + .into_iter() + .map(|word| match word { + "" => "", + _ if word.len() <= pwd_dir_length => word, + _ if word.starts_with('.') => &word[..=pwd_dir_length], + _ => &word[..pwd_dir_length], + }) + .collect::<Vec<_>>() + .join("/") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn contract_home_directory() { + let full_path = Path::new("/Users/astronaut/schematics/rocket"); + let home = Path::new("/Users/astronaut"); + + let output = contract_path(full_path, home, "~"); + assert_eq!(output, "~/schematics/rocket"); + } + + #[test] + fn contract_repo_directory() { + let full_path = Path::new("/Users/astronaut/dev/rocket-controls/src"); + let repo_root = Path::new("/Users/astronaut/dev/rocket-controls"); + + let output = contract_path(full_path, repo_root, "rocket-controls"); + assert_eq!(output, "rocket-controls/src"); + } + + #[test] + #[cfg(target_os = "windows")] + fn contract_windows_style_home_directory() { + let full_path = Path::new("C:\\Users\\astronaut\\schematics\\rocket"); + let home = Path::new("C:\\Users\\astronaut"); + + let output = contract_path(full_path, home, "~"); + assert_eq!(output, "~/schematics/rocket"); + } + + #[test] + #[cfg(target_os = "windows")] + fn contract_windows_style_repo_directory() { + let full_path = Path::new("C:\\Users\\astronaut\\dev\\rocket-controls\\src"); + let repo_root = Path::new("C:\\Users\\astronaut\\dev\\rocket-controls"); + + let output = contract_path(full_path, repo_root, "rocket-controls"); + assert_eq!(output, "rocket-controls/src"); + } + + #[test] + #[cfg(target_os = "windows")] + fn contract_windows_style_no_top_level_directory() { + let full_path = Path::new("C:\\Some\\Other\\Path"); + let top_level_path = Path::new("C:\\Users\\astronaut"); + + let output = contract_path(full_path, top_level_path, "~"); + assert_eq!(output, "/c/Some/Other/Path"); + } + + #[test] + #[cfg(target_os = "windows")] + fn contract_windows_style_root_directory() { + let full_path = Path::new("C:\\"); + let top_level_path = Path::new("C:\\Users\\astronaut"); + + let output = contract_path(full_path, top_level_path, "~"); + assert_eq!(output, "/c"); + } + + #[test] + fn truncate_smaller_path_than_provided_length() { + let path = "~/starship"; + let output = truncate(path.to_string(), 3); + assert_eq!(output, "~/starship") + } + + #[test] + fn truncate_same_path_as_provided_length() { + let path = "~/starship/engines"; + let output = truncate(path.to_string(), 3); + assert_eq!(output, "~/starship/engines") + } + + #[test] + fn truncate_slightly_larger_path_than_provided_length() { + let path = "~/starship/engines/booster"; + let output = truncate(path.to_string(), 3); + assert_eq!(output, "starship/engines/booster") + } + + #[test] + fn truncate_larger_path_than_provided_length() { + let path = "~/starship/engines/booster/rocket"; + let output = truncate(path.to_string(), 3); + assert_eq!(output, "engines/booster/rocket") + } + + #[test] + fn fish_style_with_user_home_contracted_path() { + let path = "~/starship/engines/booster/rocket"; + let output = to_fish_style(1, path.to_string(), "engines/booster/rocket"); + assert_eq!(output, "~/s/"); + } + + #[test] + fn fish_style_with_user_home_contracted_path_and_dot_dir() { + let path = "~/.starship/engines/booster/rocket"; + let output = to_fish_style(1, path.to_string(), "engines/booster/rocket"); + assert_eq!(output, "~/.s/"); + } + + #[test] + fn fish_style_with_no_contracted_path() { + // `truncatation_length = 2` + let path = "/absolute/Path/not/in_a/repo/but_nested"; + let output = to_fish_style(1, path.to_string(), "repo/but_nested"); + assert_eq!(output, "/a/P/n/i/"); + } + + #[test] + fn fish_style_with_pwd_dir_len_no_contracted_path() { + // `truncatation_length = 2` + let path = "/absolute/Path/not/in_a/repo/but_nested"; + let output = to_fish_style(2, path.to_string(), "repo/but_nested"); + assert_eq!(output, "/ab/Pa/no/in/"); + } + + #[test] + fn fish_style_with_duplicate_directories() { + let path = "~/starship/tmp/C++/C++/C++"; + let output = to_fish_style(1, path.to_string(), "C++"); + assert_eq!(output, "~/s/t/C/C/"); + } +} diff --git a/src/modules/dotnet.rs b/src/modules/dotnet.rs new file mode 100644 index 000000000..d63256e56 --- /dev/null +++ b/src/modules/dotnet.rs @@ -0,0 +1,314 @@ +use std::ffi::OsStr; +use std::iter::Iterator; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::str; + +use super::{Context, Module}; +use crate::config::RootModuleConfig; +use crate::configs::dotnet::DotnetConfig; + +type JValue = serde_json::Value; + +const GLOBAL_JSON_FILE: &str = "global.json"; +const PROJECT_JSON_FILE: &str = "project.json"; + +/// A module which shows the latest (or pinned) version of the dotnet SDK +/// +/// Will display if any of the following files are present in +/// the current directory: +/// global.json, project.json, *.sln, *.csproj, *.fsproj, *.xproj +pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { + let dotnet_files = get_local_dotnet_files(context).ok()?; + if dotnet_files.is_empty() { + return None; + } + + let mut module = context.new_module("dotnet"); + let config = DotnetConfig::try_load(module.config); + + // Internally, this module uses its own mechanism for version detection. + // Typically it is twice as fast as running `dotnet --version`. + let enable_heuristic = config.heuristic; + let version = if enable_heuristic { + let repo_root = context + .get_repo() + .ok() + .and_then(|r| r.root.as_ref().map(PathBuf::as_path)); + estimate_dotnet_version(&dotnet_files, &context.current_dir, repo_root)? + } else { + get_version_from_cli()? + }; + + module.set_style(config.style); + module.create_segment("symbol", &config.symbol); + module.create_segment("version", &config.version.with_value(&version.0)); + + Some(module) +} + +fn estimate_dotnet_version<'a>( + files: &[DotNetFile<'a>], + current_dir: &Path, + repo_root: Option<&Path>, +) -> Option<Version> { + let get_file_of_type = |t: FileType| files.iter().find(|f| f.file_type == t); + + // It's important to check for a global.json or a solution file first, + // but otherwise we can take any relevant file. We'll take whichever is first. + let relevant_file = get_file_of_type(FileType::GlobalJson) + .or_else(|| get_file_of_type(FileType::SolutionFile)) + .or_else(|| files.iter().next())?; + + match relevant_file.file_type { + FileType::GlobalJson => { + get_pinned_sdk_version_from_file(relevant_file.path).or_else(get_latest_sdk_from_cli) + } + FileType::SolutionFile => { + // With this heuristic, we'll assume that a "global.json" won't + // be found in any directory above the solution file. + get_latest_sdk_from_cli() + } + _ => { + // If we see a dotnet project, we'll check a small number of neighboring + // directories to see if we can find a global.json. Otherwise, assume the + // latest SDK is in use. + try_find_nearby_global_json(current_dir, repo_root).or_else(get_latest_sdk_from_cli) + } + } +} + +/// Looks for a `global.json` which may exist in one of the parent directories of the current path. +/// If there is one present, and it contains valid version pinning information, then return that version. +/// +/// The following places are scanned: +/// - The parent of the current directory +/// (Unless there is a git repository, and the parent is above the root of that repository) +/// - The root of the git repository +/// (If there is one) +fn try_find_nearby_global_json(current_dir: &Path, repo_root: Option<&Path>) -> Option<Version> { + let current_dir_is_repo_root = repo_root.map(|r| r == current_dir).unwrap_or(false); + let parent_dir = if current_dir_is_repo_root { + // Don't scan the parent directory if it's above the root of a git repository + None + } else { + current_dir.parent() + }; + + // Check the parent directory, or otherwise the repository root, for a global.json + let mut check_dirs = parent_dir + .iter() + .chain(repo_root.iter()) + .copied() // Copies the reference, not the Path itself + .collect::<Vec<&Path>>(); + + // The parent directory and repository root may be the same directory, + // so avoid checking it twice. + check_dirs.dedup(); + + check_dirs + .iter() + // repo_root may be the same as the current directory. We don't need to scan it again. + .filter(|&&d| d != current_dir) + .filter_map(|d| check_directory_for_global_json(d)) + // This will lazily evaluate the first directory with a global.json + .next() +} + +fn check_directory_for_global_json(path: &Path) -> Option<Version> { + let global_json_path = path.join(GLOBAL_JSON_FILE); + log::debug!( + "Checking if global.json exists at: {}", + &global_json_path.display() + ); + if global_json_path.exists() { + get_pinned_sdk_version_from_file(&global_json_path) + } else { + None + } +} + +fn get_pinned_sdk_version_from_file(path: &Path) -> Option<Version> { + let json_text = crate::utils::read_file(path).ok()?; + log::debug!( + "Checking if .NET SDK version is pinned in: {}", + path.display() + ); + get_pinned_sdk_version(&json_text) +} + +fn get_pinned_sdk_version(json: &str) -> Option<Version> { + let parsed_json: JValue = serde_json::from_str(json).ok()?; + + match parsed_json { + JValue::Object(root) => { + let sdk = root.get("sdk")?; + match sdk { + JValue::Object(sdk) => { + let version = sdk.get("version")?; + match version { + JValue::String(version_string) => { + let mut buffer = String::with_capacity(version_string.len() + 1); + buffer.push('v'); + buffer.push_str(version_string); + Some(Version(buffer)) + } + _ => None, + } + } + _ => None, + } + } + _ => None, + } +} + +fn get_local_dotnet_files<'a>(context: &'a Context) -> Result<Vec<DotNetFile<'a>>, std::io::Error> { + Ok(context + .get_dir_files()? + .iter() + .filter_map(|p| { + get_dotnet_file_type(p).map(|t| DotNetFile { + path: p.as_ref(), + file_type: t, + }) + }) + .collect()) +} + +fn get_dotnet_file_type(path: &Path) -> Option<FileType> { + let file_name_lower = map_str_to_lower(path.file_name()); + + match file_name_lower.as_ref().map(|f| f.as_ref()) { + Some(GLOBAL_JSON_FILE) => return Some(FileType::GlobalJson), + Some(PROJECT_JSON_FILE) => return Some(FileType::ProjectJson), |