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
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