on master help sign in
Add --health command for troubleshooting (#1669)
194b09fGokul Soumya
Cargo.lock+1
@@ -457,6 +457,7 @@ dependencies = [
457 "tokio",
457 "tokio",
458 "tokio-stream",
458 "tokio-stream",
459 "toml",
459 "toml",
460+ "which",
460]
461]
461
462
462[[package]]
463[[package]]
helix-core/src/lib.rs+12
@@ -124,6 +124,18 @@ pub fn cache_dir() -> std::path::PathBuf {
124 path
124 path
125}
125}
126
126
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+
127// right overrides left
139// right overrides left
128pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
140pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
129 use toml::Value;
141 use toml::Value;
helix-core/src/syntax.rs+1−1
@@ -335,7 +335,7 @@ impl TextObjectQuery {
335 }
335 }
336}
336}
337
337
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> {
339 let path = crate::RUNTIME_DIR
339 let path = crate::RUNTIME_DIR
340 .join("queries")
340 .join("queries")
341 .join(language)
341 .join(language)
helix-term/Cargo.toml+2
@@ -30,6 +30,8 @@ helix-dap = { version = "0.6", path = "../helix-dap" }
30anyhow = "1"
30anyhow = "1"
31once_cell = "1.10"
31once_cell = "1.10"
32
32
33+which = "4.2"
34+
33tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
35tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
34num_cpus = "1"
36num_cpus = "1"
35tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
37tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
helix-term/src/application.rs−1
@@ -66,7 +66,6 @@ impl Application {
66 let theme_loader =
66 let theme_loader =
67 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
67 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
68
68
69− // load default and user config, and merge both
70 let true_color = config.editor.true_color || crate::true_color();
69 let true_color = config.editor.true_color || crate::true_color();
71 let theme = config
70 let theme = config
72 .theme
71 .theme
helix-term/src/args.rs+14−12
@@ -1,4 +1,4 @@
1−use anyhow::{Error, Result};
1+use anyhow::Result;
2use helix_core::Position;
2use helix_core::Position;
3use std::path::{Path, PathBuf};
3use std::path::{Path, PathBuf};
4
4
@@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
6pub struct Args {
6pub struct Args {
7 pub display_help: bool,
7 pub display_help: bool,
8 pub display_version: bool,
8 pub display_version: bool,
9+ pub health: bool,
10+ pub health_arg: Option<String>,
9 pub load_tutor: bool,
11 pub load_tutor: bool,
10 pub verbosity: u64,
12 pub verbosity: u64,
11 pub files: Vec<(PathBuf, Position)>,
13 pub files: Vec<(PathBuf, Position)>,
@@ -14,22 +16,22 @@ pub struct Args {
14impl Args {
16impl Args {
15 pub fn parse_args() -> Result<Args> {
17 pub fn parse_args() -> Result<Args> {
16 let mut args = Args::default();
18 let mut args = Args::default();
17− let argv: Vec<String> = std::env::args().collect();
19+ let mut argv = std::env::args().peekable();
18− let mut iter = argv.iter();
19
20
20− iter.next(); // skip the program, we don't care about that
21+ argv.next(); // skip the program, we don't care about that
21
22
22− for arg in &mut iter {
23+ while let Some(arg) = argv.next() {
23 match arg.as_str() {
24 match arg.as_str() {
24 "--" => break, // stop parsing at this point treat the remaining as files
25 "--" => break, // stop parsing at this point treat the remaining as files
25 "--version" => args.display_version = true,
26 "--version" => args.display_version = true,
26 "--help" => args.display_help = true,
27 "--help" => args.display_help = true,
27 "--tutor" => args.load_tutor = true,
28 "--tutor" => args.load_tutor = true,
29+ "--health" => {
30+ args.health = true;
31+ args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
32+ }
28 arg if arg.starts_with("--") => {
33 arg if arg.starts_with("--") => {
29− return Err(Error::msg(format!(
30− "unexpected double dash argument: {}",
34+ anyhow::bail!("unexpected double dash argument: {}", arg)
31− arg
32− )))
33 }
35 }
34 arg if arg.starts_with('-') => {
36 arg if arg.starts_with('-') => {
35 let arg = arg.get(1..).unwrap().chars();
37 let arg = arg.get(1..).unwrap().chars();
@@ -38,7 +40,7 @@ impl Args {
38 'v' => args.verbosity += 1,
40 'v' => args.verbosity += 1,
39 'V' => args.display_version = true,
41 'V' => args.display_version = true,
40 'h' => args.display_help = true,
42 'h' => args.display_help = true,
41− _ => return Err(Error::msg(format!("unexpected short arg {}", chr))),
43+ _ => anyhow::bail!("unexpected short arg {}", chr),
42 }
44 }
43 }
45 }
44 }
46 }
@@ -47,8 +49,8 @@ impl Args {
47 }
49 }
48
50
49 // push the remaining args, if any to the files
51 // push the remaining args, if any to the files
50− for arg in iter {
52+ for arg in argv {
51− args.files.push(parse_file(arg));
53+ args.files.push(parse_file(&arg));
52 }
54 }
53
55
54 Ok(args)
56 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;
6pub mod commands;
6pub mod commands;
7pub mod compositor;
7pub mod compositor;
8pub mod config;
8pub mod config;
9+pub mod health;
9pub mod job;
10pub mod job;
10pub mod keymap;
11pub mod keymap;
11pub mod ui;
12pub mod ui;
helix-term/src/main.rs+21−5
@@ -40,12 +40,12 @@ fn main() -> Result<()> {
40
40
41#[tokio::main]
41#[tokio::main]
42async fn main_impl() -> Result<i32> {
42async fn main_impl() -> Result<i32> {
43− let cache_dir = helix_core::cache_dir();
43+ let logpath = helix_core::log_file();
44+ let parent = logpath.parent().unwrap();
44− if !cache_dir.exists() {
45+ if !parent.exists() {
45− std::fs::create_dir_all(&cache_dir).ok();
46+ std::fs::create_dir_all(parent).ok();
46 }
47 }
47
48
48− let logpath = cache_dir.join("helix.log");
49 let help = format!(
49 let help = format!(
50 "\
50 "\
51{} {}
51{} {}
@@ -61,6 +61,8 @@ ARGS:
61FLAGS:
61FLAGS:
62 -h, --help Prints help information
62 -h, --help Prints help information
63 --tutor Loads the tutorial
63 --tutor Loads the tutorial
64+ --health [LANG] Checks for potential errors in editor setup
65+ If given, checks for config errors in language LANG
64 -v Increases logging verbosity each use for up to 3 times
66 -v Increases logging verbosity each use for up to 3 times
65 (default file: {})
67 (default file: {})
66 -V, --version Prints version information
68 -V, --version Prints version information
@@ -85,12 +87,26 @@ FLAGS:
85 std::process::exit(0);
87 std::process::exit(0);
86 }
88 }
87
89
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+
88 let conf_dir = helix_core::config_dir();
104 let conf_dir = helix_core::config_dir();
89 if !conf_dir.exists() {
105 if !conf_dir.exists() {
90 std::fs::create_dir_all(&conf_dir).ok();
106 std::fs::create_dir_all(&conf_dir).ok();
91 }
107 }
92
108
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()) {
94 Ok(config) => toml::from_str(&config)
110 Ok(config) => toml::from_str(&config)
95 .map(merge_keys)
111 .map(merge_keys)
96 .unwrap_or_else(|err| {
112 .unwrap_or_else(|err| {
xtask/src/main.rs+6−43
@@ -3,48 +3,11 @@ use std::{env, error::Error};
3type DynError = Box<dyn Error>;
3type DynError = Box<dyn Error>;
4
4
5pub mod helpers {
5pub mod helpers {
6− use std::{
7− fmt::Display,
8− path::{Path, PathBuf},
6+ use std::path::{Path, PathBuf};
9− };
10
7
11 use crate::path;
8 use crate::path;
12 use helix_core::syntax::Configuration as LangConfig;
9 use helix_core::syntax::Configuration as LangConfig;
13−
10+ use helix_term::health::TsFeature;
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− }
48
11
49 /// Get the list of languages that support a particular tree-sitter
12 /// Get the list of languages that support a particular tree-sitter
50 /// based feature.
13 /// based feature.
@@ -105,9 +68,9 @@ pub mod md_gen {
105
68
106 use crate::helpers;
69 use crate::helpers;
107 use crate::path;
70 use crate::path;
108− use std::fs;
109−
110 use helix_term::commands::TYPABLE_COMMAND_LIST;
71 use helix_term::commands::TYPABLE_COMMAND_LIST;
72+ use helix_term::health::TsFeature;
73+ use std::fs;
111
74
112 pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
75 pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
113 pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
76 pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
@@ -151,13 +114,13 @@ pub mod md_gen {
151
114
152 pub fn lang_features() -> Result<String, DynError> {
115 pub fn lang_features() -> Result<String, DynError> {
153 let mut md = String::new();
116 let mut md = String::new();
154− let ts_features = helpers::TsFeature::all();
117+ let ts_features = TsFeature::all();
155
118
156 let mut cols = vec!["Language".to_owned()];
119 let mut cols = vec!["Language".to_owned()];
157 cols.append(
120 cols.append(
158 &mut ts_features
121 &mut ts_features
159 .iter()
122 .iter()
160− .map(|t| t.to_string())
123+ .map(|t| t.long_title().to_string())
161 .collect::<Vec<_>>(),
124 .collect::<Vec<_>>(),
162 );
125 );
163 cols.push("Default LSP".to_owned());
126 cols.push("Default LSP".to_owned());