1use std::sync::Arc;
2
3use dada_ir_ast::{DebugEvent, DebugEventPayload, span::AbsoluteOffset};
4use serde::Serialize;
5use url::Url;
6
7use crate::server::State;
8
9#[derive(Serialize)]
11pub struct RootEvent {
12 url: String,
13 start: usize,
14 end: usize,
15 line_start: usize,
16 col_start: usize,
17 line_end: usize,
18 col_end: usize,
19 text: Option<String>,
20 payload: RootEventPayload,
21}
22
23#[derive(Serialize)]
24#[serde(tag = "type")]
25enum RootEventPayload {
26 Diagnostic {
27 message: String,
28 },
29 CheckLog {
30 index: usize,
31 root_event_info: Option<serde_json::Value>,
32 total_events: Option<usize>,
33 },
34}
35
36pub async fn root(state: &State) -> anyhow::Result<String> {
38 let events = root_events(&state.debug_events.lock().unwrap())?;
39 crate::hbs::render("index", &events)
40}
41
42pub async fn root_data(state: &State) -> anyhow::Result<Vec<RootEvent>> {
43 let events = state.debug_events.lock().unwrap();
44 root_events(&events)
45}
46
47fn root_events(events: &[Arc<DebugEvent>]) -> anyhow::Result<Vec<RootEvent>> {
48 let mut output = Vec::with_capacity(events.len());
49 for (event, index) in events.iter().zip(0..) {
50 let payload = match &event.payload {
51 DebugEventPayload::Diagnostic(diagnostic) => RootEventPayload::Diagnostic {
52 message: diagnostic.message.clone(),
53 },
54 DebugEventPayload::CheckLog(log_value) => {
55 let root_event_info = log_value.get("root_event_info").cloned();
57 let total_events = log_value
58 .get("total_events")
59 .and_then(|v| v.as_u64())
60 .map(|v| v as usize);
61
62 RootEventPayload::CheckLog {
63 index,
64 root_event_info,
65 total_events,
66 }
67 }
68 };
69 let (text, line_start, col_start, line_end, col_end) =
70 extract_span(&event.url, event.start, event.end)?;
71 output.push(RootEvent {
72 url: event.url.to_string(),
73 start: event.start.as_usize(),
74 end: event.end.as_usize(),
75 line_start,
76 col_start,
77 line_end,
78 col_end,
79 text,
80 payload,
81 });
82 }
83 Ok(output)
84}
85
86fn extract_span(
87 url: &Url,
88 start: AbsoluteOffset,
89 end: AbsoluteOffset,
90) -> anyhow::Result<(Option<String>, usize, usize, usize, usize)> {
91 let path_without_leading_slash = url.path().trim_start_matches('/');
93 if !path_without_leading_slash.contains('/') {
94 return Ok((None, 0, 0, 0, 0));
95 }
96
97 let contents = match std::fs::read_to_string(url.path()) {
99 Ok(s) => s,
100 Err(e) => anyhow::bail!("failed to read `{}`: {e}", url.path()),
101 };
102 let start = start.as_usize();
103 let end = end.as_usize();
104 let text = &contents[start..end];
105 let text = if text.len() > 65 {
106 let first_40 = &text[..40];
107 let last_20 = &text[text.len() - 20..];
108 format!("{first_40} ... {last_20}")
109 } else {
110 text.to_string()
111 };
112
113 let (line_start, col_start) = line_column(&contents, start);
114 let (line_end, col_end) = line_column(&contents, end);
115
116 Ok((Some(text), line_start, col_start, line_end, col_end))
117}
118
119fn line_column(text: &str, offset: usize) -> (usize, usize) {
120 let mut line = 1;
121 let mut col = 1;
122 for ch in text[..offset].chars() {
123 if ch == '\n' {
124 line += 1;
125 col = 1;
126 } else {
127 col += 1;
128 }
129 }
130 (line, col)
131}