@@ -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+}