diff --git a/Cargo.lock b/Cargo.lock index 8ab914297..ac49da51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,6 +457,7 @@ dependencies = [ "tokio", "tokio-stream", "toml", + "which", ] [[package]] diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 8e5950deb..c3a349c12 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -124,6 +124,18 @@ pub fn cache_dir() -> std::path::PathBuf { path } +pub fn config_file() -> std::path::PathBuf { + config_dir().join("config.toml") +} + +pub fn lang_config_file() -> std::path::PathBuf { + config_dir().join("languages.toml") +} + +pub fn log_file() -> std::path::PathBuf { + cache_dir().join("helix.log") +} + // right overrides left pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value { use toml::Value; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f2939e3d3..c39e05843 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -335,7 +335,7 @@ pub fn capture_nodes_any<'a>( } } -fn load_runtime_file(language: &str, filename: &str) -> Result { +pub fn load_runtime_file(language: &str, filename: &str) -> Result { let path = crate::RUNTIME_DIR .join("queries") .join(language) diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 5b5a8f642..9f7821f69 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -30,6 +30,8 @@ helix-dap = { version = "0.6", path = "../helix-dap" } anyhow = "1" once_cell = "1.10" +which = "4.2" + tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } num_cpus = "1" tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1b8aba6a9..3eee3396c 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -66,7 +66,6 @@ pub fn new(args: Args, mut config: Config) -> Result { 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 true_color = config.editor.true_color || crate::true_color(); let theme = config .theme diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 3e50f66f7..4f386aea8 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -1,4 +1,4 @@ -use anyhow::{Error, Result}; +use anyhow::Result; use helix_core::Position; use std::path::{Path, PathBuf}; @@ -6,6 +6,8 @@ pub struct Args { pub display_help: bool, pub display_version: bool, + pub health: bool, + pub health_arg: Option, pub load_tutor: bool, pub verbosity: u64, pub files: Vec<(PathBuf, Position)>, @@ -14,22 +16,22 @@ pub struct Args { impl Args { pub fn parse_args() -> Result { let mut args = Args::default(); - let argv: Vec = std::env::args().collect(); - let mut iter = argv.iter(); + let mut argv = std::env::args().peekable(); - iter.next(); // skip the program, we don't care about that + argv.next(); // skip the program, we don't care about that - for arg in &mut iter { + while let Some(arg) = argv.next() { match arg.as_str() { "--" => break, // stop parsing at this point treat the remaining as files "--version" => args.display_version = true, "--help" => args.display_help = true, "--tutor" => args.load_tutor = true, + "--health" => { + args.health = true; + args.health_arg = argv.next_if(|opt| !opt.starts_with('-')); + } arg if arg.starts_with("--") => { - return Err(Error::msg(format!( - "unexpected double dash argument: {}", - arg - ))) + anyhow::bail!("unexpected double dash argument: {}", arg) } arg if arg.starts_with('-') => { let arg = arg.get(1..).unwrap().chars(); @@ -38,7 +40,7 @@ pub fn parse_args() -> Result { 'v' => args.verbosity += 1, 'V' => args.display_version = true, 'h' => args.display_help = true, - _ => return Err(Error::msg(format!("unexpected short arg {}", chr))), + _ => anyhow::bail!("unexpected short arg {}", chr), } } } @@ -47,8 +49,8 @@ pub fn parse_args() -> Result { } // push the remaining args, if any to the files - for arg in iter { - args.files.push(parse_file(arg)); + for arg in argv { + args.files.push(parse_file(&arg)); } Ok(args) diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs new file mode 100644 index 000000000..5ef20d934 --- /dev/null +++ b/helix-term/src/health.rs @@ -0,0 +1,221 @@ +use crossterm::style::{Color, Print, Stylize}; +use helix_core::{ + config::{default_syntax_loader, user_syntax_loader}, + syntax::load_runtime_file, +}; + +#[derive(Copy, Clone)] +pub enum TsFeature { + Highlight, + TextObject, + AutoIndent, +} + +impl TsFeature { + pub fn all() -> &'static [Self] { + &[Self::Highlight, Self::TextObject, Self::AutoIndent] + } + + pub fn runtime_filename(&self) -> &'static str { + match *self { + Self::Highlight => "highlights.scm", + Self::TextObject => "textobjects.scm", + Self::AutoIndent => "indents.toml", + } + } + + pub fn long_title(&self) -> &'static str { + match *self { + Self::Highlight => "Syntax Highlighting", + Self::TextObject => "Treesitter Textobjects", + Self::AutoIndent => "Auto Indent", + } + } + + pub fn short_title(&self) -> &'static str { + match *self { + Self::Highlight => "Highlight", + Self::TextObject => "Textobject", + Self::AutoIndent => "Indent", + } + } +} + +/// Display general diagnostics. +pub fn general() { + let config_file = helix_core::config_file(); + let lang_file = helix_core::lang_config_file(); + let log_file = helix_core::log_file(); + let rt_dir = helix_core::runtime_dir(); + + if config_file.exists() { + println!("Config file: {}", config_file.display()); + } else { + println!("Config file: default") + } + if lang_file.exists() { + println!("Language file: {}", lang_file.display()); + } else { + println!("Language file: default") + } + println!("Log file: {}", log_file.display()); + println!("Runtime directory: {}", rt_dir.display()); + + if let Ok(path) = std::fs::read_link(&rt_dir) { + let msg = format!("Runtime directory is symlinked to {}", path.display()); + println!("{}", msg.yellow()); + } + if !rt_dir.exists() { + println!("{}", "Runtime directory does not exist.".red()); + } + if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { + println!("{}", "Runtime directory is empty.".red()); + } +} + +pub fn languages_all() { + let mut syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| { + eprintln!("{}: {}", "Error parsing user language config".red(), err); + eprintln!("{}", "Using default language config".yellow()); + default_syntax_loader() + }); + + let mut headings = vec!["Language", "LSP", "DAP"]; + + for feat in TsFeature::all() { + headings.push(feat.short_title()) + } + + let terminal_cols = crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80); + let column_width = terminal_cols as usize / headings.len(); + + let column = |item: &str, color: Color| { + let data = format!( + "{:column_width$}", + item.get(..column_width - 2) + .map(|s| format!("{s}…")) + .unwrap_or_else(|| item.to_string()) + ) + .stylize() + .with(color); + + // We can't directly use println!() because of + // https://github.com/crossterm-rs/crossterm/issues/589 + let _ = crossterm::execute!(std::io::stdout(), Print(data)); + }; + + for heading in headings { + column(heading, Color::White); + } + println!(); + + syn_loader_conf + .language + .sort_unstable_by_key(|l| l.language_id.clone()); + + let check_binary = |cmd: Option| match cmd { + Some(cmd) => match which::which(&cmd) { + Ok(_) => column(&cmd, Color::Green), + Err(_) => column(&cmd, Color::Red), + }, + None => column("None", Color::Yellow), + }; + + for lang in &syn_loader_conf.language { + column(&lang.language_id, Color::Reset); + + let lsp = lang + .language_server + .as_ref() + .map(|lsp| lsp.command.to_string()); + check_binary(lsp); + + let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string()); + check_binary(dap); + + for ts_feat in TsFeature::all() { + match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() { + true => column("Found", Color::Green), + false => column("Not Found", Color::Red), + } + } + + println!(); + } +} + +/// Display diagnostics pertaining to a particular language (LSP, +/// highlight queries, etc). +pub fn language(lang_str: String) { + let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| { + eprintln!("{}: {}", "Error parsing user language config".red(), err); + eprintln!("{}", "Using default language config".yellow()); + default_syntax_loader() + }); + + let lang = match syn_loader_conf + .language + .iter() + .find(|l| l.language_id == lang_str) + { + Some(l) => l, + None => { + let msg = format!("Language '{lang_str}' not found"); + println!("{}", msg.red()); + let suggestions: Vec<&str> = syn_loader_conf + .language + .iter() + .filter(|l| l.language_id.starts_with(lang_str.chars().next().unwrap())) + .map(|l| l.language_id.as_str()) + .collect(); + if !suggestions.is_empty() { + let suggestions = suggestions.join(", "); + println!("Did you mean one of these: {} ?", suggestions.yellow()); + } + return; + } + }; + + probe_protocol( + "language server", + lang.language_server + .as_ref() + .map(|lsp| lsp.command.to_string()), + ); + + probe_protocol( + "debug adapter", + lang.debugger.as_ref().map(|dap| dap.command.to_string()), + ); + + for ts_feat in TsFeature::all() { + probe_treesitter_feature(&lang_str, *ts_feat) + } +} + +/// Display diagnostics about LSP and DAP. +fn probe_protocol(protocol_name: &str, server_cmd: Option) { + let cmd_name = match server_cmd { + Some(ref cmd) => cmd.as_str().green(), + None => "None".yellow(), + }; + println!("Configured {}: {}", protocol_name, cmd_name); + + if let Some(cmd) = server_cmd { + let path = match which::which(&cmd) { + Ok(path) => path.display().to_string().green(), + Err(_) => "Not found in $PATH".to_string().red(), + }; + println!("Binary for {}: {}", protocol_name, path); + } +} + +/// Display diagnostics about a feature that requires tree-sitter +/// query files (highlights, textobjects, etc). +fn probe_treesitter_feature(lang: &str, feature: TsFeature) { + let found = match load_runtime_file(lang, feature.runtime_filename()).is_ok() { + true => "Found".green(), + false => "Not found".red(), + }; + println!("{} queries: {}", feature.short_title(), found); +} diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index 58cb139c7..fc8e934e1 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -6,6 +6,7 @@ pub mod commands; pub mod compositor; pub mod config; +pub mod health; pub mod job; pub mod keymap; pub mod ui; diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 0f504046f..cde26c2e7 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -40,12 +40,12 @@ fn main() -> Result<()> { #[tokio::main] async fn main_impl() -> Result { - let cache_dir = helix_core::cache_dir(); - if !cache_dir.exists() { - std::fs::create_dir_all(&cache_dir).ok(); + let logpath = helix_core::log_file(); + let parent = logpath.parent().unwrap(); + if !parent.exists() { + std::fs::create_dir_all(parent).ok(); } - let logpath = cache_dir.join("helix.log"); let help = format!( "\ {} {} @@ -61,6 +61,8 @@ async fn main_impl() -> Result { FLAGS: -h, --help Prints help information --tutor Loads the tutorial + --health [LANG] Checks for potential errors in editor setup + If given, checks for config errors in language LANG -v Increases logging verbosity each use for up to 3 times (default file: {}) -V, --version Prints version information @@ -85,12 +87,26 @@ async fn main_impl() -> Result { std::process::exit(0); } + if args.health { + if let Some(lang) = args.health_arg { + match lang.as_str() { + "all" => helix_term::health::languages_all(), + _ => helix_term::health::language(lang), + } + } else { + helix_term::health::general(); + println!(); + helix_term::health::languages_all(); + } + std::process::exit(0); + } + let conf_dir = helix_core::config_dir(); if !conf_dir.exists() { std::fs::create_dir_all(&conf_dir).ok(); } - let config = match std::fs::read_to_string(conf_dir.join("config.toml")) { + let config = match std::fs::read_to_string(helix_core::config_file()) { Ok(config) => toml::from_str(&config) .map(merge_keys) .unwrap_or_else(|err| { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ad0eb16f5..ad120f4f1 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,48 +3,11 @@ type DynError = Box; pub mod helpers { - use std::{ - fmt::Display, - path::{Path, PathBuf}, - }; + use std::path::{Path, PathBuf}; use crate::path; use helix_core::syntax::Configuration as LangConfig; - - #[derive(Copy, Clone)] - pub enum TsFeature { - Highlight, - TextObjects, - AutoIndent, - } - - impl TsFeature { - pub fn all() -> &'static [Self] { - &[Self::Highlight, Self::TextObjects, Self::AutoIndent] - } - - pub fn runtime_filename(&self) -> &'static str { - match *self { - Self::Highlight => "highlights.scm", - Self::TextObjects => "textobjects.scm", - Self::AutoIndent => "indents.toml", - } - } - } - - impl Display for TsFeature { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match *self { - Self::Highlight => "Syntax Highlighting", - Self::TextObjects => "Treesitter Textobjects", - Self::AutoIndent => "Auto Indent", - } - ) - } - } + use helix_term::health::TsFeature; /// Get the list of languages that support a particular tree-sitter /// based feature. @@ -105,9 +68,9 @@ pub mod md_gen { use crate::helpers; use crate::path; - use std::fs; - use helix_term::commands::TYPABLE_COMMAND_LIST; + use helix_term::health::TsFeature; + use std::fs; pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md"; pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md"; @@ -151,13 +114,13 @@ pub fn typable_commands() -> Result { pub fn lang_features() -> Result { let mut md = String::new(); - let ts_features = helpers::TsFeature::all(); + let ts_features = TsFeature::all(); let mut cols = vec!["Language".to_owned()]; cols.append( &mut ts_features .iter() - .map(|t| t.to_string()) + .map(|t| t.long_title().to_string()) .collect::>(), ); cols.push("Default LSP".to_owned());