use std::{collections::BTreeMap, ops::Deref, sync::Arc};
use miden_assembly_syntax::{
debuginfo::{SourceFile, SourceId, SourceSpan},
diagnostics::{Report, SourceCode},
};
use ratatui::{prelude::*, widgets::*};
use crate::{
debug::ResolvedLocation,
ui::{
action::Action,
panes::Pane,
state::State,
syntax_highlighting::{
Highlighter, HighlighterState, NoopHighlighter, SyntectHighlighter,
clamp_byte_selection_to_str,
},
tui::Frame,
},
};
pub struct SourceCodePane {
focused: bool,
current_source_id: SourceId,
current_span: SourceSpan,
current_line: u32,
current_col: u32,
num_lines: u32,
selected_line: u32,
syntax_highlighter: Box<dyn Highlighter>,
syntax_highlighting_states: BTreeMap<SourceId, Box<dyn HighlighterState>>,
current_file: Option<HighlightedFile>,
theme: Theme,
}
struct HighlightedFile {
source_file: Arc<SourceFile>,
lines: Vec<Vec<Span<'static>>>,
selected_span: SourceSpan,
gutter_width: u8,
}
impl SourceCodePane {
fn highlight_file(&mut self, resolved: &ResolvedLocation) -> HighlightedFile {
let highlighter_state = self
.syntax_highlighting_states
.entry(resolved.source_file.id())
.or_insert_with(|| {
let span_contents = resolved
.source_file
.read_span(&resolved.source_file.source_span().into(), 0, 0)
.expect("failed to read span of file");
self.syntax_highlighter.start_highlighter_state(span_contents.as_ref())
});
let resolved_span = resolved.span.into_slice_index();
let content = resolved.source_file.content();
let last_line = content.last_line_index();
let max_line_no = last_line.number().to_usize();
let gutter_width = max_line_no.ilog10() as u8;
let lines = (0..(max_line_no - 1))
.map(|line_index| {
let line_index = miden_debug_types::LineIndex::from(line_index as u32);
let span = content.line_range(line_index).expect("invalid line index");
let span = span.start.to_usize()..span.end.to_usize();
let is_highlighted = span.contains(&resolved_span.start)
&& span.contains(&resolved_span.end)
&& span != resolved_span;
let line_content =
strip_newline(&content.as_bytes()[span.start..span.end]).into_owned();
if is_highlighted {
let selection = if resolved.span.is_empty() {
0..(span.end - span.start)
} else {
(resolved_span.start - span.start)..(resolved_span.end - span.start)
};
let selection = clamp_byte_selection_to_str(&line_content, selection);
highlighter_state.highlight_line_with_selection(
line_content.into(),
selection,
self.theme.current_span,
)
} else {
highlighter_state.highlight_line(line_content.into())
}
})
.collect::<Vec<_>>();
HighlightedFile {
source_file: resolved.source_file.clone(),
lines,
selected_span: resolved.span,
gutter_width,
}
}
fn current_location(&self, state: &State) -> Option<ResolvedLocation> {
match state.executor().callstack.current_frame() {
Some(frame) => {
let resolved = frame.last_resolved(&state.source_manager);
resolved.cloned()
}
None if !self.current_source_id.is_unknown() => {
let source_file = state.source_manager.get(self.current_source_id).ok();
source_file.map(|src| ResolvedLocation {
source_file: src,
line: self.current_line,
col: self.current_col,
span: self.current_span,
})
}
None => {
None
}
}
}
}
struct Theme {
focused_border_style: Style,
current_line: Style,
current_span: Style,
line_number: Style,
#[allow(dead_code)]
gutter_border: Style,
}
impl Default for Theme {
fn default() -> Self {
Self {
focused_border_style: Style::default(),
current_line: Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED),
current_span: Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED),
line_number: Style::default(),
gutter_border: Style::default(),
}
}
}
impl Theme {
pub fn patch_from_syntect(&mut self, _theme: &syntect::highlighting::Theme) {
}
}
impl SourceCodePane {
pub fn new(focused: bool, focused_border_style: Style) -> Self {
let theme = Theme {
focused_border_style,
..Default::default()
};
Self {
focused,
current_source_id: SourceId::UNKNOWN,
num_lines: 0,
selected_line: 0,
current_line: 0,
current_col: 0,
current_span: SourceSpan::default(),
syntax_highlighter: Box::new(NoopHighlighter),
syntax_highlighting_states: Default::default(),
current_file: None,
theme,
}
}
fn reload(&mut self, state: &State) {
self.current_source_id = SourceId::UNKNOWN;
self.current_span = SourceSpan::default();
self.current_line = 0;
self.current_col = 0;
self.num_lines = 0;
self.selected_line = 0;
self.current_file = None;
if let Some(frame) = state.executor().callstack.current_frame()
&& let Some(loc) = frame.last_resolved(&state.source_manager)
{
self.current_file = Some(self.highlight_file(loc));
self.current_source_id = loc.source_file.id();
self.current_span = loc.span;
self.current_line = loc.line;
self.current_col = loc.col;
self.num_lines = loc.source_file.line_count() as u32;
self.selected_line = loc.line;
}
}
fn border_style(&self) -> Style {
match self.focused {
true => self.theme.focused_border_style,
false => Style::default(),
}
}
fn border_type(&self) -> BorderType {
match self.focused {
true => BorderType::Thick,
false => BorderType::Plain,
}
}
fn enable_syntax_highlighting(&mut self, state: &State) {
let nocolor = !state.config.color.should_attempt_color();
if nocolor {
return;
}
let syntax_set = syntect::parsing::SyntaxSet::load_defaults_nonewlines();
let theme_set = syntect::highlighting::ThemeSet::load_defaults();
let theme = theme_set.themes["base16-eighties.dark"].clone();
self.theme.patch_from_syntect(&theme);
self.syntax_highlighter = Box::new(SyntectHighlighter::new(syntax_set, theme, false));
}
}
impl Pane for SourceCodePane {
fn init(&mut self, state: &State) -> Result<(), Report> {
self.enable_syntax_highlighting(state);
if let Some(frame) = state.executor().callstack.current_frame()
&& let Some(loc) = frame.last_resolved(&state.source_manager)
{
self.current_file = Some(self.highlight_file(loc));
self.current_source_id = loc.source_file.id();
self.current_span = loc.span;
self.current_line = loc.line;
self.current_col = loc.col;
self.num_lines = loc.source_file.line_count() as u32;
self.selected_line = loc.line;
}
Ok(())
}
fn height_constraint(&self) -> Constraint {
match self.focused {
true => Constraint::Fill(3),
false => Constraint::Fill(3),
}
}
fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>, Report> {
match action {
Action::Down => {
if self.num_lines > 0 {
self.selected_line =
core::cmp::min(self.selected_line.saturating_add(1), self.num_lines);
}
return Ok(Some(Action::Update));
}
Action::Up => {
if self.num_lines > 0 {
self.selected_line =
core::cmp::min(self.selected_line.saturating_sub(1), self.num_lines);
}
return Ok(Some(Action::Update));
}
Action::Focus => {
self.focused = true;
static STATUS_LINE: &str = "[j,k → movement]";
return Ok(Some(Action::TimedStatusLine(STATUS_LINE.into(), 3)));
}
Action::UnFocus => {
self.focused = false;
}
Action::Submit => {}
Action::Update | Action::Reload => {
if action == Action::Reload {
self.reload(state);
}
if let Some(loc) = self.current_location(state) {
let source_id = loc.source_file.id();
if source_id != self.current_source_id {
self.current_file = Some(self.highlight_file(&loc));
self.current_source_id = source_id;
self.num_lines = loc.source_file.line_count() as u32;
self.selected_line = loc.line;
} else if self.selected_line != loc.line {
self.selected_line = loc.line;
}
self.current_span = loc.span;
self.current_line = loc.line;
self.current_col = loc.col;
}
}
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, _state: &State) -> Result<(), Report> {
let current_file = self.current_file.as_ref();
if current_file.is_none() {
frame.render_widget(
Block::default()
.title("Source Code")
.borders(Borders::ALL)
.border_style(self.border_style())
.border_type(self.border_type())
.title_bottom(
Line::from("no source code available for current instruction")
.right_aligned(),
)
.title(
Line::styled("nofile", Style::default().add_modifier(Modifier::ITALIC))
.right_aligned(),
),
area,
);
return Ok(());
}
let current_file = unsafe { current_file.unwrap_unchecked() };
let mut lines = current_file.lines.clone();
let selected_line = self.selected_line.saturating_sub(1) as usize;
let selected_line_deconstructed = lines[selected_line]
.iter()
.map(|span| {
(
crate::ui::syntax_highlighting::convert_to_syntect_style(span.style, false),
span.content.as_ref(),
)
})
.collect::<Vec<_>>();
let syntect_style = syntect::highlighting::StyleModifier {
foreground: self
.theme
.current_span
.fg
.map(crate::ui::syntax_highlighting::convert_to_syntect_color),
background: self
.theme
.current_span
.bg
.map(crate::ui::syntax_highlighting::convert_to_syntect_color),
font_style: if self.theme.current_span.add_modifier.is_empty() {
None
} else {
Some(crate::ui::syntax_highlighting::convert_to_font_style(
self.theme.current_span.add_modifier,
))
},
};
let span = current_file.selected_span;
let line_span = current_file
.source_file
.content()
.line_range((selected_line as u32).into())
.unwrap();
let selection_start = core::cmp::max(span.start(), line_span.start);
let selection_end = core::cmp::min(span.end(), line_span.end);
let selected = if selection_start >= selection_end {
0..0
} else {
let selected_span = SourceSpan::new(span.source_id(), selection_start..selection_end);
let selected = selected_span.into_slice_index();
if selected_span.is_empty() {
let start = selected.start - line_span.start.to_usize();
start..start
} else {
(selected.start - line_span.start.to_usize())
..(selected.end - line_span.start.to_usize())
}
};
let selected_line_content = selected_line_deconstructed
.iter()
.map(|(_, content)| *content)
.collect::<String>();
let selected = clamp_byte_selection_to_str(&selected_line_content, selected);
let mut parts = syntect::util::modify_range(
selected_line_deconstructed.as_slice(),
selected,
syntect_style,
)
.into_iter()
.map(|(style, str)| {
Span::styled(
str.to_string(),
crate::ui::syntax_highlighting::convert_style(style, true),
)
})
.collect();
lines[selected_line].clear();
lines[selected_line].append(&mut parts);
let gutter_width = self.current_file.as_ref().unwrap().gutter_width as usize;
let lines = lines.into_iter().enumerate().map(|(line_index, highlighted_parts)| {
let line_number_style = if line_index == selected_line {
self.theme.current_line
} else {
self.theme.line_number
};
Line::from_iter(
[
Span::styled(
format!("{line_no:gutter_width$}", line_no = line_index + 1),
line_number_style,
),
Span::styled(" | ", line_number_style),
]
.into_iter()
.chain(highlighted_parts),
)
});
let list = List::new(lines)
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(symbols::scrollbar::HORIZONTAL.end)
.highlight_spacing(HighlightSpacing::Always)
.scroll_padding(15);
let mut list_state = ListState::default().with_selected(Some(selected_line));
frame.render_stateful_widget(list, area, &mut list_state);
frame.render_widget(
Block::default()
.title("Source Code")
.borders(Borders::ALL)
.border_style(self.border_style())
.border_type(self.border_type())
.title_bottom(
Line::from(format!("{} of {}", self.selected_line, self.num_lines,))
.right_aligned(),
)
.title(
Line::styled(
current_file.source_file.deref().uri().as_str(),
Style::default().add_modifier(Modifier::ITALIC),
)
.right_aligned(),
),
area,
);
Ok(())
}
}
fn strip_newline(s: &[u8]) -> std::borrow::Cow<'_, str> {
if let Some(sans_newline) = s.strip_suffix(b"\n") {
String::from_utf8_lossy(sans_newline)
} else {
String::from_utf8_lossy(s)
}
}