aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
authorGokul Soumya2022-03-08 05:25:46 +0000
committerGitHub2022-03-08 05:25:46 +0000
commit194b09fbc1edb2d0ccdadc53ec0893f61dbded8e (patch)
tree61e341c96647b254c24f59656d9856e606641707 /helix-term
parentf31e85aca43dc7d1c5aa3e20add0f1126b112b0f (diff)
Add --health command for troubleshooting (#1669)
* Move runtime file location definitions to core * Add basic --health command * Add language specific --health * Show summary for all langs with bare --health * Use TsFeature from xtask for --health * cargo fmt Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/Cargo.toml2
-rw-r--r--helix-term/src/application.rs1
-rw-r--r--helix-term/src/args.rs26
-rw-r--r--helix-term/src/health.rs221
-rw-r--r--helix-term/src/lib.rs1
-rw-r--r--helix-term/src/main.rs26
6 files changed, 259 insertions, 18 deletions
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 5b5a8f64..9f7821f6 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 1b8aba6a..3eee3396 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -66,7 +66,6 @@ impl Application {
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 3e50f66f..4f386aea 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 @@ use std::path::{Path, PathBuf};
pub struct Args {
pub display_help: bool,
pub display_version: bool,
+ pub health: bool,
+ pub health_arg: Option<String>,
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<Args> {
let mut args = Args::default();
- let argv: Vec<String> = 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 @@ impl Args {
'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 @@ impl Args {
}
// 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 00000000..5ef20d93
--- /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<String>| 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<String>) {
+ 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 58cb139c..fc8e934e 100644
--- a/helix-term/src/lib.rs
+++ b/helix-term/src/lib.rs
@@ -6,6 +6,7 @@ pub mod args;
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 0f504046..cde26c2e 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<i32> {
- 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 @@ ARGS:
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 @@ FLAGS:
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| {