| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/developer/debug/zxdb/console/format_context.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "src/developer/debug/zxdb/client/arch_info.h" |
| #include "src/developer/debug/zxdb/client/disassembler.h" |
| #include "src/developer/debug/zxdb/client/memory_dump.h" |
| #include "src/developer/debug/zxdb/client/process.h" |
| #include "src/developer/debug/zxdb/client/session.h" |
| #include "src/developer/debug/zxdb/client/setting_schema_definition.h" |
| #include "src/developer/debug/zxdb/common/file_util.h" |
| #include "src/developer/debug/zxdb/common/string_util.h" |
| #include "src/developer/debug/zxdb/console/command_utils.h" |
| #include "src/developer/debug/zxdb/console/console.h" |
| #include "src/developer/debug/zxdb/console/format_location.h" |
| #include "src/developer/debug/zxdb/console/format_table.h" |
| #include "src/developer/debug/zxdb/console/output_buffer.h" |
| #include "src/developer/debug/zxdb/console/string_util.h" |
| #include "src/developer/debug/zxdb/expr/expr_tokenizer.h" |
| #include "src/developer/debug/zxdb/expr/keywords.h" |
| #include "src/developer/debug/zxdb/symbols/input_location.h" |
| #include "src/developer/debug/zxdb/symbols/loaded_module_symbols.h" |
| #include "src/developer/debug/zxdb/symbols/location.h" |
| #include "src/developer/debug/zxdb/symbols/module_symbols.h" |
| #include "src/developer/debug/zxdb/symbols/process_symbols.h" |
| #include "src/developer/debug/zxdb/symbols/resolve_options.h" |
| #include "src/developer/debug/zxdb/symbols/source_file_provider.h" |
| #include "src/developer/debug/zxdb/symbols/source_util.h" |
| #include "src/lib/files/file.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| |
| namespace zxdb { |
| |
| namespace { |
| |
| using LineInfo = std::pair<int, std::string>; // Line #, Line contents. |
| using LineVector = std::vector<LineInfo>; |
| |
| OutputBuffer FormatSourceLineNoSyntax(const FormatSourceOpts& opts, bool is_highlight_line, |
| const std::string& line) { |
| if (!is_highlight_line) { |
| // Non-highlighted lines just get output in either regular or dim. |
| Syntax syntax = opts.dim_others ? Syntax::kComment : Syntax::kNormal; |
| return OutputBuffer(syntax, line); |
| } |
| |
| // Highlighted lines may need part of the line highligted or all of it. Convert the colum to |
| // 0-based with clamping (since the offsets come from symbols, they could be invalid). |
| int line_size = static_cast<int>(line.size()); |
| int col_index = std::min(std::max(0, opts.highlight_column - 1), line_size); |
| |
| OutputBuffer result; |
| if (col_index == 0) { |
| result.Append(Syntax::kHeading, line); |
| } else { |
| result.Append(Syntax::kNormal, line.substr(0, col_index)); |
| if (col_index < line_size) |
| result.Append(Syntax::kHeading, line.substr(col_index)); |
| } |
| return result; |
| } |
| |
| struct SyntaxVariants { |
| SyntaxVariants(Syntax normal_in, Syntax dim_in, Syntax bold_in) |
| : normal(normal_in), dim(dim_in), bold(bold_in) {} |
| Syntax normal; |
| Syntax dim; |
| Syntax bold; |
| }; |
| |
| SyntaxVariants SyntaxForTokenType(ExprTokenType type) { |
| // Normal names and such. |
| if (type == ExprTokenType::kInvalid || type == ExprTokenType::kName) |
| return SyntaxVariants(Syntax::kNormal, Syntax::kComment, Syntax::kHeading); |
| |
| // Numbers. Treat true and false as numbers as well. |
| if (type == ExprTokenType::kFloat || type == ExprTokenType::kInteger || |
| type == ExprTokenType::kTrue || type == ExprTokenType::kFalse) |
| return SyntaxVariants(Syntax::kNumberNormal, Syntax::kNumberDim, Syntax::kNumberBold); |
| |
| // Strings. |
| if (type == ExprTokenType::kStringLiteral) |
| return SyntaxVariants(Syntax::kStringNormal, Syntax::kStringDim, Syntax::kStringBold); |
| |
| // Comments. |
| if (type == ExprTokenType::kComment) |
| return SyntaxVariants(Syntax::kComment, Syntax::kComment, Syntax::kComment); |
| |
| // Assume everything that's an alphanumeric token is a keyword. Count Rust lifetimes as keywords |
| // also since they're special language things. |
| const ExprTokenRecord& record = RecordForTokenType(type); |
| if (record.is_alphanum || type == ExprTokenType::kRustLifetime) |
| return SyntaxVariants(Syntax::kKeywordNormal, Syntax::kKeywordDim, Syntax::kKeywordBold); |
| |
| // Everything else is an operator. |
| return SyntaxVariants(Syntax::kOperatorNormal, Syntax::kOperatorDim, Syntax::kOperatorBold); |
| } |
| |
| // Assumes a valid nonempty token list. |
| OutputBuffer FormatSourceLineWithTokens(const FormatSourceOpts& opts, bool is_highlight_line, |
| const std::string& line, |
| const std::vector<ExprToken>& tokens) { |
| FX_DCHECK(!tokens.empty()); |
| FX_DCHECK(opts.language); |
| |
| // The code here always uses the text from the source file. We always want to show the literal |
| // source rather than what the tokenizer interpreted it as (though normally these will be the |
| // same). |
| OutputBuffer out; |
| |
| // Specially handle C preprocessor and Rust attributes. Assume these lines start with a "#" and |
| // ignore everything after it. Since this code is line-based anyway, the lack of multiline support |
| // for these constructs isn't an additional limitation. This just makes the typical uses look |
| // better. |
| if (tokens[0].type() == ExprTokenType::kOctothorpe) { |
| out.Append(Syntax::kComment, line); |
| return out; |
| } |
| |
| // Construct a list of ranges indicating the syntax type. The last item will reference the end of |
| // the list to make end conditions easier to handle. |
| using OffsetType = std::pair<size_t, ExprTokenType>; |
| std::vector<OffsetType> spans; |
| if (tokens[0].byte_offset() > 0) // Stuff before first token (normally whitespace). |
| spans.emplace_back(0, ExprTokenType::kInvalid); |
| |
| const std::set<std::string>& keywords = AllKeywordsForLanguage(*opts.language, true); |
| for (const auto& token : tokens) { |
| // The tokenizer doesn't kave tokens for all keywords. Check the name to see if it's a common |
| // builtin to annotate accordingly. |
| if (token.type() == ExprTokenType::kName && keywords.find(token.value()) != keywords.end()) { |
| // Keyword or quasi-built-in. Since there's no general "keyword" token type, assign these all |
| // to the "if" token which will trigger the keyword formatting. |
| spans.emplace_back(token.byte_offset(), ExprTokenType::kIf); |
| } else if (token.type() == ExprTokenType::kCommentBlockEnd) { |
| // We have a "*/" on a line. Assume that everything before it was actually a comment and |
| // we just didn't see the opening "/*" on a previous line. |
| spans.clear(); |
| spans.emplace_back(0, ExprTokenType::kComment); |
| } else { |
| // All other tokens. |
| spans.emplace_back(token.byte_offset(), token.type()); |
| } |
| } |
| spans.emplace_back(line.size(), ExprTokenType::kInvalid); // End boundary. |
| |
| // Convert spans to formatted text. Skip the last once since that's the dummy span at the end. |
| for (size_t i = 0; i < spans.size() - 1; i++) { |
| // Since we added a dummy span at the end not covered by the loop, there will always be a next |
| // span. |
| size_t begin_offset = spans[i].first; |
| size_t end_offset = spans[i + 1].first; |
| if (begin_offset == end_offset) |
| continue; |
| |
| SyntaxVariants variants = SyntaxForTokenType(spans[i].second); |
| Syntax syntax = variants.normal; |
| if (!is_highlight_line) { |
| // Non-highlighted lines just get output in either regular or dim. |
| if (opts.dim_others) |
| syntax = variants.dim; |
| } else { |
| // Highlighted line, Anything past the (1-based) colum gets bolded, everything else is normal. |
| if (static_cast<int>(begin_offset) >= opts.highlight_column - 1) |
| syntax = variants.bold; |
| } |
| |
| out.Append(syntax, line.substr(begin_offset, end_offset - begin_offset)); |
| } |
| |
| return out; |
| } |
| |
| // Retrieves the proper MOduleSymbols (or null) for the given location as a weak pointer. This is |
| // used to compute the right module to ask for out-of-date file warnings. |
| fxl::WeakPtr<ModuleSymbols> GetWeakModuleForLocation(Process* process, const Location& location) { |
| if (LoadedModuleSymbols* loaded_sym = |
| process->GetSymbols()->GetModuleForAddress(location.address())) |
| return loaded_sym->module_symbols()->GetWeakPtr(); |
| return fxl::WeakPtr<ModuleSymbols>(); |
| } |
| |
| // Generates the source listing for source intersperced with assembly code for the source between |
| // the given two lines. The prev_line is the last one outputted. |
| // |
| // This re-opens and line splits the file for each block of source shown. This is very inefficient |
| // but normally disassembly is not performance sensitive. If needed this could be cached. |
| // |
| // The module_for_time_warning is an optional pointer to the module corresponding to this source |
| // file so we can show warnings if the build is out-of-date. |
| OutputBuffer FormatAsmSourceForRange(Process* process, |
| fxl::WeakPtr<ModuleSymbols> module_for_time_warning, |
| const SourceFileProvider& file_provider, |
| const FileLine& prev_line, const FileLine& line) { |
| // Maximum number of lines of source we'll include. |
| constexpr int kMaxContext = 4; |
| |
| int first_num = line.line() - kMaxContext + 1; // Most context we'll show. |
| if (prev_line.file() == line.file()) // Same file, try to include since the last line. |
| first_num = std::max(prev_line.line() + 1, first_num); |
| first_num = std::max(1, first_num); // Clamp to beginning of file. |
| |
| FormatSourceOpts opts; |
| opts.first_line = first_num; |
| opts.last_line = line.line(); |
| opts.left_indent = 2; |
| opts.dim_others = true; // Dim everything (we didn't specify an active line). |
| opts.module_for_time_warning = std::move(module_for_time_warning); |
| |
| FileLine start_line(line.file(), line.comp_dir(), first_num); |
| OutputBuffer out; |
| if (FormatSourceFileContext(start_line, file_provider, opts, &out).ok()) { |
| // The formatted table will end in a newline which will combine with our table's newline and |
| // insert a blank below the source code. Trim the embedded newline so we only get one. |
| out.TrimTrailingNewlines(); |
| return out; |
| } |
| |
| // Some error getting the source code, show the location file/line number instead. |
| return FormatFileLine(start_line, process->GetSymbols()->target_symbols()); |
| } |
| |
| // Describes the destination for the given call destination, formatted as for a disassembly. The |
| // process may be null which will mean only addresses will be printed, no symbols. |
| OutputBuffer DescribeAsmCallDest(Process* process, uint64_t call_dest) { |
| OutputBuffer result(Syntax::kComment, GetRightArrow() + " "); |
| |
| std::vector<Location> resolved; |
| if (process) { |
| // If there are multiple symbols starting at the given location (like nested inline calls), use |
| // the outermost one since this is a jump *to* that location. |
| ResolveOptions options; |
| options.ambiguous_inline = ResolveOptions::AmbiguousInline::kOuter; |
| |
| resolved = process->GetSymbols()->ResolveInputLocation(InputLocation(call_dest), options); |
| FX_DCHECK(resolved.size() == 1); // Addresses should always match one location. |
| } else { |
| // Can't symbolize, use the address. |
| resolved.emplace_back(Location(Location::State::kAddress, call_dest)); |
| } |
| |
| FormatLocationOptions opts; |
| if (process) |
| opts = FormatLocationOptions(process->GetTarget()); |
| opts.always_show_addresses = false; |
| opts.show_file_line = false; |
| |
| result.Append(FormatLocation(resolved[0], opts)); |
| return result; |
| } |
| |
| } // namespace |
| |
| void FormatSourceOpts::SetLanguageFromFileName(const std::string& file_name) { |
| language = FileNameToLanguage(file_name); |
| if (!language) { |
| // Default to C for anything still unknown because it should give reasonable highlighting for |
| // most languages. |
| language = ExprLanguage::kC; |
| } |
| } |
| |
| Err OutputSourceContext(Process* process, std::unique_ptr<SourceFileProvider> file_provider, |
| const Location& location, SourceAffinity source_affinity) { |
| if (source_affinity != SourceAffinity::kAssembly && location.file_line().is_valid()) { |
| // Synchronous source output. |
| FormatSourceOpts source_opts; |
| source_opts.active_line = location.file_line().line(); |
| source_opts.highlight_line = source_opts.active_line; |
| source_opts.highlight_column = location.column(); |
| source_opts.first_line = source_opts.active_line - 2; |
| source_opts.last_line = source_opts.active_line + 2; |
| source_opts.dim_others = true; |
| source_opts.module_for_time_warning = GetWeakModuleForLocation(process, location); |
| |
| if (const Symbol* sym = location.symbol().Get()) |
| source_opts.language = DwarfLangToExprLanguage(sym->GetLanguage()); |
| |
| OutputBuffer out; |
| Err err = FormatSourceFileContext(location.file_line(), *file_provider, source_opts, &out); |
| if (err.has_error()) |
| return err; |
| |
| Console::get()->Output(out); |
| } else { |
| // Fall back to disassembly. |
| FormatAsmOpts options; |
| options.emit_addresses = true; |
| options.emit_bytes = false; |
| options.include_source = true; |
| options.active_address = location.address(); |
| |
| uint64_t start_address; |
| const ArchInfo& arch_info = process->session()->arch_info(); |
| if (arch_info.is_fixed_instr()) { |
| // Fixed instruction length, back up 2 instructions to provide context. |
| start_address = location.address() - 2 * arch_info.max_instr_len(); |
| options.max_instructions = 5; |
| } else { |
| // Variable length instructions. Since this code path is triggered when |
| // there are no symbols, we can't back up reliably. Just disassemble |
| // starting from the current location. |
| // |
| // In the future it might be nice to keep some record of recently stepped |
| // instructions since usually this address will be the one after the one |
| // that was just stepped. |
| start_address = location.address(); |
| options.max_instructions = 4; |
| } |
| |
| size_t size = options.max_instructions * arch_info.max_instr_len(); |
| |
| process->ReadMemory( |
| start_address, size, |
| [options, weak_process = process->GetWeakPtr(), file_provider = std::move(file_provider)]( |
| const Err& in_err, MemoryDump dump) { |
| if (!weak_process) |
| return; // Give up when the process went away. |
| |
| Console* console = Console::get(); |
| if (in_err.has_error()) { |
| console->Output(in_err); |
| return; |
| } |
| OutputBuffer out; |
| Err err = FormatAsmContext(weak_process->session()->arch_info(), dump, options, |
| weak_process.get(), *file_provider, &out); |
| if (err.has_error()) |
| console->Output(err); |
| else |
| console->Output(out); |
| }); |
| } |
| return Err(); |
| } |
| |
| // This doesn't cache the file contents. We may want to add that for performance, but we should be |
| // careful to always pick the latest version since it can get updated. |
| Err FormatSourceFileContext(const FileLine& file_line, const SourceFileProvider& file_provider, |
| const FormatSourceOpts& opts, OutputBuffer* out) { |
| auto data_or = file_provider.GetFileData(file_line.file(), file_line.comp_dir()); |
| if (data_or.has_error()) |
| return data_or.err(); |
| |
| // Check modification times for warning about out-of-date builds. |
| if (opts.module_for_time_warning) { |
| // Either of the times can be 0 if there was an error. Ignore the check in that case. |
| std::time_t module_time = opts.module_for_time_warning->GetModificationTime(); |
| std::time_t file_time = data_or.value().modification_time; |
| if (module_time && file_time && file_time > module_time) { |
| // File is known out-of-date. Only show warning once for each file per module. |
| if (opts.module_for_time_warning->newer_files_warned().insert(file_line.file()).second) { |
| out->Append(Syntax::kWarning, GetExclamation() + " Warning:"); |
| out->Append(" Source file is newer than the binary. The build may be out-of-date.\n"); |
| } |
| } |
| } |
| |
| return FormatSourceContext(data_or.value().full_path, data_or.value().contents, opts, out); |
| } |
| |
| Err FormatSourceContext(const std::string& file_name_for_display, const std::string& file_contents, |
| const FormatSourceOpts& opts, OutputBuffer* out) { |
| FX_DCHECK(opts.active_line == 0 || !opts.require_active_line || |
| (opts.active_line >= opts.first_line && opts.active_line <= opts.last_line)); |
| |
| // Allow the beginning to be out-of-range. This mirrors the end handling |
| // (clamped to end-of-file) so callers can blindly create offsets from |
| // a current line without clamping. |
| int first_line = std::max(1, opts.first_line); |
| |
| std::vector<std::string> context = ExtractSourceLines(file_contents, first_line, opts.last_line); |
| if (context.empty()) { |
| // No source found for this location. If highlight_line exists, assume |
| // it's the one the user cares about. |
| int err_line = opts.highlight_line ? opts.highlight_line : first_line; |
| return Err(fxl::StringPrintf("There is no line %d in the file %s", err_line, |
| file_name_for_display.c_str())); |
| } |
| if (opts.active_line != 0 && opts.require_active_line && |
| first_line + static_cast<int>(context.size()) < opts.active_line) { |
| return Err(fxl::StringPrintf("There is no line %d in the file %s", opts.active_line, |
| file_name_for_display.c_str())); |
| } |
| |
| // Optional file name. |
| if (opts.show_file_name) { |
| out->Append("📄 "); |
| out->Append(Syntax::kFileName, file_name_for_display); |
| out->Append("\n"); |
| } |
| |
| // String to put at the beginning of each line. |
| std::string indent(opts.left_indent, ' '); |
| |
| std::vector<std::vector<OutputBuffer>> rows; |
| for (size_t i = 0; i < context.size(); i++) { |
| int line_number = first_line + i; |
| |
| rows.emplace_back(); |
| std::vector<OutputBuffer>& row = rows.back(); |
| |
| // Compute markers in the left margin. |
| OutputBuffer margin; |
| if (opts.left_indent) |
| margin.Append(indent); |
| |
| auto found_bp = opts.bp_lines.find(line_number); |
| if (found_bp != opts.bp_lines.end()) { |
| std::string breakpoint_marker = |
| found_bp->second ? GetBreakpointMarker() : GetDisabledBreakpointMarker(); |
| |
| if (line_number == opts.active_line) { |
| // Active + breakpoint. |
| margin.Append(Syntax::kError, breakpoint_marker); |
| margin.Append(Syntax::kHeading, GetCurrentRowMarker()); |
| } else { |
| // Breakpoint. |
| margin.Append(Syntax::kError, " " + breakpoint_marker); |
| } |
| } else { |
| if (line_number == opts.active_line) { |
| // Active line. |
| margin.Append(Syntax::kHeading, " " + GetCurrentRowMarker()); |
| } else { |
| // Inactive line with no breakpoint. |
| margin.Append(" "); |
| } |
| } |
| row.push_back(std::move(margin)); |
| |
| std::string number = std::to_string(line_number); |
| if (line_number == opts.highlight_line) { |
| // This is the line to mark. |
| row.emplace_back(Syntax::kHeading, std::move(number)); |
| row.push_back(FormatSourceLine(opts, true, context[i])); |
| } else { |
| // Normal context line. |
| Syntax syntax = opts.dim_others ? Syntax::kComment : Syntax::kNormal; |
| row.emplace_back(syntax, std::move(number)); |
| row.push_back(FormatSourceLine(opts, false, context[i])); |
| } |
| } |
| |
| FormatTable( |
| {ColSpec(Align::kLeft), ColSpec(Align::kRight), ColSpec(Align::kLeft, 0, std::string(), 0)}, |
| rows, out); |
| return Err(); |
| } |
| |
| Err FormatAsmContext(const ArchInfo& arch_info, const MemoryDump& dump, const FormatAsmOpts& opts, |
| Process* process, const SourceFileProvider& file_provider, OutputBuffer* out) { |
| // Make the disassembler. |
| Disassembler disassembler; |
| Err my_err = disassembler.Init(&arch_info); |
| if (my_err.has_error()) |
| return my_err; |
| |
| Disassembler::Options options; |
| |
| std::vector<Disassembler::Row> rows; |
| disassembler.DisassembleDump(dump, dump.address(), options, opts.max_instructions, &rows); |
| |
| FileLine prev_file_line; // Last source line printed. |
| |
| std::vector<std::vector<OutputBuffer>> table; |
| for (auto& row : rows) { |
| if (opts.include_source) { |
| // Output source code if necessary. |
| std::vector<Location> loc = |
| process->GetSymbols()->ResolveInputLocation(InputLocation(row.address)); |
| if (!loc.empty() && loc[0].file_line().is_valid() && prev_file_line != loc[0].file_line()) { |
| std::vector<OutputBuffer>& out_row = table.emplace_back(); |
| out_row.push_back( |
| FormatAsmSourceForRange(process, GetWeakModuleForLocation(process, loc[0]), |
| file_provider, prev_file_line, loc[0].file_line())); |
| |
| prev_file_line = loc[0].file_line(); |
| } |
| } |
| |
| std::vector<OutputBuffer>& out_row = table.emplace_back(); |
| |
| // Compute markers in the left margin. |
| OutputBuffer margin; |
| auto found_bp = opts.bp_addrs.find(row.address); |
| if (found_bp != opts.bp_addrs.end()) { |
| std::string breakpoint_marker = |
| found_bp->second ? GetBreakpointMarker() : GetDisabledBreakpointMarker(); |
| |
| if (row.address == opts.active_address) { |
| // Active + breakpoint. |
| margin.Append(Syntax::kError, breakpoint_marker); |
| margin.Append(Syntax::kHeading, GetCurrentRowMarker()); |
| } else { |
| // Breakpoint. |
| margin.Append(Syntax::kError, " " + breakpoint_marker); |
| } |
| } else { |
| if (row.address == opts.active_address) { |
| // Active line. |
| margin.Append(Syntax::kHeading, " " + GetCurrentRowMarker()); |
| } else { |
| // Inactive line with no breakpoint. |
| margin.Append(" "); |
| } |
| } |
| out_row.push_back(std::move(margin)); |
| |
| if (opts.emit_addresses) |
| out_row.emplace_back(Syntax::kComment, to_hex_string(row.address)); |
| if (opts.emit_bytes) { |
| std::string bytes_str; |
| for (size_t i = 0; i < row.bytes.size(); i++) { |
| if (i > 0) |
| bytes_str.push_back(' '); |
| bytes_str.append(fxl::StringPrintf("%2.2x", row.bytes[i])); |
| } |
| out_row.emplace_back(Syntax::kComment, std::move(bytes_str)); |
| } |
| |
| Syntax op_param_syntax = |
| row.address == opts.active_address ? Syntax::kHeading : Syntax::kNormal; |
| out_row.emplace_back(op_param_syntax, std::move(row.op)); |
| out_row.emplace_back(op_param_syntax, std::move(row.params)); |
| |
| // If there's a call destination, include that. Otherwise use the disassembler-generated comment |
| // if present. |
| if (row.call_dest) { |
| out_row.push_back(DescribeAsmCallDest(process, *row.call_dest)); |
| } else { |
| out_row.emplace_back(Syntax::kComment, std::move(row.comment)); |
| } |
| } |
| |
| std::vector<ColSpec> spec; |
| spec.emplace_back(Align::kLeft); // Margin. |
| if (opts.emit_addresses) |
| spec.emplace_back(Align::kRight); |
| if (opts.emit_bytes) { |
| // Max out the bytes @ 17 cols (holds 6 bytes) to keep it from pushing |
| // things too far over in the common case. |
| spec.emplace_back(Align::kLeft, 17, std::string(), 1); |
| } |
| |
| // When there was an address or byte listing, put 1 extra column of space |
| // to separate the opcode. Otherwise keep it by the left margin. |
| if (spec.size() > 1) |
| spec.emplace_back(Align::kLeft, 0, std::string(), 1); // Instructions. |
| else |
| spec.emplace_back(Align::kLeft, 0, std::string(), 0); // Instructions. |
| |
| // Params. Some can be very long so provide a max so the comments don't get |
| // pushed too far out. |
| spec.emplace_back(Align::kLeft, 10, std::string(), 1); |
| spec.emplace_back(Align::kLeft); // Comments. |
| |
| FormatTable(spec, table, out); |
| return Err(); |
| } |
| |
| Err FormatBreakpointContext(const Location& location, const SourceFileProvider& file_provider, |
| bool enabled, OutputBuffer* out) { |
| if (!location.has_symbols()) |
| return Err("No symbols for this location."); |
| |
| int line = location.file_line().line(); |
| constexpr int kBreakpointContext = 1; |
| |
| FormatSourceOpts opts; |
| opts.SetLanguageFromFileName(location.file_line().file()); |
| opts.first_line = line - kBreakpointContext; |
| opts.last_line = line + kBreakpointContext; |
| opts.highlight_line = line; |
| opts.bp_lines[line] = enabled; |
| return FormatSourceFileContext(location.file_line(), file_provider, opts, out); |
| } |
| |
| OutputBuffer FormatSourceLine(const FormatSourceOpts& opts, bool is_highlight_line, |
| const std::string& line) { |
| if (opts.language) { |
| ExprTokenizer tokenizer(line, *opts.language); |
| if (tokenizer.Tokenize() && !tokenizer.tokens().empty()) |
| return FormatSourceLineWithTokens(opts, is_highlight_line, line, tokenizer.tokens()); |
| } |
| return FormatSourceLineNoSyntax(opts, is_highlight_line, line); |
| } |
| |
| } // namespace zxdb |