backtrace_ls/
cli.rs

1//! CLI entry point for backtrace-ls.
2
3use std::path::PathBuf;
4
5use clap::Parser;
6use env_logger::Env;
7
8use crate::{config::Config, error::LSError, server, text};
9
10/// LSP server for showing test failures as diagnostics.
11#[derive(Parser, Debug)]
12#[command(name = "backtrace-ls")]
13#[command(about = "LSP server for showing test failures as diagnostics")]
14pub struct Args {
15    /// Workspace root path for automatic runner detection.
16    /// If not specified, the workspace folder from LSP initialization is used.
17    #[arg(long = "path")]
18    pub workspace_root: Option<PathBuf>,
19
20    /// Run in text mode instead of LSP mode.
21    /// Prints test failures to stdout/stderr and watches for file changes.
22    #[arg(long = "text")]
23    pub text_mode: bool,
24
25    /// Enable verbose logging (shows debug output).
26    /// Only effective with --text mode.
27    #[arg(long = "verbose", short = 'v')]
28    pub verbose: bool,
29
30    /// Test runner to use (e.g., "cargo-test", "jest", "gtest").
31    /// If not specified, auto-detected from project markers.
32    #[arg(long = "runner")]
33    pub runner: Option<String>,
34}
35
36/// Run the application.
37///
38/// Parses CLI arguments and starts either the LSP server or text mode.
39pub async fn run() -> Result<(), LSError> {
40    let args = Args::parse();
41
42    // Initialize logger with appropriate level (only for backtrace_ls crate)
43    let default_level = if args.verbose {
44        "backtrace_ls=debug"
45    } else {
46        "backtrace_ls=info"
47    };
48    let l = Env::default().default_filter_or(default_level);
49    env_logger::Builder::from_env(l)
50        .format(format_log_record)
51        .init();
52    log::debug!("Starting logger with level");
53
54    let config = Config {
55        workspace_root: args.workspace_root,
56        runner: args.runner,
57        ..Config::default()
58    };
59
60    if args.text_mode {
61        text::run(config).await
62    } else {
63        server::run(config).await
64    }
65}
66use std::{env::current_dir, io, io::Write};
67
68use env_logger::fmt::Formatter;
69
70/// Initialize the logger with an easy to read format for stdout terminal
71/// output. Use it to debug tests with print debugging.
72#[cfg(test)]
73pub fn init_env_log() {
74    env_logger::builder()
75        .format(format_log_record)
76        .is_test(true)
77        .try_init()
78        .ok();
79}
80
81pub fn init_log() {
82    env_logger::builder()
83        .format(format_log_record)
84        .filter_level(log::LevelFilter::Debug)
85        .try_init()
86        .ok();
87}
88
89fn format_log_record(buf: &mut Formatter, record: &log::Record) -> io::Result<()> {
90    let relative_file = get_relative_file_path(record);
91    let (color_start, color_end) = get_level_colors(record.level());
92
93    writeln!(
94        buf,
95        "{}{}:{} {}{}",
96        color_start,
97        relative_file,
98        record.line().unwrap_or(0),
99        record.args(),
100        color_end
101    )
102}
103
104fn get_relative_file_path<'a>(record: &'a log::Record) -> &'a str {
105    let file = record.file().unwrap_or("unknown");
106    current_dir()
107        .ok()
108        .and_then(|cwd| file.strip_prefix(&*cwd.to_string_lossy()))
109        .unwrap_or(file)
110        .trim_start_matches('/')
111}
112
113const fn get_level_colors(level: log::Level) -> (&'static str, &'static str) {
114    match level {
115        log::Level::Error => ("\x1b[91m", "\x1b[0m"), // Red
116        log::Level::Warn => ("\x1b[93m", "\x1b[0m"),  // Yellow
117        log::Level::Info => ("\x1b[34m", "\x1b[0m"),  // Dark blue
118        log::Level::Debug => ("\x1b[96m", "\x1b[0m"), // Cyan
119        log::Level::Trace => ("\x1b[90m", "\x1b[0m"), // Gray
120    }
121}