Skip to content

Commit fc11cfc

Browse files
motiz88meta-codesync[bot]
authored andcommitted
Show structured errors with syntax-highlighted code frames
Summary: Here, we give the RedBox 2.0 overlay structured error presentation matching LogBox's visual language. Before this diff, RedBox 2.0 showed a raw error string and a flat call stack. Now, the error message is parsed into a dynamic header title (e.g. "Syntax Error", "Render Error", "Uncaught Error"), a cleaned message body, and — for compilation errors — a "Source" section with ANSI syntax-highlighted code frames showing file name, line, and column. The call stack section is hidden when empty (e.g. for syntax errors that have no meaningful JS stack). Changelog: [Internal] Differential Revision: D98113191
1 parent 22f76d2 commit fc11cfc

5 files changed

Lines changed: 378 additions & 32 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
/**
11+
* Parses ANSI escape sequences in text and produces an NSAttributedString
12+
* with the corresponding foreground/background colors applied.
13+
*
14+
* Uses the Afterglow color theme (matching LogBox's AnsiHighlight.js).
15+
*/
16+
@interface RCTRedBox2AnsiParser : NSObject
17+
18+
+ (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text
19+
baseFont:(UIFont *)font
20+
baseColor:(UIColor *)color;
21+
22+
@end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTRedBox2AnsiParser+Internal.h"
9+
10+
#import <React/RCTDefines.h>
11+
#import <react/debug/redbox/AnsiParser.h>
12+
13+
#if RCT_DEV_MENU
14+
15+
using facebook::react::unstable_redbox::AnsiColor;
16+
using facebook::react::unstable_redbox::parseAnsi;
17+
18+
static UIColor *RCTUIColorFromAnsiColor(const AnsiColor &c)
19+
{
20+
return [UIColor colorWithRed:c.r / 255.0 green:c.g / 255.0 blue:c.b / 255.0 alpha:1.0];
21+
}
22+
23+
@implementation RCTRedBox2AnsiParser
24+
25+
+ (NSAttributedString *)attributedStringFromAnsiText:(NSString *)text baseFont:(UIFont *)font baseColor:(UIColor *)color
26+
{
27+
if (text == nil) {
28+
return [[NSAttributedString alloc] init];
29+
}
30+
31+
auto spans = parseAnsi(text.UTF8String);
32+
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
33+
NSDictionary *baseAttributes = @{NSFontAttributeName : font, NSForegroundColorAttributeName : color};
34+
35+
for (const auto &span : spans) {
36+
NSString *str = [NSString stringWithUTF8String:span.text.c_str()];
37+
if (str == nil) {
38+
continue;
39+
}
40+
NSMutableDictionary *attrs = [baseAttributes mutableCopy];
41+
if (span.foregroundColor.has_value()) {
42+
attrs[NSForegroundColorAttributeName] = RCTUIColorFromAnsiColor(*span.foregroundColor);
43+
}
44+
if (span.backgroundColor.has_value()) {
45+
attrs[NSBackgroundColorAttributeName] = RCTUIColorFromAnsiColor(*span.backgroundColor);
46+
}
47+
[result appendAttributedString:[[NSAttributedString alloc] initWithString:str attributes:attrs]];
48+
}
49+
50+
return result;
51+
}
52+
53+
@end
54+
55+
#endif

packages/react-native/React/CoreModules/RCTRedBox2Controller.mm

Lines changed: 205 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
#import <React/RCTReloadCommand.h>
1313
#import <React/RCTUtils.h>
1414

15+
#include <array>
16+
17+
#import "RCTRedBox2AnsiParser+Internal.h"
18+
#import "RCTRedBox2ErrorParser+Internal.h"
19+
20+
// @lint-ignore-every CLANGTIDY clang-diagnostic-switch-default
21+
// NOTE: clang-diagnostic-switch-default conflicts with clang-diagnostic-switch-enum
22+
1523
#if RCT_DEV_MENU
1624

1725
#pragma mark - RCTRedBox2Controller
@@ -32,6 +40,13 @@
3240
return [UIColor colorWithWhite:1.0 alpha:opacity];
3341
}
3442

43+
enum class Section : uint8_t { Message, CodeFrame, CallStack, kMaxValue };
44+
static constexpr size_t kSectionCount = static_cast<size_t>(Section::kMaxValue);
45+
46+
struct SectionState {
47+
bool visible = false;
48+
};
49+
3550
@implementation RCTRedBox2Controller {
3651
UITableView *_stackTraceTableView;
3752
UILabel *_headerTitleLabel;
@@ -41,6 +56,8 @@ @implementation RCTRedBox2Controller {
4156
NSArray<NSString *> *_customButtonTitles;
4257
NSArray<RCTRedBox2ButtonPressHandler> *_customButtonHandlers;
4358
int _lastErrorCookie;
59+
RCTRedBox2ErrorData *_errorData;
60+
std::array<SectionState, kSectionCount> _sectionStates;
4461
}
4562

4663
- (instancetype)initWithCustomButtonTitles:(NSArray<NSString *> *)customButtonTitles
@@ -255,6 +272,10 @@ - (void)showErrorMessage:(NSString *)message
255272
_lastErrorMessage = [messageWithoutAnsi substringToIndex:MIN((NSUInteger)10000, messageWithoutAnsi.length)];
256273
_lastErrorCookie = errorCookie;
257274

275+
// Parse the message to extract structure (title, code frame, etc.)
276+
_errorData = [RCTRedBox2ErrorParser parseErrorMessage:message name:nil componentStack:nil isFatal:YES];
277+
[self updateSectionVisibility];
278+
258279
[_stackTraceTableView reloadData];
259280

260281
if (!isRootViewControllerPresented) {
@@ -313,23 +334,77 @@ - (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
313334
return lineInfo;
314335
}
315336

337+
#pragma mark - Section Helpers
338+
339+
- (void)updateSectionVisibility
340+
{
341+
_sectionStates = {};
342+
_sectionStates[static_cast<size_t>(Section::Message)].visible = true;
343+
_sectionStates[static_cast<size_t>(Section::CodeFrame)].visible = _errorData.codeFrame.length > 0;
344+
_sectionStates[static_cast<size_t>(Section::CallStack)].visible =
345+
_lastStackTrace.count > 0 && _errorData.codeFrame.length == 0;
346+
}
347+
348+
- (NSInteger)visibleSectionCount
349+
{
350+
NSInteger count = 0;
351+
for (size_t i = 0; i < kSectionCount; i++) {
352+
if (_sectionStates[i].visible) {
353+
count++;
354+
}
355+
}
356+
return count;
357+
}
358+
359+
- (Section)sectionForIndex:(NSInteger)index
360+
{
361+
NSInteger visible = 0;
362+
for (size_t i = 0; i < kSectionCount; i++) {
363+
if (_sectionStates[i].visible) {
364+
if (visible == index) {
365+
return static_cast<Section>(i);
366+
}
367+
visible++;
368+
}
369+
}
370+
RCTAssert(NO, @"Invalid section index %ld", (long)index);
371+
return Section::kMaxValue;
372+
}
373+
374+
- (NSString *)displayMessage
375+
{
376+
return _errorData.message.length > 0 ? [self stripAnsi:_errorData.message] : _lastErrorMessage;
377+
}
378+
316379
#pragma mark - TableView DataSource & Delegate
317380

318381
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView
319382
{
320-
return _lastStackTrace.count > 0 ? 2 : 1;
383+
return [self visibleSectionCount];
321384
}
322385

323386
- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section
324387
{
325-
return section == 0 ? 1 : static_cast<NSInteger>(_lastStackTrace.count);
388+
if ([self sectionForIndex:section] == Section::CallStack) {
389+
return static_cast<NSInteger>(_lastStackTrace.count);
390+
}
391+
return 1;
326392
}
327393

328394
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
329395
{
330-
if (indexPath.section == 0) {
331-
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
332-
return [self reuseCell:cell forErrorMessage:_lastErrorMessage];
396+
switch ([self sectionForIndex:indexPath.section]) {
397+
case Section::Message: {
398+
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
399+
return [self reuseCell:cell forErrorMessage:[self displayMessage]];
400+
}
401+
case Section::CodeFrame: {
402+
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"code-cell"];
403+
return [self reuseCell:cell forCodeFrame:_errorData];
404+
}
405+
case Section::CallStack:
406+
case Section::kMaxValue:
407+
break;
333408
}
334409
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
335410
NSUInteger index = indexPath.row;
@@ -375,7 +450,7 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString
375450
]];
376451
}
377452

378-
_errorCategoryLabel.text = @"Error";
453+
_errorCategoryLabel.text = _errorData.title;
379454
UILabel *messageLabel = [cell.contentView viewWithTag:100];
380455
messageLabel.text = message;
381456

@@ -415,55 +490,153 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStack
415490
return cell;
416491
}
417492

418-
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
493+
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forCodeFrame:(RCTRedBox2ErrorData *)errorData
494+
{
495+
if (cell == nullptr) {
496+
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"code-cell"];
497+
cell.backgroundColor = [UIColor clearColor];
498+
cell.selectionStyle = UITableViewCellSelectionStyleNone;
499+
}
500+
501+
// Remove old subviews
502+
for (UIView *subview in cell.contentView.subviews) {
503+
[subview removeFromSuperview];
504+
}
505+
506+
// Code frame container with rounded corners
507+
UIView *container = [[UIView alloc] init];
508+
container.translatesAutoresizingMaskIntoConstraints = NO;
509+
container.backgroundColor = RCTRedBox2BackgroundColor();
510+
container.layer.cornerRadius = 3;
511+
container.clipsToBounds = YES;
512+
[cell.contentView addSubview:container];
513+
514+
// Render code frame with ANSI syntax highlighting
515+
UIFont *codeFont = [UIFont fontWithName:@"Menlo-Regular" size:12];
516+
NSAttributedString *highlighted = [RCTRedBox2AnsiParser attributedStringFromAnsiText:errorData.codeFrame
517+
baseFont:codeFont
518+
baseColor:[UIColor whiteColor]];
519+
520+
UILabel *codeLabel = [[UILabel alloc] init];
521+
codeLabel.translatesAutoresizingMaskIntoConstraints = NO;
522+
codeLabel.attributedText = highlighted;
523+
codeLabel.numberOfLines = 0;
524+
codeLabel.lineBreakMode = NSLineBreakByClipping;
525+
526+
UIScrollView *codeScrollView = [[UIScrollView alloc] init];
527+
codeScrollView.translatesAutoresizingMaskIntoConstraints = NO;
528+
codeScrollView.showsHorizontalScrollIndicator = YES;
529+
codeScrollView.showsVerticalScrollIndicator = NO;
530+
codeScrollView.bounces = NO;
531+
[codeScrollView addSubview:codeLabel];
532+
[container addSubview:codeScrollView];
533+
534+
// File name label below the code frame
535+
UILabel *fileLabel = [[UILabel alloc] init];
536+
fileLabel.translatesAutoresizingMaskIntoConstraints = NO;
537+
NSString *fileName = errorData.codeFrameFileName.lastPathComponent ? errorData.codeFrameFileName.lastPathComponent
538+
: errorData.codeFrameFileName;
539+
if (errorData.codeFrameRow > 0) {
540+
fileLabel.text = [NSString
541+
stringWithFormat:@"%@ (%ld:%ld)", fileName, (long)errorData.codeFrameRow, (long)errorData.codeFrameColumn + 1];
542+
} else if (fileName.length > 0) {
543+
fileLabel.text = fileName;
544+
}
545+
fileLabel.textColor = RCTRedBox2TextColor(0.5);
546+
fileLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:12];
547+
fileLabel.textAlignment = NSTextAlignmentCenter;
548+
[cell.contentView addSubview:fileLabel];
549+
550+
[NSLayoutConstraint activateConstraints:@[
551+
[container.topAnchor constraintEqualToAnchor:cell.contentView.topAnchor constant:5],
552+
[container.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:10],
553+
[container.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-10],
554+
555+
[codeScrollView.topAnchor constraintEqualToAnchor:container.topAnchor constant:10],
556+
[codeScrollView.leadingAnchor constraintEqualToAnchor:container.leadingAnchor constant:10],
557+
[codeScrollView.trailingAnchor constraintEqualToAnchor:container.trailingAnchor constant:-10],
558+
[codeScrollView.bottomAnchor constraintEqualToAnchor:container.bottomAnchor constant:-10],
559+
560+
[codeLabel.topAnchor constraintEqualToAnchor:codeScrollView.topAnchor],
561+
[codeLabel.leadingAnchor constraintEqualToAnchor:codeScrollView.leadingAnchor],
562+
[codeLabel.trailingAnchor constraintEqualToAnchor:codeScrollView.trailingAnchor],
563+
[codeLabel.bottomAnchor constraintEqualToAnchor:codeScrollView.bottomAnchor],
564+
[codeLabel.heightAnchor constraintEqualToAnchor:codeScrollView.heightAnchor],
565+
566+
[fileLabel.topAnchor constraintEqualToAnchor:container.bottomAnchor constant:10],
567+
[fileLabel.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:10],
568+
[fileLabel.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-10],
569+
[fileLabel.bottomAnchor constraintEqualToAnchor:cell.contentView.bottomAnchor constant:-10],
570+
]];
571+
572+
return cell;
573+
}
574+
575+
- (CGFloat)tableView:(__unused UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
419576
{
420-
if (indexPath.section == 0) {
577+
auto section = [self sectionForIndex:indexPath.section];
578+
if (section == Section::Message || section == Section::CodeFrame) {
421579
return UITableViewAutomaticDimension;
422-
} else {
423-
return 50;
424580
}
581+
return 50;
425582
}
426583

427584
- (CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
428585
{
429-
if (indexPath.section == 0) {
430-
return 100;
586+
switch ([self sectionForIndex:indexPath.section]) {
587+
case Section::Message:
588+
return 100;
589+
case Section::CodeFrame:
590+
return 200;
591+
case Section::CallStack:
592+
case Section::kMaxValue:
593+
return 50;
431594
}
432-
return 50;
433595
}
434596

435-
- (UIView *)tableView:(__unused UITableView *)tableView viewForHeaderInSection:(NSInteger)section
597+
- (UIView *)sectionHeaderViewWithTitle:(NSString *)title
436598
{
437-
if (section == 1) {
438-
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 38)];
439-
headerView.backgroundColor = [UIColor clearColor];
599+
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 38)];
600+
headerView.backgroundColor = [UIColor clearColor];
440601

441-
UILabel *label = [[UILabel alloc] init];
442-
label.translatesAutoresizingMaskIntoConstraints = NO;
443-
label.text = @"Call Stack";
444-
label.textColor = [UIColor whiteColor];
445-
label.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
446-
[headerView addSubview:label];
602+
UILabel *label = [[UILabel alloc] init];
603+
label.translatesAutoresizingMaskIntoConstraints = NO;
604+
label.text = title;
605+
label.textColor = [UIColor whiteColor];
606+
label.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
607+
[headerView addSubview:label];
447608

448-
[NSLayoutConstraint activateConstraints:@[
449-
[label.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor constant:12],
450-
[label.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor constant:-12],
451-
[label.bottomAnchor constraintEqualToAnchor:headerView.bottomAnchor constant:-10],
452-
]];
609+
[NSLayoutConstraint activateConstraints:@[
610+
[label.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor constant:12],
611+
[label.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor constant:-12],
612+
[label.bottomAnchor constraintEqualToAnchor:headerView.bottomAnchor constant:-10],
613+
]];
614+
615+
return headerView;
616+
}
453617

454-
return headerView;
618+
- (UIView *)tableView:(__unused UITableView *)tableView viewForHeaderInSection:(NSInteger)section
619+
{
620+
switch ([self sectionForIndex:section]) {
621+
case Section::CodeFrame:
622+
return [self sectionHeaderViewWithTitle:@"Source"];
623+
case Section::CallStack:
624+
return [self sectionHeaderViewWithTitle:@"Call Stack"];
625+
case Section::Message:
626+
case Section::kMaxValue:
627+
return nil;
455628
}
456-
return nil;
457629
}
458630

459631
- (CGFloat)tableView:(__unused UITableView *)tableView heightForHeaderInSection:(NSInteger)section
460632
{
461-
return section == 1 ? 38 : 0;
633+
auto s = [self sectionForIndex:section];
634+
return (s == Section::CodeFrame || s == Section::CallStack) ? 38 : 0;
462635
}
463636

464637
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
465638
{
466-
if (indexPath.section == 1) {
639+
if ([self sectionForIndex:indexPath.section] == Section::CallStack) {
467640
NSUInteger row = indexPath.row;
468641
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
469642
[_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame];

0 commit comments

Comments
 (0)