on master help sign in
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>

commit194b09fbc1edb2d0ccdadc53ec0893f61dbded8e authorGokul Soumya <gokulps15@gmail.com> dateTue 8 Mar 2022, 10:55 (5 years ago) parentf31e85a
10 files changed+279−62
Cargo.lock+1 helix-core/src/lib.rs+12 helix-core/src/syntax.rs+1−1 helix-term/Cargo.toml+2 helix-term/src/application.rs−1 helix-term/src/args.rs+14−12 helix-term/src/health.rs+221 helix-term/src/lib.rs+1 helix-term/src/main.rs+21−5 xtask/src/main.rs+6−43
Cargo.lock+1
@@ -457,6 +457,7 @@ dependencies = [
457457 "tokio",
458458 "tokio-stream",
459459 "toml",
460+ "which",
460461]
461462
462463[[package]]
helix-core/src/lib.rs+12
@@ -124,6 +124,18 @@ pub fn cache_dir() -> std::path::PathBuf {
124124 path
125125}
126126
127+pub fn config_file() -> std::path::PathBuf {
128+ config_dir().join("config.toml")
129+}
130+
131+pub fn lang_config_file() -> std::path::PathBuf {
132+ config_dir().join("languages.toml")
133+}
134+
135+pub fn log_file() -> std::path::PathBuf {
136+ cache_dir().join("helix.log")
137+}
138+
127139// right overrides left
128140pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
129141 use toml::Value;
helix-core/src/syntax.rs+1−1
@@ -335,7 +335,7 @@ impl TextObjectQuery {
335335 }
336336}
337337
338−fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
338+pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
339339 let path = crate::RUNTIME_DIR
340340 .join("queries")
341341 .join(language)
helix-term/Cargo.toml+2
@@ -30,6 +30,8 @@ helix-dap = { version = "0.6", path = "../helix-dap" }
3030anyhow = "1"
3131once_cell = "1.10"
3232
33+which = "4.2"
34+
3335tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
3436num_cpus = "1"
3537tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
helix-term/src/application.rs−1
@@ -66,7 +66,6 @@ impl Application {
6666 let theme_loader =
6767 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
6868
69− // load default and user config, and merge both
7069 let true_color = config.editor.true_color || crate::true_color();
7170 let theme = config
7271 .theme
helix-term/src/args.rs+14−12
@@ -1,4 +1,4 @@
1−use anyhow::{Error, Result};
1+use anyhow::Result;
22use helix_core::Position;
33use std::path::{Path, PathBuf};
44
@@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
66pub struct Args {
77 pub display_help: bool,
88 pub display_version: bool,
9+ pub health: bool,
10+ pub health_arg: Option<String>,
911 pub load_tutor: bool,
1012 pub verbosity: u64,
1113 pub files: Vec<(PathBuf, Position)>,
@@ -14,22 +16,22 @@ pub struct Args {
1416impl Args {
1517 pub fn parse_args() -> Result<Args> {
1618 let mut args = Args::default();
17− let argv: Vec<String> = std::env::args().collect();
18− let mut iter = argv.iter();
19+ let mut argv = std::env::args().peekable();
1920
20− iter.next(); // skip the program, we don't care about that
21+ argv.next(); // skip the program, we don't care about that
2122
22− for arg in &mut iter {
23+ while let Some(arg) = argv.next() {
2324 match arg.as_str() {
2425 "--" => break, // stop parsing at this point treat the remaining as files
2526 "--version" => args.display_version = true,
2627 "--help" => args.display_help = true,
2728 "--tutor" => args.load_tutor = true,
29+ "--health" => {
30+ args.health = true;
31+ args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
32+ }
2833 arg if arg.starts_with("--") => {
29− return Err(Error::msg(format!(
30− "unexpected double dash argument: {}",
31− arg
32− )))
34+ anyhow::bail!("unexpected double dash argument: {}", arg)
3335 }
3436 arg if arg.starts_with('-') => {
3537 let arg = arg.get(1..).unwrap().chars();
@@ -38,7 +40,7 @@ impl Args {
3840 'v' => args.verbosity += 1,
3941 'V' => args.display_version = true,
4042 'h' => args.display_help = true,
41− _ => return Err(Error::msg(format!("unexpected short arg {}", chr))),
43+ _ => anyhow::bail!("unexpected short arg {}", chr),
4244 }
4345 }
4446 }
@@ -47,8 +49,8 @@ impl Args {
4749 }
4850
4951 // push the remaining args, if any to the files
50− for arg in iter {
51− args.files.push(parse_file(arg));
52+ for arg in argv {
53+ args.files.push(parse_file(&arg));
5254 }
5355
5456 Ok(args)
helix-term/src/health.rsnew file+221
@@ -0,0 +1,221 @@
1+use crossterm::style::{Color, Print, Stylize};
2+use helix_core::{
3+ config::{default_syntax_loader, user_syntax_loader},
4+ syntax::load_runtime_file,
5+};
6+
7+#[derive(Copy, Clone)]
8+pub enum TsFeature {
9+ Highlight,
10+ TextObject,
11+ AutoIndent,
12+}
13+
14+impl TsFeature {
15+ pub fn all() -> &'static [Self] {
16+ &[Self::Highlight, Self::TextObject, Self::AutoIndent]
17+ }
18+
19+ pub fn runtime_filename(&self) -> &'static str {
20+ match *self {
21+ Self::Highlight => "highlights.scm",
22+ Self::TextObject => "textobjects.scm",
23+ Self::AutoIndent => "indents.toml",
24+ }
25+ }
26+
27+ pub fn long_title(&self) -> &'static str {
28+ match *self {
29+ Self::Highlight => "Syntax Highlighting",
30+ Self::TextObject => "Treesitter Textobjects",
31+ Self::AutoIndent => "Auto Indent",
32+ }
33+ }
34+
35+ pub fn short_title(&self) -> &'static str {
36+ match *self {
37+ Self::Highlight => "Highlight",
38+ Self::TextObject => "Textobject",
39+ Self::AutoIndent => "Indent",
40+ }
41+ }
42+}
43+
44+/// Display general diagnostics.
45+pub fn general() {
46+ let config_file = helix_core::config_file();
47+ let lang_file = helix_core::lang_config_file();
48+ let log_file = helix_core::log_file();
49+ let rt_dir = helix_core::runtime_dir();
50+
51+ if config_file.exists() {
52+ println!("Config file: {}", config_file.display());
53+ } else {
54+ println!("Config file: default")
55+ }
56+ if lang_file.exists() {
57+ println!("Language file: {}", lang_file.display());
58+ } else {
59+ println!("Language file: default")
60+ }
61+ println!("Log file: {}", log_file.display());
62+ println!("Runtime directory: {}", rt_dir.display());
63+
64+ if let Ok(path) = std::fs::read_link(&rt_dir) {
65+ let msg = format!("Runtime directory is symlinked to {}", path.display());
66+ println!("{}", msg.yellow());
67+ }
68+ if !rt_dir.exists() {
69+ println!("{}", "Runtime directory does not exist.".red());
70+ }
71+ if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) {
72+ println!("{}", "Runtime directory is empty.".red());
73+ }
74+}
75+
76+pub fn languages_all() {
77+ let mut syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| {
78+ eprintln!("{}: {}", "Error parsing user language config".red(), err);
79+ eprintln!("{}", "Using default language config".yellow());
80+ default_syntax_loader()
81+ });
82+
83+ let mut headings = vec!["Language", "LSP", "DAP"];
84+
85+ for feat in TsFeature::all() {
86+ headings.push(feat.short_title())
87+ }
88+
89+ let terminal_cols = crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80);
90+ let column_width = terminal_cols as usize / headings.len();
91+
92+ let column = |item: &str, color: Color| {
93+ let data = format!(
94+ "{:column_width$}",
95+ item.get(..column_width - 2)
96+ .map(|s| format!("{s}…"))
97+ .unwrap_or_else(|| item.to_string())
98+ )
99+ .stylize()
100+ .with(color);
101+
102+ // We can't directly use println!() because of
103+ // https://github.com/crossterm-rs/crossterm/issues/589
104+ let _ = crossterm::execute!(std::io::stdout(), Print(data));
105+ };
106+
107+ for heading in headings {
108+ column(heading, Color::White);
109+ }
110+ println!();
111+
112+ syn_loader_conf
113+ .language
114+ .sort_unstable_by_key(|l| l.language_id.clone());
115+
116+ let check_binary = |cmd: Option<String>| match cmd {
117+ Some(cmd) => match which::which(&cmd) {
118+ Ok(_) => column(&cmd, Color::Green),
119+ Err(_) => column(&cmd, Color::Red),
120+ },
121+ None => column("None", Color::Yellow),
122+ };
123+
124+ for lang in &syn_loader_conf.language {
125+ column(&lang.language_id, Color::Reset);
126+
127+ let lsp = lang
128+ .language_server
129+ .as_ref()
130+ .map(|lsp| lsp.command.to_string());
131+ check_binary(lsp);
132+
133+ let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
134+ check_binary(dap);
135+
136+ for ts_feat in TsFeature::all() {
137+ match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() {
138+ true => column("Found", Color::Green),
139+ false => column("Not Found", Color::Red),
140+ }
141+ }
142+
143+ println!();
144+ }
145+}
146+
147+/// Display diagnostics pertaining to a particular language (LSP,
148+/// highlight queries, etc).
149+pub fn language(lang_str: String) {
150+ let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| {
151+ eprintln!("{}: {}", "Error parsing user language config".red(), err);
152+ eprintln!("{}", "Using default language config".yellow());
153+ default_syntax_loader()
154+ });
155+
156+ let lang = match syn_loader_conf
157+ .language
158+ .iter()
159+ .find(|l| l.language_id == lang_str)
160+ {
161+ Some(l) => l,
162+ None => {
163+ let msg = format!("Language '{lang_str}' not found");
164+ println!("{}", msg.red());
165+ let suggestions: Vec<&str> = syn_loader_conf
166+ .language
167+ .iter()
168+ .filter(|l| l.language_id.starts_with(lang_str.chars().next().unwrap()))
169+ .map(|l| l.language_id.as_str())
170+ .collect();
171+ if !suggestions.is_empty() {
172+ let suggestions = suggestions.join(", ");
173+ println!("Did you mean one of these: {} ?", suggestions.yellow());
174+ }
175+ return;
176+ }
177+ };
178+
179+ probe_protocol(
180+ "language server",
181+ lang.language_server
182+ .as_ref()
183+ .map(|lsp| lsp.command.to_string()),
184+ );
185+
186+ probe_protocol(
187+ "debug adapter",
188+ lang.debugger.as_ref().map(|dap| dap.command.to_string()),
189+ );
190+
191+ for ts_feat in TsFeature::all() {
192+ probe_treesitter_feature(&lang_str, *ts_feat)
193+ }
194+}
195+
196+/// Display diagnostics about LSP and DAP.
197+fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) {
198+ let cmd_name = match server_cmd {
199+ Some(ref cmd) => cmd.as_str().green(),
200+ None => "None".yellow(),
201+ };
202+ println!("Configured {}: {}", protocol_name, cmd_name);
203+
204+ if let Some(cmd) = server_cmd {
205+ let path = match which::which(&cmd) {
206+ Ok(path) => path.display().to_string().green(),
207+ Err(_) => "Not found in $PATH".to_string().red(),
208+ };
209+ println!("Binary for {}: {}", protocol_name, path);
210+ }
211+}
212+
213+/// Display diagnostics about a feature that requires tree-sitter
214+/// query files (highlights, textobjects, etc).
215+fn probe_treesitter_feature(lang: &str, feature: TsFeature) {
216+ let found = match load_runtime_file(lang, feature.runtime_filename()).is_ok() {
217+ true => "Found".green(),
218+ false => "Not found".red(),
219+ };
220+ println!("{} queries: {}", feature.short_title(), found);
221+}
helix-term/src/lib.rs+1
@@ -6,6 +6,7 @@ pub mod args;
66pub mod commands;
77pub mod compositor;
88pub mod config;
9+pub mod health;
910pub mod job;
1011pub mod keymap;
1112pub mod ui;
helix-term/src/main.rs+21−5
@@ -40,12 +40,12 @@ fn main() -> Result<()> {
4040
4141#[tokio::main]
4242async fn main_impl() -> Result<i32> {
43− let cache_dir = helix_core::cache_dir();
44− if !cache_dir.exists() {
45− std::fs::create_dir_all(&cache_dir).ok();
43+ let logpath = helix_core::log_file();
44+ let parent = logpath.parent().unwrap();
45+ if !parent.exists() {
46+ std::fs::create_dir_all(parent).ok();
4647 }
4748
48− let logpath = cache_dir.join("helix.log");
4949 let help = format!(
5050 "\
5151{} {}
@@ -61,6 +61,8 @@ ARGS:
6161FLAGS:
6262 -h, --help Prints help information
6363 --tutor Loads the tutorial
64+ --health [LANG] Checks for potential errors in editor setup
65+ If given, checks for config errors in language LANG
6466 -v Increases logging verbosity each use for up to 3 times
6567 (default file: {})
6668 -V, --version Prints version information
@@ -85,12 +87,26 @@ FLAGS:
8587 std::process::exit(0);
8688 }
8789
90+ if args.health {
91+ if let Some(lang) = args.health_arg {
92+ match lang.as_str() {
93+ "all" => helix_term::health::languages_all(),
94+ _ => helix_term::health::language(lang),
95+ }
96+ } else {
97+ helix_term::health::general();
98+ println!();
99+ helix_term::health::languages_all();
100+ }
101+ std::process::exit(0);
102+ }
103+
88104 let conf_dir = helix_core::config_dir();
89105 if !conf_dir.exists() {
90106 std::fs::create_dir_all(&conf_dir).ok();
91107 }
92108
93− let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
109+ let config = match std::fs::read_to_string(helix_core::config_file()) {
94110 Ok(config) => toml::from_str(&config)
95111 .map(merge_keys)
96112 .unwrap_or_else(|err| {
xtask/src/main.rs+6−43
@@ -3,48 +3,11 @@ use std::{env, error::Error};
33type DynError = Box<dyn Error>;
44
55pub mod helpers {
6− use std::{
7− fmt::Display,
8− path::{Path, PathBuf},
9− };
6+ use std::path::{Path, PathBuf};
107
118 use crate::path;
129 use helix_core::syntax::Configuration as LangConfig;
13−
14− #[derive(Copy, Clone)]
15− pub enum TsFeature {
16− Highlight,
17− TextObjects,
18− AutoIndent,
19− }
20−
21− impl TsFeature {
22− pub fn all() -> &'static [Self] {
23− &[Self::Highlight, Self::TextObjects, Self::AutoIndent]
24− }
25−
26− pub fn runtime_filename(&self) -> &'static str {
27− match *self {
28− Self::Highlight => "highlights.scm",
29− Self::TextObjects => "textobjects.scm",
30− Self::AutoIndent => "indents.toml",
31− }
32− }
33− }
34−
35− impl Display for TsFeature {
36− fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37− write!(
38− f,
39− "{}",
40− match *self {
41− Self::Highlight => "Syntax Highlighting",
42− Self::TextObjects => "Treesitter Textobjects",
43− Self::AutoIndent => "Auto Indent",
44− }
45− )
46− }
47− }
10+ use helix_term::health::TsFeature;
4811
4912 /// Get the list of languages that support a particular tree-sitter
5013 /// based feature.
@@ -105,9 +68,9 @@ pub mod md_gen {
10568
10669 use crate::helpers;
10770 use crate::path;
108− use std::fs;
109−
11071 use helix_term::commands::TYPABLE_COMMAND_LIST;
72+ use helix_term::health::TsFeature;
73+ use std::fs;
11174
11275 pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
11376 pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
@@ -151,13 +114,13 @@ pub mod md_gen {
151114
152115 pub fn lang_features() -> Result<String, DynError> {
153116 let mut md = String::new();
154− let ts_features = helpers::TsFeature::all();
117+ let ts_features = TsFeature::all();
155118
156119 let mut cols = vec!["Language".to_owned()];
157120 cols.append(
158121 &mut ts_features
159122 .iter()
160− .map(|t| t.to_string())
123+ .map(|t| t.long_title().to_string())
161124 .collect::<Vec<_>>(),
162125 );
163126 cols.push("Default LSP".to_owned());