mod instr;
use std::sync::Arc;
use comemo::Tracked;
use parking_lot::RwLock;
use tinymist_analysis::location::{PositionEncoding, to_lsp_position};
use tinymist_std::hash::{FxHashMap, FxHashSet};
use tinymist_world::vfs::FileId;
use typst::World;
use typst::diag::FileResult;
use typst::engine::Engine;
use typst::foundations::{Binding, Context, Dict, Scopes, func};
use typst::syntax::{Source, Span};
use typst_shim::syntax::source_range;
use crate::instrument::Instrumenter;
#[derive(Default)]
pub struct BreakpointInstr {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BreakpointKind {
CallStart,
CallEnd,
Function,
Break,
Continue,
Return,
BlockStart,
BlockEnd,
ShowStart,
ShowEnd,
DocStart,
DocEnd,
BeforeCompile,
AfterCompile,
}
impl BreakpointKind {
pub fn to_str(self) -> &'static str {
match self {
BreakpointKind::CallStart => "call_start",
BreakpointKind::CallEnd => "call_end",
BreakpointKind::Function => "function",
BreakpointKind::Break => "break",
BreakpointKind::Continue => "continue",
BreakpointKind::Return => "return",
BreakpointKind::BlockStart => "block_start",
BreakpointKind::BlockEnd => "block_end",
BreakpointKind::ShowStart => "show_start",
BreakpointKind::ShowEnd => "show_end",
BreakpointKind::DocStart => "doc_start",
BreakpointKind::DocEnd => "doc_end",
BreakpointKind::BeforeCompile => "before_compile",
BreakpointKind::AfterCompile => "after_compile",
}
}
}
#[derive(Default)]
pub struct BreakpointInfo {
pub meta: Vec<BreakpointItem>,
}
pub struct BreakpointItem {
pub kind: BreakpointKind,
pub function_name: Option<String>,
pub origin_span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SourceBreakpoint {
pub line: u32,
pub column: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceBreakpointResolution {
pub requested: SourceBreakpoint,
pub resolved: Option<ResolvedSourceBreakpoint>,
pub pending: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedSourceBreakpoint {
pub id: usize,
pub kind: BreakpointKind,
pub line: u32,
pub column: u32,
}
static DEBUG_SESSION: RwLock<Option<DebugSession>> = RwLock::new(None);
pub trait DebugSessionHandler: Send + Sync {
fn on_breakpoint(
&self,
engine: &Engine,
context: Tracked<Context>,
scopes: Scopes,
span: Span,
kind: BreakpointKind,
function_name: Option<String>,
);
}
pub struct DebugSession {
enabled_function_breakpoints: FxHashSet<(FileId, usize, BreakpointKind)>,
enabled_source_breakpoints: FxHashSet<(FileId, usize, BreakpointKind)>,
function_breakpoints: FxHashSet<String>,
source_breakpoints: FxHashMap<FileId, Vec<SourceBreakpoint>>,
breakpoints: FxHashMap<FileId, Arc<BreakpointInfo>>,
pub handler: Arc<dyn DebugSessionHandler>,
}
impl DebugSession {
pub fn new(handler: Arc<dyn DebugSessionHandler>) -> Self {
Self {
enabled_function_breakpoints: FxHashSet::default(),
enabled_source_breakpoints: FxHashSet::default(),
function_breakpoints: FxHashSet::default(),
source_breakpoints: FxHashMap::default(),
breakpoints: FxHashMap::default(),
handler,
}
}
pub fn set_function_breakpoints(&mut self, names: impl IntoIterator<Item = String>) {
self.function_breakpoints = names.into_iter().collect();
self.enabled_function_breakpoints.clear();
for (fid, info) in self.breakpoints.clone() {
self.enable_function_breakpoints_for(fid, &info);
}
}
pub fn set_source_breakpoints(
&mut self,
fid: FileId,
breakpoints: impl IntoIterator<Item = SourceBreakpoint>,
) {
let breakpoints = breakpoints.into_iter().collect::<Vec<_>>();
if breakpoints.is_empty() {
self.source_breakpoints.remove(&fid);
} else {
self.source_breakpoints.insert(fid, breakpoints);
}
self.enabled_source_breakpoints
.retain(|(enabled_fid, _, _)| *enabled_fid != fid);
}
pub fn set_source_breakpoints_for(
&mut self,
source: &Source,
breakpoints: impl IntoIterator<Item = SourceBreakpoint>,
) -> Vec<SourceBreakpointResolution> {
let fid = source.id();
self.set_source_breakpoints(fid, breakpoints);
let Some(info) = self.breakpoints.get(&fid).cloned() else {
return self.pending_source_breakpoints(fid);
};
self.enable_source_breakpoints_for(source, &info)
}
fn enable_breakpoints_for(
&mut self,
source: &Source,
info: &BreakpointInfo,
) -> Vec<SourceBreakpointResolution> {
let fid = source.id();
self.enable_function_breakpoints_for(fid, info);
self.enable_source_breakpoints_for(source, info)
}
fn enable_function_breakpoints_for(&mut self, fid: FileId, info: &BreakpointInfo) {
for (id, item) in info.meta.iter().enumerate() {
if !matches!(item.kind, BreakpointKind::Function) {
continue;
}
if item
.function_name
.as_ref()
.is_some_and(|name| self.function_breakpoints.contains(name))
{
self.enabled_function_breakpoints
.insert((fid, id, BreakpointKind::Function));
}
}
}
fn enable_source_breakpoints_for(
&mut self,
source: &Source,
info: &BreakpointInfo,
) -> Vec<SourceBreakpointResolution> {
let fid = source.id();
self.enabled_source_breakpoints
.retain(|(enabled_fid, _, _)| *enabled_fid != fid);
let Some(breakpoints) = self.source_breakpoints.get(&fid).cloned() else {
return vec![];
};
let candidates = SourceBreakpointCandidate::collect(source, info);
breakpoints
.into_iter()
.map(|requested| {
let resolved =
best_source_breakpoint_candidate(&requested, &candidates).map(|candidate| {
self.enabled_source_breakpoints
.insert((fid, candidate.id, candidate.kind));
ResolvedSourceBreakpoint {
id: candidate.id,
kind: candidate.kind,
line: candidate.line,
column: candidate.column,
}
});
SourceBreakpointResolution {
requested,
resolved,
pending: false,
}
})
.collect()
}
fn pending_source_breakpoints(&self, fid: FileId) -> Vec<SourceBreakpointResolution> {
self.source_breakpoints
.get(&fid)
.into_iter()
.flatten()
.cloned()
.map(|requested| SourceBreakpointResolution {
requested,
resolved: None,
pending: true,
})
.collect()
}
fn breakpoint_enabled(&self, bp: &(FileId, usize, BreakpointKind)) -> bool {
self.enabled_function_breakpoints.contains(bp)
|| self.enabled_source_breakpoints.contains(bp)
}
}
#[derive(Debug, Clone, Copy)]
struct SourceBreakpointCandidate {
id: usize,
kind: BreakpointKind,
line: u32,
column: u32,
}
impl SourceBreakpointCandidate {
fn collect(source: &Source, info: &BreakpointInfo) -> Vec<Self> {
info.meta
.iter()
.enumerate()
.filter_map(|(id, item)| {
if !is_source_breakpoint_target(item.kind) {
return None;
}
let range = source_range(source, item.origin_span)?;
let position = to_lsp_position(range.start, PositionEncoding::Utf16, source);
Some(Self {
id,
kind: item.kind,
line: position.line,
column: position.character,
})
})
.collect()
}
}
fn best_source_breakpoint_candidate(
breakpoint: &SourceBreakpoint,
candidates: &[SourceBreakpointCandidate],
) -> Option<SourceBreakpointCandidate> {
candidates
.iter()
.min_by_key(|candidate| {
let line_bucket = if matches!(candidate.kind, BreakpointKind::BlockEnd) {
3
} else if candidate.line == breakpoint.line {
0
} else if candidate.line > breakpoint.line {
1
} else {
2
};
let column = breakpoint.column.unwrap_or(0);
(
line_bucket,
candidate.line.abs_diff(breakpoint.line),
source_breakpoint_kind_priority(candidate.kind),
candidate.column.abs_diff(column),
candidate.id,
)
})
.copied()
}
fn is_source_breakpoint_target(kind: BreakpointKind) -> bool {
matches!(
kind,
BreakpointKind::Function
| BreakpointKind::BlockStart
| BreakpointKind::ShowStart
| BreakpointKind::Return
| BreakpointKind::BlockEnd
)
}
fn source_breakpoint_kind_priority(kind: BreakpointKind) -> u8 {
match kind {
BreakpointKind::Function => 0,
BreakpointKind::BlockStart => 1,
BreakpointKind::ShowStart => 2,
BreakpointKind::Return => 3,
BreakpointKind::BlockEnd => 4,
_ => 5,
}
}
pub fn with_debug_session<F, R>(f: F) -> Option<R>
where
F: FnOnce(&DebugSession) -> R,
{
Some(f(DEBUG_SESSION.read().as_ref()?))
}
pub fn set_debug_session(session: Option<DebugSession>) -> bool {
let mut lock = DEBUG_SESSION.write();
if session.is_some() && lock.is_some() {
return false;
}
let _ = std::mem::replace(&mut *lock, session);
true
}
pub fn set_debug_function_breakpoints(names: impl IntoIterator<Item = String>) -> bool {
let mut session = DEBUG_SESSION.write();
let Some(session) = session.as_mut() else {
return false;
};
session.set_function_breakpoints(names);
true
}
pub fn set_debug_source_breakpoints(
source: Source,
breakpoints: impl IntoIterator<Item = SourceBreakpoint>,
) -> Option<Vec<SourceBreakpointResolution>> {
let mut session = DEBUG_SESSION.write();
let session = session.as_mut()?;
Some(session.set_source_breakpoints_for(&source, breakpoints))
}
fn check_soft_breakpoint(span: Span, id: usize, kind: BreakpointKind) -> Option<bool> {
let fid = span.id()?;
let session = DEBUG_SESSION.read();
let session = session.as_ref()?;
let bp_feature = (fid, id, kind);
Some(session.breakpoint_enabled(&bp_feature))
}
fn soft_breakpoint_handle(
engine: &Engine,
context: Tracked<Context>,
span: Span,
id: usize,
kind: BreakpointKind,
scope: Option<Dict>,
) -> Option<()> {
let fid = span.id()?;
let (handler, origin_span, function_name) = {
let session = DEBUG_SESSION.read();
let session = session.as_ref()?;
let bp_feature = (fid, id, kind);
if !session.breakpoint_enabled(&bp_feature) {
return None;
}
let item = session.breakpoints.get(&fid)?.meta.get(id)?;
(
session.handler.clone(),
item.origin_span,
item.function_name.clone(),
)
};
let mut scopes = Scopes::new(Some(engine.world.library()));
if let Some(scope) = scope {
for (key, value) in scope.into_iter() {
scopes.top.bind(key.into(), Binding::detached(value));
}
}
handler.on_breakpoint(engine, context, scopes, origin_span, kind, function_name);
Some(())
}
pub mod breakpoints {
use super::*;
macro_rules! bp_handler {
($name:ident, $name2:expr, $name3:ident, $name4:expr, $title:expr, $kind:ident) => {
#[func(name = $name2, title = $title)]
pub fn $name(span: Span, id: usize) -> bool {
check_soft_breakpoint(span, id, BreakpointKind::$kind).unwrap_or_default()
}
#[func(name = $name4, title = $title)]
pub fn $name3(
engine: &Engine,
context: Tracked<Context>,
span: Span,
id: usize,
scope: Option<Dict>,
) {
soft_breakpoint_handle(engine, context, span, id, BreakpointKind::$kind, scope);
}
};
}
bp_handler!(
__breakpoint_call_start,
"__breakpoint_call_start",
__breakpoint_call_start_handle,
"__breakpoint_call_start_handle",
"A Software Breakpoint at the start of a call.",
CallStart
);
bp_handler!(
__breakpoint_call_end,
"__breakpoint_call_end",
__breakpoint_call_end_handle,
"__breakpoint_call_end_handle",
"A Software Breakpoint at the end of a call.",
CallEnd
);
bp_handler!(
__breakpoint_function,
"__breakpoint_function",
__breakpoint_function_handle,
"__breakpoint_function_handle",
"A Software Breakpoint at the start of a function.",
Function
);
bp_handler!(
__breakpoint_break,
"__breakpoint_break",
__breakpoint_break_handle,
"__breakpoint_break_handle",
"A Software Breakpoint at a break.",
Break
);
bp_handler!(
__breakpoint_continue,
"__breakpoint_continue",
__breakpoint_continue_handle,
"__breakpoint_continue_handle",
"A Software Breakpoint at a continue.",
Continue
);
bp_handler!(
__breakpoint_return,
"__breakpoint_return",
__breakpoint_return_handle,
"__breakpoint_return_handle",
"A Software Breakpoint at a return.",
Return
);
bp_handler!(
__breakpoint_block_start,
"__breakpoint_block_start",
__breakpoint_block_start_handle,
"__breakpoint_block_start_handle",
"A Software Breakpoint at the start of a block.",
BlockStart
);
bp_handler!(
__breakpoint_block_end,
"__breakpoint_block_end",
__breakpoint_block_end_handle,
"__breakpoint_block_end_handle",
"A Software Breakpoint at the end of a block.",
BlockEnd
);
bp_handler!(
__breakpoint_show_start,
"__breakpoint_show_start",
__breakpoint_show_start_handle,
"__breakpoint_show_start_handle",
"A Software Breakpoint at the start of a show.",
ShowStart
);
bp_handler!(
__breakpoint_show_end,
"__breakpoint_show_end",
__breakpoint_show_end_handle,
"__breakpoint_show_end_handle",
"A Software Breakpoint at the end of a show.",
ShowEnd
);
bp_handler!(
__breakpoint_doc_start,
"__breakpoint_doc_start",
__breakpoint_doc_start_handle,
"__breakpoint_doc_start_handle",
"A Software Breakpoint at the start of a doc.",
DocStart
);
bp_handler!(
__breakpoint_doc_end,
"__breakpoint_doc_end",
__breakpoint_doc_end_handle,
"__breakpoint_doc_end_handle",
"A Software Breakpoint at the end of a doc.",
DocEnd
);
bp_handler!(
__breakpoint_before_compile,
"__breakpoint_before_compile",
__breakpoint_before_compile_handle,
"__breakpoint_before_compile_handle",
"A Software Breakpoint before compilation.",
BeforeCompile
);
bp_handler!(
__breakpoint_after_compile,
"__breakpoint_after_compile",
__breakpoint_after_compile_handle,
"__breakpoint_after_compile_handle",
"A Software Breakpoint after compilation.",
AfterCompile
);
}