use crate::clangd::index::ProgressEvent;
use regex::Regex;
use std::path::PathBuf;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::mpsc;
use tracing::{debug, trace, warn};
pub trait LogParser: Send + Sync {
fn parse_line(&self, line: &str) -> Option<ProgressEvent>;
}
#[derive(Clone)]
pub struct ClangdLogParser {
indexing_start_regex: Regex,
indexing_complete_regex: Regex,
ast_indexed_regex: Regex,
ast_failed_compiler_invocation_regex: Regex,
ast_failed_execute_regex: Regex,
ast_failed_begin_source_regex: Regex,
stdlib_start_regex: Regex,
stdlib_complete_regex: Regex,
}
impl ClangdLogParser {
pub fn new() -> Result<Self, regex::Error> {
Ok(Self {
indexing_start_regex: Regex::new(
r"V\[\d{2}:\d{2}:\d{2}\.\d{3}\] Indexing (.+?) \(digest:=(.+?)\)",
)?,
indexing_complete_regex: Regex::new(
r"I\[\d{2}:\d{2}:\d{2}\.\d{3}\] Indexed (.+?) \((\d+) symbols?, (\d+) refs?, \d+ files?\)",
)?,
ast_indexed_regex: Regex::new(
r"V\[\d{2}:\d{2}:\d{2}\.\d{3}\] indexed file AST for (.+?) version \d+",
)?,
ast_failed_compiler_invocation_regex: Regex::new(
r"[EW]\[\d{2}:\d{2}:\d{2}\.\d{3}\] Could not build CompilerInvocation for file (.+)",
)?,
ast_failed_execute_regex: Regex::new(
r"[EW]\[\d{2}:\d{2}:\d{2}\.\d{3}\] Execute\(\) failed when building AST for (.+?): .+",
)?,
ast_failed_begin_source_regex: Regex::new(
r"[EW]\[\d{2}:\d{2}:\d{2}\.\d{3}\] BeginSourceFile\(\) failed when building AST for (.+)",
)?,
stdlib_start_regex: Regex::new(
r"I\[\d{2}:\d{2}:\d{2}\.\d{3}\] Indexing (.+?) standard library in the context of (.+)",
)?,
stdlib_complete_regex: Regex::new(
r"I\[\d{2}:\d{2}:\d{2}\.\d{3}\] Indexed (.+?) standard library: (\d+) symbols?, (\d+) filtered",
)?,
})
}
}
impl Default for ClangdLogParser {
fn default() -> Self {
Self::new().expect("Failed to compile regex patterns")
}
}
impl LogParser for ClangdLogParser {
fn parse_line(&self, line: &str) -> Option<ProgressEvent> {
if let Some(captures) = self.indexing_start_regex.captures(line) {
let path = captures.get(1)?.as_str();
let digest = captures.get(2)?.as_str();
return Some(ProgressEvent::FileIndexingStarted {
path: PathBuf::from(path),
digest: digest.to_string(),
});
}
if let Some(captures) = self.indexing_complete_regex.captures(line) {
let path = captures.get(1)?.as_str();
let symbols: u32 = captures.get(2)?.as_str().parse().ok()?;
let refs: u32 = captures.get(3)?.as_str().parse().ok()?;
return Some(ProgressEvent::FileIndexingCompleted {
path: PathBuf::from(path),
symbols,
refs,
});
}
if let Some(captures) = self.ast_indexed_regex.captures(line) {
let path = captures.get(1)?.as_str();
return Some(ProgressEvent::FileAstIndexed {
path: PathBuf::from(path),
});
}
if let Some(captures) = self.ast_failed_compiler_invocation_regex.captures(line) {
let path = captures.get(1)?.as_str();
return Some(ProgressEvent::FileAstFailed {
path: PathBuf::from(path),
});
}
if let Some(captures) = self.ast_failed_execute_regex.captures(line) {
let path = captures.get(1)?.as_str();
return Some(ProgressEvent::FileAstFailed {
path: PathBuf::from(path),
});
}
if let Some(captures) = self.ast_failed_begin_source_regex.captures(line) {
let path = captures.get(1)?.as_str();
return Some(ProgressEvent::FileAstFailed {
path: PathBuf::from(path),
});
}
if let Some(captures) = self.stdlib_start_regex.captures(line) {
let stdlib_version = captures.get(1)?.as_str();
let context_file = captures.get(2)?.as_str();
return Some(ProgressEvent::StandardLibraryStarted {
context_file: PathBuf::from(context_file),
stdlib_version: stdlib_version.to_string(),
});
}
if let Some(captures) = self.stdlib_complete_regex.captures(line) {
let symbols: u32 = captures.get(2)?.as_str().parse().ok()?;
let filtered: u32 = captures.get(3)?.as_str().parse().ok()?;
return Some(ProgressEvent::StandardLibraryCompleted { symbols, filtered });
}
None
}
}
pub struct LogMonitor {
parser: ClangdLogParser,
event_sender: Option<mpsc::Sender<ProgressEvent>>,
}
impl LogMonitor {
pub fn new() -> Self {
Self {
parser: ClangdLogParser::default(),
event_sender: None,
}
}
pub fn with_sender(sender: mpsc::Sender<ProgressEvent>) -> Self {
Self {
parser: ClangdLogParser::default(),
event_sender: Some(sender),
}
}
pub fn with_parser_and_sender(
parser: ClangdLogParser,
sender: mpsc::Sender<ProgressEvent>,
) -> Self {
Self {
parser,
event_sender: Some(sender),
}
}
pub fn process_line(&self, line: &str) {
trace!("LogMonitor: Processing stderr line: {}", line);
if let Some(event) = self.parser.parse_line(line)
&& let Some(ref sender) = self.event_sender
{
if sender.try_send(event).is_err() {
warn!("LogMonitor: Progress event channel full, dropping event");
}
}
}
pub fn create_stderr_processor(&self) -> impl Fn(String) + Send + Sync + 'static {
let parser = self.parser.clone();
let sender = self.event_sender.clone();
move |line: String| {
if let Some(event) = parser.parse_line(&line) {
trace!("LogMonitor: Parsed event from stderr: {:?}", event);
if let Some(ref tx) = sender {
if tx.try_send(event).is_err() {
warn!("LogMonitor: Progress event channel full, dropping event");
}
}
}
}
}
pub async fn monitor_stream<R>(&self, reader: R) -> Result<(), std::io::Error>
where
R: tokio::io::AsyncRead + Unpin,
{
let mut lines = BufReader::new(reader).lines();
debug!("LogMonitor: Starting stderr monitoring");
while let Some(line) = lines.next_line().await? {
self.process_line(&line);
}
debug!("LogMonitor: Stderr monitoring ended");
Ok(())
}
}
impl Default for LogMonitor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::sync::mpsc;
#[test]
fn test_clangd_log_parser_creation() {
let parser = ClangdLogParser::new();
assert!(parser.is_ok());
}
#[test]
fn test_parse_indexing_start_log() {
let parser = ClangdLogParser::default();
let line = "V[14:23:45.123] Indexing /path/to/file.cpp (digest:=0x1234ABCD)";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::FileIndexingStarted { path, digest } => {
assert_eq!(path, PathBuf::from("/path/to/file.cpp"));
assert_eq!(digest, "0x1234ABCD");
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_indexing_complete_log() {
let parser = ClangdLogParser::default();
let line = "I[14:23:46.456] Indexed /path/to/file.cpp (42 symbols, 10 refs, 3 files)";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::FileIndexingCompleted {
path,
symbols,
refs,
} => {
assert_eq!(path, PathBuf::from("/path/to/file.cpp"));
assert_eq!(symbols, 42);
assert_eq!(refs, 10);
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_ast_indexed_log() {
let parser = ClangdLogParser::default();
let line =
"V[22:06:42.564] indexed file AST for /tmp/.tmpCSsikQ/src/Container.cpp version 1:";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::FileAstIndexed { path } => {
assert_eq!(path, PathBuf::from("/tmp/.tmpCSsikQ/src/Container.cpp"));
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_stdlib_indexing_start() {
let parser = ClangdLogParser::default();
let line =
"I[14:23:47.789] Indexing c++20 standard library in the context of /path/to/file.cpp";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::StandardLibraryStarted {
context_file,
stdlib_version,
} => {
assert_eq!(context_file, PathBuf::from("/path/to/file.cpp"));
assert_eq!(stdlib_version, "c++20");
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_stdlib_indexing_complete() {
let parser = ClangdLogParser::default();
let line = "I[14:23:48.000] Indexed c++20 standard library: 1234 symbols, 567 filtered";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::StandardLibraryCompleted { symbols, filtered } => {
assert_eq!(symbols, 1234);
assert_eq!(filtered, 567);
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_ignore_unrelated_logs() {
let parser = ClangdLogParser::default();
let line = "I[14:23:48.000] Some other log message";
let event = parser.parse_line(line);
assert!(event.is_none());
}
#[test]
fn test_log_monitor_creation() {
let monitor = LogMonitor::new();
assert!(monitor.event_sender.is_none());
}
#[tokio::test]
async fn test_log_monitor_with_channel() {
let (tx, mut rx) = mpsc::channel(10);
let monitor = LogMonitor::with_sender(tx);
let line = "V[14:23:45.123] Indexing /test.cpp (digest:=0xABC)";
monitor.process_line(line);
let event = rx.recv().await.expect("Should receive event");
match event {
ProgressEvent::FileIndexingStarted { path, digest } => {
assert_eq!(path, PathBuf::from("/test.cpp"));
assert_eq!(digest, "0xABC");
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_log_monitor_no_sender() {
let monitor = LogMonitor::new();
let line = "V[14:23:45.123] Indexing /test.cpp (digest:=0xABC)";
monitor.process_line(line);
}
#[tokio::test]
async fn test_monitor_stream() {
let (tx, mut rx) = mpsc::channel(10);
let monitor = LogMonitor::with_sender(tx);
let log_data = "V[14:23:45.123] Indexing /test1.cpp (digest:=0xABC)\n\
I[14:23:46.456] Indexed /test1.cpp (42 symbols, 10 refs, 3 files)\n\
I[14:23:47.000] Some unrelated log\n\
V[14:23:48.123] Indexing /test2.cpp (digest:=0xDEF)\n";
let cursor = std::io::Cursor::new(log_data.as_bytes());
monitor.monitor_stream(cursor).await.unwrap();
let mut events = Vec::new();
while let Ok(event) = rx.try_recv() {
events.push(event);
}
assert_eq!(events.len(), 3);
match &events[0] {
ProgressEvent::FileIndexingStarted { path, .. } => {
assert_eq!(*path, PathBuf::from("/test1.cpp"));
}
_ => panic!("Wrong event type at index 0"),
}
match &events[1] {
ProgressEvent::FileIndexingCompleted {
path,
symbols,
refs,
} => {
assert_eq!(*path, PathBuf::from("/test1.cpp"));
assert_eq!(*symbols, 42);
assert_eq!(*refs, 10);
}
_ => panic!("Wrong event type at index 1"),
}
match &events[2] {
ProgressEvent::FileIndexingStarted { path, .. } => {
assert_eq!(*path, PathBuf::from("/test2.cpp"));
}
_ => panic!("Wrong event type at index 2"),
}
}
#[test]
fn test_parse_ast_failed_compiler_invocation() {
let parser = ClangdLogParser::default();
let line = "E[14:23:45.123] Could not build CompilerInvocation for file /path/to/file.cpp";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::FileAstFailed { path } => {
assert_eq!(path, PathBuf::from("/path/to/file.cpp"));
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_ast_failed_execute() {
let parser = ClangdLogParser::default();
let line =
"E[14:23:45.123] Execute() failed when building AST for /path/to/file.cpp: some error";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::FileAstFailed { path } => {
assert_eq!(path, PathBuf::from("/path/to/file.cpp"));
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_ast_failed_begin_source() {
let parser = ClangdLogParser::default();
let line =
"E[14:23:45.123] BeginSourceFile() failed when building AST for /path/to/file.cpp";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::FileAstFailed { path } => {
assert_eq!(path, PathBuf::from("/path/to/file.cpp"));
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_parse_ast_failed_warning_level() {
let parser = ClangdLogParser::default();
let line = "W[14:23:45.123] Could not build CompilerInvocation for file /path/to/file.cpp";
let event = parser.parse_line(line);
assert!(event.is_some());
match event.unwrap() {
ProgressEvent::FileAstFailed { path } => {
assert_eq!(path, PathBuf::from("/path/to/file.cpp"));
}
_ => panic!("Wrong event type"),
}
}
#[test]
fn test_regex_edge_cases() {
let parser = ClangdLogParser::default();
let line1 = "V[01:02:03.999] Indexing /some/path.cpp (digest:=ABC123)";
assert!(parser.parse_line(line1).is_some());
let line2 = "V[14:23:45.123] Indexing /path.cpp (digest:=0x1234ABCDEF)";
assert!(parser.parse_line(line2).is_some());
let line3 = "V[14:23:45.123] Indexing /path with spaces/file.cpp (digest:=ABC)";
assert!(parser.parse_line(line3).is_some());
let line4 = "V[14:23:45.123] Indexing incomplete line";
assert!(parser.parse_line(line4).is_none());
}
}