use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::widgets::{Cell, Row, Table, TableState};
use arborium::AnsiHighlighter;
use super::{GREEN, effective_mode, pane_block};
use crate::{Mode, State};
pub fn draw_source(state: &mut State, f: &mut Frame, area: Rect) {
let language = state.source_language.clone().unwrap_or_else(|| "c".to_string());
let context =
if let (Some(file), Some(line)) = (&state.current_source_file, state.current_source_line) {
let filename =
std::path::Path::new(file).file_name().and_then(|n| n.to_str()).unwrap_or(file);
Some(format!("{filename}:{line} [{language}]"))
} else {
return;
};
let active = matches!(effective_mode(state), Mode::OnlySource);
let block = pane_block("Source", context, "", active);
if state.source_lines.is_empty() || state.current_source_line.is_none() {
f.render_widget(block, area);
return;
}
let current_line = state.current_source_line.unwrap() as usize;
let total_lines = state.source_lines.len();
let lines_to_show = (area.height as usize).saturating_sub(1);
state.source_scroll.set_max_scroll(total_lines.saturating_sub(lines_to_show));
let start_line = state.source_scroll.scroll;
let end_line = (start_line + lines_to_show).min(total_lines);
let theme = arborium::theme::builtin::ayu_dark();
let mut highlighter = AnsiHighlighter::new(theme);
let lines_to_display: Vec<String> = state
.source_lines
.clone()
.into_iter()
.skip(start_line)
.take(end_line - start_line)
.collect();
let joined_lines = lines_to_display.join("\n");
let ansi_text = highlighter
.highlight(&language, &joined_lines)
.unwrap_or_else(|_| joined_lines.to_string());
let ansi_text = ansi_text.replace("\x1b[9m", "");
let parsed_lines: Vec<Line> = match ansi_to_tui::IntoText::into_text(&ansi_text) {
Ok(text) => text.lines,
Err(_) => lines_to_display.iter().map(|s| Line::raw(s.to_string())).collect(),
};
let rows: Vec<Row> = lines_to_display
.iter()
.enumerate()
.map(|(i, line_content)| {
let line_num = start_line + i + 1;
let is_current = line_num == current_line;
let marker = if is_current {
Cell::from(">").style(Style::default().fg(GREEN))
} else {
Cell::from(" ")
};
let line_num_cell = Cell::from(format!("{:>4}", line_num)).style(if is_current {
Style::default().fg(GREEN)
} else {
Style::default()
});
let mut line =
parsed_lines.get(i).cloned().unwrap_or_else(|| Line::raw(line_content.to_string()));
line.spans.insert(0, " ".into());
let content_cell = Cell::from(line);
Row::new(vec![marker, line_num_cell, content_cell])
})
.collect();
let widths = [Constraint::Length(1), Constraint::Length(4), Constraint::Fill(1)];
let table = Table::new(rows, widths).block(block);
let mut table_state = TableState::default();
if current_line > start_line {
table_state = table_state.with_selected(current_line - start_line - 1);
}
f.render_stateful_widget(table, area, &mut table_state);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Args, PtrSize};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
fn create_test_state() -> State {
let args = Args {
gdb_path: None,
remote: None,
ptr_size: PtrSize::Size64,
cmds: None,
log_path: None,
};
State::new(args)
}
#[test]
fn test_draw_source_no_file() {
let mut state = create_test_state();
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_source(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_source_with_file_no_lines() {
let mut state = create_test_state();
state.current_source_file = Some("test.c".to_string());
state.current_source_line = Some(10);
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_source(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_source_with_file_and_lines() {
let mut state = create_test_state();
state.current_source_file = Some("test.c".to_string());
state.current_source_line = Some(5);
state.source_lines = vec![
"int main() {".to_string(),
" int x = 0;".to_string(),
" int y = 1;".to_string(),
" int z = 2;".to_string(),
" return x + y + z;".to_string(),
"}".to_string(),
];
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_source(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_source_many_lines_centered() {
let mut state = create_test_state();
state.current_source_file = Some("test.c".to_string());
state.current_source_line = Some(50);
state.source_lines = (1..=100).map(|i| format!("line {i}")).collect();
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_source(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_source_first_line() {
let mut state = create_test_state();
state.current_source_file = Some("test.c".to_string());
state.current_source_line = Some(1);
state.source_lines = vec!["first line".to_string(), "second line".to_string()];
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_source(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_source_last_line() {
let mut state = create_test_state();
state.current_source_file = Some("/path/to/long/directory/test.c".to_string());
state.current_source_line = Some(10);
state.source_lines = (1..=10).map(|i| format!("line {i}")).collect();
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_source(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_source_no_line_number() {
let mut state = create_test_state();
state.current_source_file = Some("test.c".to_string());
state.current_source_line = None;
state.source_lines = vec!["line 1".to_string()];
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_source(&mut state, f, area);
})
.unwrap();
}
}