mirror of https://github.com/helix-editor/helix
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>pull/1775/head
parent
f31e85aca4
commit
194b09fbc1
@ -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);
|
||||
}
|
Loading…
Reference in New Issue