///|
priv struct HumanRenderer {}
///|
fn HumanRenderer::new() -> HumanRenderer {
HumanRenderer::{ }
}
///|
fn help_padding(width : Int, content_length : Int) -> String {
let padding = width - content_length
" ".repeat(if padding > 1 { padding } else { 2 })
}
///|
fn format_help_entry(entry : CommandHelpEntry) -> String {
let alias_usage = entry.alias_usage()
guard alias_usage != "" else {
let padding = help_padding(38, entry.usage.length())
return " \{entry.usage}\{padding}\{entry.summary}"
}
let multiple_aliases = entry.aliases.length() > 1
let usage_width = if multiple_aliases { 22 } else { 21 }
let usage_padding = help_padding(usage_width, entry.usage.length())
let alias_padding = help_padding(17, alias_usage.length())
" \{entry.usage}\{usage_padding}\{alias_usage}\{alias_padding}\{entry.summary}"
}
///|
fn render_help(entries : Array[CommandHelpEntry]) -> Unit {
println("Commands:")
for entry in entries {
println(format_help_entry(entry))
}
}
///|
const TERMINAL_OUTPUT_BEGIN : String = ">>>>>>>>> begin terminal output >>>>>>>>>"
///|
const TERMINAL_OUTPUT_END : String = "<<<<<<<<< end terminal output <<<<<<<<<<<"
///|
async fn render_debuggee_outputs(
output_items : Array[@debugger.DebugOutput],
) -> Unit {
guard output_items is [_, ..] else { return }
println(TERMINAL_OUTPUT_BEGIN)
println("")
let mut ended_with_newline = true
for output in output_items {
@stdio.stdout.write(output.text())
ended_with_newline = output.text().has_suffix("\n") ||
output.text().has_suffix("\r")
}
if !ended_with_newline {
println("")
}
println("")
println(TERMINAL_OUTPUT_END)
}
///|
fn render_breakpoint_update(breakpoint : @debugger.BreakpointSnapshot) -> Unit {
let requested = "\{breakpoint.source_path()}:\{breakpoint.requested_line()}"
match breakpoint.status() {
@debugger.Pending =>
println("Breakpoint \{breakpoint.id()} is still pending at \{requested}")
@debugger.Verified =>
match breakpoint.actual_line() {
Some(actual_line) if actual_line != breakpoint.requested_line() =>
println(
"Breakpoint \{breakpoint.id()} moved from \{requested} to \{breakpoint.source_path()}:\{actual_line}",
)
_ => println("Breakpoint \{breakpoint.id()} verified at \{requested}")
}
@debugger.Rejected(message) =>
println(
"Breakpoint \{breakpoint.id()} rejected at \{requested}: \{message}",
)
}
}
///|
fn render_function_breakpoint_update(
breakpoint : @debugger.FunctionBreakpointSnapshot,
) -> Unit {
match breakpoint.status() {
@debugger.Pending =>
println(
"Breakpoint \{breakpoint.id()} is still pending at function \{breakpoint.display_name()}",
)
@debugger.Verified =>
match breakpoint.location_count() {
Some(count) if count > 1 =>
println(
"Breakpoint \{breakpoint.id()} verified at function \{breakpoint.display_name()} (\{count} locations)",
)
_ =>
println(
"Breakpoint \{breakpoint.id()} verified at function \{breakpoint.display_name()}",
)
}
@debugger.Rejected(message) =>
println(
"Breakpoint \{breakpoint.id()} rejected at function \{breakpoint.display_name()}: \{message}",
)
}
}
///|
fn stop_location(snapshot : @debugger.StopSnapshot) -> String {
match (snapshot.source_path(), snapshot.line()) {
(Some(source_path), Some(line)) => "\{source_path}:\{line}"
(Some(source_path), None) => source_path
(None, _) => "an unknown source location"
}
}
///|
fn source_window(
current_line : Int,
total_lines : Int,
line_count~ : Int,
) -> (Int, Int) {
let visible_count = if line_count < total_lines {
line_count
} else {
total_lines
}
let centered_first = if current_line > visible_count / 2 {
current_line - visible_count / 2
} else {
1
}
let maximum_first = total_lines - visible_count + 1
let first_line = if centered_first < maximum_first {
centered_first
} else {
maximum_first
}
(first_line, first_line + visible_count - 1)
}
///|
async fn render_source_view(view : SourceView) -> Unit {
guard (view.source_path, view.line) is (Some(source_path), Some(line)) else {
println(
"Source unavailable: lldb-dap did not provide a local source path and line.",
)
return
}
let source = @fs.read_file(source_path).text() catch {
error => {
println("Source unavailable: \{@debug.to_string(error)}")
return
}
}
let lines = @render.render_mbt_lines(
source,
theme=@render.Theme::gruvbox_dark(),
)
guard line > 0 && line <= lines.length() else {
println(
"Source unavailable: line \{line} is outside \{source_path} (1..\{lines.length()}).",
)
return
}
let (first_line, last_line) = source_window(
line,
lines.length(),
line_count=view.line_count,
)
let width = last_line.to_string().length()
for source_line in first_line..<=last_line {
let number = source_line.to_string()
let padding = " ".repeat(width - number.length())
let marker = if source_line == line { ">" } else { " " }
println("\{marker} \{padding}\{number} │ \{lines[source_line - 1]}")
}
}
///|
fn render_stop_header(snapshot : @debugger.StopSnapshot) -> Unit {
println(
"Stopped (\{snapshot.reason()}) in \{snapshot.function_name()} at \{stop_location(snapshot)}",
)
}
///|
fn stack_frame_location(frame : @debugger.StackFrameSnapshot) -> String {
match (frame.source_path(), frame.line()) {
(Some(source_path), Some(line)) => "\{source_path}:\{line}"
(Some(source_path), None) => source_path
(None, _) => "source unavailable"
}
}
///|
fn stack_frame_kind_label(kind : @debugger.FrameKind) -> String {
match kind {
@debugger.MoonBitUserFrame => "moonbit"
@debugger.MoonBitRuntimeFrame => "runtime"
@debugger.NativeFrame => "native"
@debugger.UnknownFrame => "unknown"
}
}
///|
fn backtrace_lines(view : BacktraceView) -> Array[String] {
guard view.frames is [_, ..] else {
return ["No stack frames are available."]
}
let lines = view.frames.map(frame => {
let marker = if view.selected_frame == Some(frame.physical_index()) {
"*"
} else {
" "
}
"\{marker} #\{frame.physical_index()} [\{stack_frame_kind_label(frame.kind())}] \{frame.display_name()} at \{stack_frame_location(frame)}"
})
if view.selected_frame is Some(selected) &&
!view.frames.any(frame => frame.physical_index() == selected) {
lines.push(
" Current frame #\{selected} is hidden; use 'bt --all' to show it.",
)
}
lines
}
///|
fn render_backtrace(view : BacktraceView) -> Unit {
for line in backtrace_lines(view) {
println(line)
}
}
///|
fn frame_selected_line(frame : @debugger.StackFrameSnapshot) -> String {
"Selected frame #\{frame.physical_index()}: \{frame.display_name()} at \{stack_frame_location(frame)}"
}
///|
fn render_frame_selection_failure(failure : FrameSelectionFailure) -> Unit {
match failure {
FrameUnavailable(index) => println("Frame #\{index} is not available.")
CurrentFrameUnavailable => println("No current frame is selected.")
CallerBoundary(index) =>
println("Frame #\{index} has no visible MoonBit caller.")
CalleeBoundary(index) =>
println("Frame #\{index} has no visible MoonBit callee.")
}
}
///|
fn moonbit_variable_type_name(type_name : StringView) -> String {
match type_name {
"bool" => "Bool"
"float" => "Float"
"double" => "Double"
"int32_t" => "Int"
"uint32_t" => "UInt"
"int64_t" => "Int64"
"uint64_t" => "UInt64"
_ => {
let base : StringView = match type_name.strip_suffix(" &") {
Some(base) => base
None =>
match type_name.strip_suffix(" *") {
Some(base) => base
None => type_name
}
}
match base.rev_split_once("/") {
Some((package_name, source_name)) if package_name != "" &&
source_name != "" => source_name.to_owned()
_ => base.to_owned()
}
}
}
}
///|
fn variable_label(variable : @debugger.VariableSnapshot) -> String {
match variable.type_name() {
Some(type_name) =>
"\{variable.name()}: \{moonbit_variable_type_name(type_name)}"
None => variable.name()
}
}
///|
const INDEXED_COLLECTION_CONTENT_WIDTH : Int = 100
///|
fn indexed_element_text(
element : @debugger.IndexedVariableElement,
element_type_name : String,
) -> String {
match element.value() {
Some(value) => value.value()
None => element_type_name
}
}
///|
fn indexed_content_text(
collection : @debugger.IndexedCollectionSnapshot,
selected : Array[@debugger.IndexedVariableElement],
) -> String {
let parts : Array[String] = []
let mut next_index = 0
for element in selected {
if element.index() > next_index {
parts.push("...")
}
parts.push(indexed_element_text(element, collection.element_type_name()))
next_index = element.index() + 1
}
if next_index < collection.total_length() {
parts.push("...")
}
"[\{parts.join(", ")}]"
}
///|
fn bounded_indexed_content_text(
collection : @debugger.IndexedCollectionSnapshot,
) -> String {
let selected : Array[@debugger.IndexedVariableElement] = []
selected.push_iter(collection.preview_elements().iter())
let mut content = indexed_content_text(collection, selected)
while content.length() > INDEXED_COLLECTION_CONTENT_WIDTH &&
selected.length() > 2 {
selected.remove(selected.length() - 2) |> ignore
content = indexed_content_text(collection, selected)
}
content
}
///|
fn append_indexed_collection_lines(
lines : Array[String],
variable : @debugger.VariableSnapshot,
collection : @debugger.IndexedCollectionSnapshot,
indentation : Int,
) -> Unit {
let prefix = " ".repeat(indentation)
lines.push(
"\{prefix}\{variable.name()}: \{collection.kind().source_name()}[\{collection.element_type_name()}] with length = \{collection.total_length()}",
)
lines.push("")
lines.push("\{prefix} \{bounded_indexed_content_text(collection)}")
}
///|
fn enum_payload_text(payload : @debugger.VariableEnumPayload) -> String {
payload.scalar_value().unwrap_or(payload.type_name())
}
///|
fn append_enum_lines(
lines : Array[String],
variable : @debugger.VariableSnapshot,
enum_value : @debugger.VariableEnumSnapshot,
indentation : Int,
) -> Unit {
let prefix = " ".repeat(indentation)
let payload = enum_value.payload().map(enum_payload_text)
let rendered = if payload is [] {
enum_value.constructor_name()
} else {
"\{enum_value.constructor_name()}(\{payload.join(", ")})"
}
lines.push("\{prefix}\{variable_label(variable)}")
lines.push("")
lines.push("\{prefix} \{rendered}")
}
///|
fn append_variable_lines(
lines : Array[String],
variable : @debugger.VariableSnapshot,
indentation : Int,
) -> Unit {
let prefix = " ".repeat(indentation)
let label = variable_label(variable)
match variable.content() {
@debugger.VariableLeaf =>
lines.push("\{prefix}\{label} = \{variable.value()}")
@debugger.VariableStructure(structure) => {
let children = structure.children()
if children is [] && !structure.is_truncated() {
lines.push("\{prefix}\{label} = {}")
} else {
lines.push("\{prefix}\{label} = {")
for child in children {
append_variable_lines(lines, child, indentation + 2)
}
if structure.is_truncated() {
lines.push("\{prefix} …")
}
lines.push("\{prefix}}")
}
}
@debugger.VariableIndexedCollection(collection) =>
append_indexed_collection_lines(lines, variable, collection, indentation)
@debugger.VariableEnum(enum_value) =>
append_enum_lines(lines, variable, enum_value, indentation)
}
}
///|
fn variable_lines(variable : @debugger.VariableSnapshot) -> Array[String] {
let lines : Array[String] = []
append_variable_lines(lines, variable, 0)
lines
}
///|
fn render_variable(variable : @debugger.VariableSnapshot) -> Unit {
for line in variable_lines(variable) {
println(line)
}
}
///|
fn render_process_exit(exit_code : Int?) -> Unit {
match exit_code {
Some(0) => println("Process exited normally (code 0).")
Some(code) => println("Process exited with code \{code}.")
None => println("Process exited.")
}
}
///|
fn render_breakpoint_error(error : BreakpointCommandError) -> Unit {
match error {
BreakpointCurrentFrameMissing =>
println(
"Cannot set breakpoint by line: no stopped frame is selected. Use b :.",
)
BreakpointCurrentFrameSourceMissing =>
println(
"Cannot set breakpoint by line: the selected frame has no source file. Use b :.",
)
BreakpointSourceNotFound(path) =>
println("Cannot set breakpoint: source file not found: \{path}")
BreakpointSourceNotAFile(path) =>
println("Cannot set breakpoint: source path is not a file: \{path}")
BreakpointOperationNotAllowed(state, action) =>
println(
"Cannot set breakpoint: \{action} is not allowed while the debugger is \{@debug.to_string(state)}",
)
BreakpointFunctionContextMissing =>
println(
"Cannot set function breakpoint: package context is missing; launch moondbg with a package directory or provide --current-package.",
)
BreakpointUnknownPackageAlias(package_alias) =>
println(
"Cannot set function breakpoint: unknown package alias '@\{package_alias}'.",
)
BreakpointUnexpectedFailure(message) =>
println("Cannot set breakpoint: \{message}")
}
}
///|
fn variable_lookup_failure_message(
failure : @debugger.VariableLookupFailure,
) -> String {
failure.message()
}
///|
fn local_type_suffix(type_name : String?) -> String {
match type_name {
Some(name) if name != "" => ": \{name}"
_ => ""
}
}
///|
fn local_summary_line(variable : @debugger.LocalVariableSnapshot) -> String {
let prefix = " \{variable.name()}\{local_type_suffix(variable.type_name())}"
match variable.availability() {
@debugger.LocalVariableUnavailable => "\{prefix} = "
@debugger.LocalVariableAmbiguous => "\{prefix} = "
@debugger.LocalVariableAvailable(value) =>
match value {
@debugger.LocalScalar(scalar) => "\{prefix} = \{scalar}"
@debugger.LocalCollection(_, length) =>
"\{prefix} with length = \{length}"
@debugger.LocalEnumConstructor(name) => "\{prefix} = \{name}"
@debugger.LocalStructure | @debugger.LocalOpaque => prefix
}
}
}
///|
fn local_snapshot_lines(
snapshot : @debugger.LocalVariablesSnapshot,
) -> Array[String] {
guard snapshot.scope_availability() is @debugger.LocalScopeAvailable else {
return ["Local variables are unavailable in the selected frame."]
}
let variables = snapshot.variables()
guard variables.length() > 0 else {
return ["No local variables in the selected frame."]
}
let lines = ["Locals:"]
for variable in variables {
lines.push(local_summary_line(variable))
}
lines
}
///|
fn render_locals(snapshot : @debugger.LocalVariablesSnapshot) -> Unit {
for line in local_snapshot_lines(snapshot) {
println(line)
}
}
///|
async fn render_presentation_event(event : PresentationEvent) -> Unit {
match event {
HelpPresentation(entries) => render_help(entries)
UserErrorPresentation(message) => println(message)
DebuggeeOutputPresentation(output) => render_debuggee_outputs(output)
ProgramRestartingPresentation => println("Restarting program.")
BreakpointPendingPresentation(id, source, line) =>
println("Breakpoint \{id} pending at \{source}:\{line}")
BreakpointUpdatePresentation(breakpoint) =>
render_breakpoint_update(breakpoint)
FunctionBreakpointPendingPresentation(id, display_name) =>
println("Breakpoint \{id} pending at function \{display_name}")
FunctionBreakpointUpdatePresentation(breakpoint) =>
render_function_breakpoint_update(breakpoint)
BreakpointErrorPresentation(error) => render_breakpoint_error(error)
BreakpointsPresentation(entries) =>
for line in breakpoint_list_lines(entries) {
println(line)
}
BreakpointManagedPresentation(result) =>
println(breakpoint_managed_line(result))
BreakpointManagementFailurePresentation(failure) =>
println(breakpoint_management_failure_line(failure))
StopPresentation(snapshot) => render_stop_header(snapshot)
BacktracePresentation(view) => render_backtrace(view)
FrameSelectedPresentation(frame) => println(frame_selected_line(frame))
FrameSelectionFailurePresentation(failure) =>
render_frame_selection_failure(failure)
SourceViewPresentation(view) => render_source_view(view)
VariablePresentation(variable) => render_variable(variable)
VariableLookupFailurePresentation(failure) =>
println(variable_lookup_failure_message(failure))
LocalsPresentation(snapshot) => render_locals(snapshot)
NoStoppedSourcePresentation =>
println("No stopped source location is available.")
ProcessExitPresentation(exit_code) => render_process_exit(exit_code)
OperationNotAllowedPresentation(state, action) =>
println(
"Cannot \{action} while the debugger is \{@debug.to_string(state)}.",
)
AdapterFailurePresentation(message) =>
println("moondbg: debugger adapter failed: \{message}")
}
}
///|
impl PresentationRenderer for HumanRenderer with fn render(_self, events) {
for event in events {
render_presentation_event(event)
}
}