diff --git a/Cargo.lock b/Cargo.lock index 7a6a77ad..4845705c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,15 +369,6 @@ dependencies = [ "simdutf8", ] -[[package]] -name = "fern" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" -dependencies = [ - "log", -] - [[package]] name = "filetime" version = "0.2.27" @@ -1398,7 +1389,6 @@ name = "helix-dap" version = "25.7.1" dependencies = [ "anyhow", - "fern", "futures-executor", "futures-util", "helix-core", @@ -1521,7 +1511,6 @@ dependencies = [ "content_inspector", "crossterm", "dashmap", - "fern", "futures-util", "grep-matcher", "grep-regex", diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml index 1cf00351..f800029d 100644 --- a/helix-dap/Cargo.toml +++ b/helix-dap/Cargo.toml @@ -28,6 +28,3 @@ futures-executor.workspace = true futures-util.workspace = true tokio-stream.workspace = true sonic-rs.workspace = true - -[dev-dependencies] -fern = "0.7" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 4ff6062d..3bd7015b 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -64,8 +64,7 @@ termini = "1" indexmap = { version = "2.14", features = ["serde"] } # Logging -fern = "0.7" -log = "0.4" +log = { version = "0.4", features = ["std"] } # File picker nucleo.workspace = true diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index f9cd720b..26a82044 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -87,20 +87,7 @@ fn setup_integration_logging() { .map(|lvl| lvl.parse().unwrap()) .unwrap_or(log::LevelFilter::Info); - // Separate file config so we can include year, month and day in file logs - let _ = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} {} [{}] {}", - crate::logging::log_timestamp(), - record.target(), - record.level(), - message - )) - }) - .level(level) - .chain(std::io::stdout()) - .apply(); + crate::logging::init_stdout(level); } impl Application { diff --git a/helix-term/src/logging.rs b/helix-term/src/logging.rs index 75fa04e2..765107be 100644 --- a/helix-term/src/logging.rs +++ b/helix-term/src/logging.rs @@ -1,12 +1,71 @@ -//! Logging support for the editor binary. -//! -//! The timestamp is formatted by hand so we don't need chrono's `clock` feature -//! (which drags in the `iana-time-zone` timezone subtree just to read the local -//! offset). We only need to *format* a `SystemTime`, and log files in UTC are -//! unambiguous across machines, so that is what we emit. +//! Logging support for `hx`. +use std::io::Write; +use std::path::Path; +use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; +/// Minimal `log::Log` implementation: a level filter plus a single line-buffered sink. +struct Logger { + level: log::LevelFilter, + sink: Mutex>, +} + +impl log::Log for Logger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + if let Ok(mut sink) = self.sink.lock() { + let _ = writeln!( + sink, + "{} {} [{}] {}", + log_timestamp(), + record.target(), + record.level(), + record.args() + ); + } + } + + fn flush(&self) { + if let Ok(mut sink) = self.sink.lock() { + let _ = sink.flush(); + } + } +} + +fn install( + level: log::LevelFilter, + sink: Box, +) -> Result<(), log::SetLoggerError> { + log::set_boxed_logger(Box::new(Logger { + level, + sink: Mutex::new(sink), + }))?; + log::set_max_level(level); + Ok(()) +} + +/// Install the global logger writing to `path` (created if absent, appended to). +pub fn init_file(level: log::LevelFilter, path: &Path) -> std::io::Result<()> { + let file = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(path)?; + install(level, Box::new(file)).map_err(std::io::Error::other) +} + +/// Install the global logger writing to stdout (used by integration tests). +#[cfg(feature = "integration")] +pub fn init_stdout(level: log::LevelFilter) { + let _ = install(level, Box::new(std::io::stdout())); +} + /// RFC3339-style UTC timestamp for a log line: `YYYY-MM-DDTHH:MM:SS.mmm`. pub fn log_timestamp() -> String { let now = SystemTime::now() @@ -48,7 +107,10 @@ mod tests { // epoch assert_eq!(format_timestamp(0, 0), "1970-01-01T00:00:00.000"); // next day, and time-of-day + millis - assert_eq!(format_timestamp(86_400 + 3661, 7), "1970-01-02T01:01:01.007"); + assert_eq!( + format_timestamp(86_400 + 3661, 7), + "1970-01-02T01:01:01.007" + ); // a leap day: 2024-02-29T12:30:45.123 UTC == 1709209845 s assert_eq!( format_timestamp(1_709_209_845, 123), diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index c300f03b..87768da1 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -5,29 +5,14 @@ use helix_term::args::Args; use helix_term::config::{Config, ConfigLoadError}; fn setup_logging(verbosity: u64) -> Result<()> { - let mut base_config = fern::Dispatch::new(); - - base_config = match verbosity { - 0 => base_config.level(log::LevelFilter::Warn), - 1 => base_config.level(log::LevelFilter::Info), - 2 => base_config.level(log::LevelFilter::Debug), - _3_or_more => base_config.level(log::LevelFilter::Trace), + let level = match verbosity { + 0 => log::LevelFilter::Warn, + 1 => log::LevelFilter::Info, + 2 => log::LevelFilter::Debug, + _3_or_more => log::LevelFilter::Trace, }; - // Separate file config so we can include year, month and day in file logs - let file_config = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} {} [{}] {}", - helix_term::logging::log_timestamp(), - record.target(), - record.level(), - message - )) - }) - .chain(fern::log_file(helix_loader::log_file())?); - - base_config.chain(file_config).apply()?; + helix_term::logging::init_file(level, &helix_loader::log_file())?; Ok(()) }