react-native-macos/React/Modules/RCTRedBox.m

959 строки
40 KiB
Objective-C

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTRedBox.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTDefines.h"
#import "RCTErrorInfo.h"
#import "RCTEventDispatcher.h"
#import "RCTJSStackFrame.h"
#import "RCTRedBoxExtraDataViewController.h"
#import "RCTUtils.h"
#if RCT_DEBUG
@class RCTRedBoxWindow;
@protocol RCTRedBoxWindowActionDelegate <NSObject>
- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;
- (void)loadExtraDataViewController;
@end
#if !TARGET_OS_OSX // TODO(macOS ISS#2323203)
@interface RCTRedBoxWindow : NSObject <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UIViewController *rootViewController;
@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
@end
@implementation RCTRedBoxWindow
{
UITableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray<RCTJSStackFrame *> *_lastStackTrace;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super init])) {
_rootViewController = [UIViewController new];
UIView *rootView = _rootViewController.view;
rootView.frame = frame;
rootView.backgroundColor = [UIColor blackColor];
const CGFloat buttonHeight = 60;
CGRect detailsFrame = rootView.bounds;
detailsFrame.size.height -= buttonHeight + [self bottomSafeViewHeight];
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_stackTraceTableView.delegate = self;
_stackTraceTableView.dataSource = self;
_stackTraceTableView.backgroundColor = [UIColor clearColor];
#if !TARGET_OS_TV
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
#endif
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[rootView addSubview:_stackTraceTableView];
#if TARGET_OS_SIMULATOR
NSString *reloadText = @"Reload\n(\u2318R)";
NSString *dismissText = @"Dismiss\n(ESC)";
NSString *copyText = @"Copy\n(\u2325\u2318C)";
NSString *extraText = @"Extra Info\n(\u2318E)";
#else
NSString *reloadText = @"Reload JS";
NSString *dismissText = @"Dismiss";
NSString *copyText = @"Copy";
NSString *extraText = @"Extra Info";
#endif
UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom];
dismissButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
dismissButton.accessibilityIdentifier = @"redbox-dismiss";
dismissButton.titleLabel.font = [UIFont systemFontOfSize:13];
dismissButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
dismissButton.titleLabel.textAlignment = NSTextAlignmentCenter;
dismissButton.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
[dismissButton setTitle:dismissText forState:UIControlStateNormal];
[dismissButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[dismissButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted];
[dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside];
UIButton *reloadButton = [UIButton buttonWithType:UIButtonTypeCustom];
reloadButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
reloadButton.accessibilityIdentifier = @"redbox-reload";
reloadButton.titleLabel.font = [UIFont systemFontOfSize:13];
reloadButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
reloadButton.titleLabel.textAlignment = NSTextAlignmentCenter;
reloadButton.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
[reloadButton setTitle:reloadText forState:UIControlStateNormal];
[reloadButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[reloadButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted];
[reloadButton addTarget:self action:@selector(reload) forControlEvents:UIControlEventTouchUpInside];
UIButton *copyButton = [UIButton buttonWithType:UIButtonTypeCustom];
copyButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
copyButton.accessibilityIdentifier = @"redbox-copy";
copyButton.titleLabel.font = [UIFont systemFontOfSize:13];
copyButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
copyButton.titleLabel.textAlignment = NSTextAlignmentCenter;
copyButton.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
[copyButton setTitle:copyText forState:UIControlStateNormal];
[copyButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[copyButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted];
[copyButton addTarget:self action:@selector(copyStack) forControlEvents:UIControlEventTouchUpInside];
UIButton *extraButton = [UIButton buttonWithType:UIButtonTypeCustom];
extraButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
extraButton.accessibilityIdentifier = @"redbox-extra";
extraButton.titleLabel.font = [UIFont systemFontOfSize:13];
extraButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
extraButton.titleLabel.textAlignment = NSTextAlignmentCenter;
extraButton.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
[extraButton setTitle:extraText forState:UIControlStateNormal];
[extraButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[extraButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted];
[extraButton addTarget:self action:@selector(showExtraDataViewController) forControlEvents:UIControlEventTouchUpInside];
CGFloat buttonWidth = frame.size.width / 4;
CGFloat bottomButtonHeight = frame.size.height - buttonHeight - [self bottomSafeViewHeight];
dismissButton.frame = CGRectMake(0, bottomButtonHeight, buttonWidth, buttonHeight);
reloadButton.frame = CGRectMake(buttonWidth, bottomButtonHeight, buttonWidth, buttonHeight);
copyButton.frame = CGRectMake(buttonWidth * 2, bottomButtonHeight, buttonWidth, buttonHeight);
extraButton.frame = CGRectMake(buttonWidth * 3, bottomButtonHeight, buttonWidth, buttonHeight);
UIView *topBorder = [[UIView alloc] initWithFrame:CGRectMake(0, bottomButtonHeight + 1, rootView.frame.size.width, 1)];
topBorder.backgroundColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
[rootView addSubview:dismissButton];
[rootView addSubview:reloadButton];
[rootView addSubview:copyButton];
[rootView addSubview:extraButton];
[rootView addSubview:topBorder];
UIView *bottomSafeView = [UIView new];
bottomSafeView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
bottomSafeView.frame = CGRectMake(0, frame.size.height - [self bottomSafeViewHeight], frame.size.width, [self bottomSafeViewHeight]);
[rootView addSubview:bottomSafeView];
}
return self;
}
- (NSInteger)bottomSafeViewHeight
{
if (@available(iOS 11.0, *)) {
return RCTSharedApplication().delegate.window.safeAreaInsets.bottom;
} else {
return 0;
}
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (void)dealloc
{
_stackTraceTableView.dataSource = nil;
_stackTraceTableView.delegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSString *)stripAnsi:(NSString *)text
{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\x1b\\[[0-9;]*m" options:NSRegularExpressionCaseInsensitive error:&error];
return [regex stringByReplacingMatchesInString:text options:0 range:NSMakeRange(0, [text length]) withTemplate:@""];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate
{
// Remove ANSI color codes from the message
NSString *messageWithoutAnsi = [self stripAnsi:message];
// Show if this is a new message, or if we're updating the previous message
BOOL hidden = !self.rootViewController.isBeingPresented;
if ((hidden && !isUpdate) || (!hidden && isUpdate && [_lastErrorMessage isEqualToString:messageWithoutAnsi])) {
_lastStackTrace = stack;
// message is displayed using UILabel, which is unable to render text of
// unlimited length, so we truncate it
_lastErrorMessage = [messageWithoutAnsi substringToIndex:MIN((NSUInteger)10000, messageWithoutAnsi.length)];
[_stackTraceTableView reloadData];
if (hidden) {
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[RCTKeyWindow().rootViewController presentViewController:self.rootViewController
animated:YES
completion:nil];
}
}
}
- (void)dismiss
{
[self.rootViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)reload
{
[_actionDelegate reloadFromRedBoxWindow:self];
}
- (void)showExtraDataViewController
{
[_actionDelegate loadExtraDataViewController];
}
- (void)copyStack
{
NSMutableString *fullStackTrace;
if (_lastErrorMessage != nil) {
fullStackTrace = [_lastErrorMessage mutableCopy];
[fullStackTrace appendString:@"\n\n"];
}
else {
fullStackTrace = [NSMutableString string];
}
for (RCTJSStackFrame *stackFrame in _lastStackTrace) {
[fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]];
if (stackFrame.file) {
[fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]];
}
}
#if !TARGET_OS_TV
UIPasteboard *pb = [UIPasteboard generalPasteboard];
[pb setString:fullStackTrace];
#endif
}
- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
{
NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @"<unknown file>";
NSString *lineInfo = [NSString stringWithFormat:@"%@:%lld",
fileName,
(long long)stackFrame.lineNumber];
if (stackFrame.column != 0) {
lineInfo = [lineInfo stringByAppendingFormat:@":%lld", (long long)stackFrame.column];
}
return lineInfo;
}
#pragma mark - TableView
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView
{
return 2;
}
- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return section == 0 ? 1 : _lastStackTrace.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
return [self reuseCell:cell forErrorMessage:_lastErrorMessage];
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
NSUInteger index = indexPath.row;
RCTJSStackFrame *stackFrame = _lastStackTrace[index];
return [self reuseCell:cell forStackFrame:stackFrame];
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"];
cell.textLabel.accessibilityIdentifier = @"redbox-error";
cell.textLabel.textColor = [UIColor whiteColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:16];
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.textLabel.numberOfLines = 0;
cell.detailTextLabel.textColor = [UIColor whiteColor];
cell.backgroundColor = [UIColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
cell.textLabel.text = message;
return cell;
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
cell.textLabel.textColor = [UIColor whiteColor];
cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14];
cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping;
cell.textLabel.numberOfLines = 2;
cell.detailTextLabel.textColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11];
cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
cell.backgroundColor = [UIColor clearColor];
cell.selectedBackgroundView = [UIView new];
cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2];
}
cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)";
if (stackFrame.file) {
cell.detailTextLabel.text = [self formatFrameSource:stackFrame];
} else {
cell.detailTextLabel.text = @"";
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16],
NSParagraphStyleAttributeName: paragraphStyle};
CGRect boundingRect = [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
return ceil(boundingRect.size.height) + 40;
} else {
return 50;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 1) {
NSUInteger row = indexPath.row;
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
[_actionDelegate redBoxWindow:self openStackFrameInEditor:stackFrame];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - Key commands
- (NSArray<UIKeyCommand *> *)keyCommands
{
// NOTE: We could use RCTKeyCommands for this, but since
// we control this window, we can use the standard, non-hacky
// mechanism instead
return @[
// Dismiss red box
[UIKeyCommand keyCommandWithInput:UIKeyInputEscape
modifierFlags:0
action:@selector(dismiss)],
// Reload
[UIKeyCommand keyCommandWithInput:@"r"
modifierFlags:UIKeyModifierCommand
action:@selector(reload)],
// Copy = Cmd-Option C since Cmd-C in the simulator copies the pasteboard from
// the simulator to the desktop pasteboard.
[UIKeyCommand keyCommandWithInput:@"c"
modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate
action:@selector(copyStack)],
// Extra data
[UIKeyCommand keyCommandWithInput:@"e"
modifierFlags:UIKeyModifierCommand
action:@selector(showExtraDataViewController)]
];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end
#elif TARGET_OS_OSX // [TODO(macOS ISS#2323203)
@interface RCTRedBoxScrollView : NSScrollView
@end
@implementation RCTRedBoxScrollView
- (NSSize)intrinsicContentSize
{
NSView *documentView = self.documentView;
return documentView != nil ? documentView.intrinsicContentSize : super.intrinsicContentSize;
}
@end
@interface RCTRedBoxWindow : NSObject <NSTableViewDataSource, NSTableViewDelegate>
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate;
- (void)dismiss;
@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
@end
@implementation RCTRedBoxWindow
{
NSWindow *_window;
NSTableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray<RCTJSStackFrame *> *_lastStackTrace;
BOOL _visible;
}
- (instancetype)init
{
if ((self = [super init])) {
_window = [[NSWindow alloc] initWithContentRect:NSZeroRect styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:YES];
_window.backgroundColor = [NSColor colorWithRed:0.8 green:0 blue:0 alpha:1];
_window.animationBehavior = NSWindowAnimationBehaviorDocumentWindow;
NSScrollView *scrollView = [[RCTRedBoxScrollView alloc] initWithFrame:NSZeroRect];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = [NSColor clearColor];
scrollView.drawsBackground = NO;
scrollView.hasVerticalScroller = YES;
NSTableColumn *tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"info"];
tableColumn.editable = false;
tableColumn.resizingMask = NSTableColumnAutoresizingMask;
_stackTraceTableView = [[NSTableView alloc] initWithFrame:NSZeroRect];
_stackTraceTableView.dataSource = self;
_stackTraceTableView.delegate = self;
_stackTraceTableView.headerView = nil;
_stackTraceTableView.allowsColumnReordering = NO;
_stackTraceTableView.allowsColumnResizing = NO;
_stackTraceTableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle;
_stackTraceTableView.backgroundColor = [NSColor clearColor];
_stackTraceTableView.allowsTypeSelect = NO;
[_stackTraceTableView addTableColumn:tableColumn];
scrollView.documentView = _stackTraceTableView;
NSButton *dismissButton = [[NSButton alloc] initWithFrame:NSZeroRect];
dismissButton.accessibilityIdentifier = @"redbox-dismiss";
dismissButton.translatesAutoresizingMaskIntoConstraints = NO;
dismissButton.target = self;
dismissButton.action = @selector(dismiss:);
[dismissButton setButtonType:NSButtonTypeMomentaryPushIn];
dismissButton.bezelStyle = NSBezelStyleRounded;
dismissButton.title = @"Dismiss (Esc)";
dismissButton.keyEquivalent = @"\e";
[dismissButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
NSButton *reloadButton = [[NSButton alloc] initWithFrame:NSZeroRect];
reloadButton.accessibilityIdentifier = @"redbox-reload";
reloadButton.translatesAutoresizingMaskIntoConstraints = NO;
reloadButton.target = self;
reloadButton.action = @selector(reload:);
reloadButton.bezelStyle = NSBezelStyleRounded;
reloadButton.title = @"Reload JS (\u2318R)";
[reloadButton setButtonType:NSButtonTypeMomentaryPushIn];
reloadButton.keyEquivalent = @"r";
reloadButton.keyEquivalentModifierMask = NSEventModifierFlagCommand;
[reloadButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
[reloadButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical];
NSButton *copyButton = [[NSButton alloc] initWithFrame:NSZeroRect];
copyButton.accessibilityIdentifier = @"redbox-copy";
copyButton.translatesAutoresizingMaskIntoConstraints = NO;
copyButton.target = self;
copyButton.action = @selector(copyStack:);
copyButton.title = @"Copy (\u2325\u2318C)";
copyButton.bezelStyle = NSBezelStyleRounded;
[copyButton setButtonType:NSButtonTypeMomentaryPushIn];
copyButton.keyEquivalent = @"c";
copyButton.keyEquivalentModifierMask = NSEventModifierFlagOption | NSEventModifierFlagCommand;
[copyButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
NSView *contentView = _window.contentView;
[contentView addSubview:scrollView];
[contentView addSubview:dismissButton];
[contentView addSubview:reloadButton];
[contentView addSubview:copyButton];
[NSLayoutConstraint activateConstraints:@[
// the window shouldn't be any bigger than 375x643 points
[NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:375],
[NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationLessThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:643],
// scroll view hugs the left, top, and right sides of the window, and the buttons at the bottom
[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeLeading multiplier:1 constant:16],
[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeTop multiplier:1 constant:16],
[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeTrailing multiplier:1 constant:-16],
[NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeTop multiplier:1 constant:-8],
// buttons have equal widths
[NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeWidth multiplier:1 constant:0],
[NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:copyButton attribute:NSLayoutAttributeWidth multiplier:1 constant:0],
// buttons are centered horizontally in the window
[NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:contentView attribute:NSLayoutAttributeLeading multiplier:1 constant:16],
[NSLayoutConstraint constraintWithItem:copyButton attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationLessThanOrEqual toItem:contentView attribute:NSLayoutAttributeTrailing multiplier:1 constant:-16],
[NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeLeading multiplier:1 constant:-8],
[NSLayoutConstraint constraintWithItem:reloadButton attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0],
[NSLayoutConstraint constraintWithItem:copyButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeTrailing multiplier:1 constant:8],
// buttons are baseline aligned
[NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeBaseline relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeBaseline multiplier:1 constant:0],
[NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeBaseline relatedBy:NSLayoutRelationEqual toItem:copyButton attribute:NSLayoutAttributeBaseline multiplier:1 constant:0],
// buttons appear at the bottom of the window
[NSLayoutConstraint constraintWithItem:reloadButton attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeBottom multiplier:1 constant:-16],
]];
}
return self;
}
- (void)dealloc
{
// VSO#1878643: On macOS the RedBox can be dealloc'd on the JS thread causing the Main Thread Checker to throw when the NSTableView properties below are accessed.
NSTableView *stackTraceTableView = _stackTraceTableView;
RCTUnsafeExecuteOnMainQueueSync(^{
stackTraceTableView.dataSource = nil;
stackTraceTableView.delegate = nil;
});
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate
{
// Show if this is a new message, or if we're updating the previous message
if ((!_visible && !isUpdate) || (_visible && isUpdate && [_lastErrorMessage isEqualToString:message])) {
_lastStackTrace = stack;
// message is displayed using UILabel, which is unable to render text of
// unlimited length, so we truncate it
_lastErrorMessage = [message substringToIndex:MIN((NSUInteger)10000, message.length)];
[_window layoutIfNeeded]; // layout the window for the correct width
[_stackTraceTableView reloadData]; // load the new data
[_stackTraceTableView.enclosingScrollView invalidateIntrinsicContentSize]; // the height of the scroll view changed with the new data
[_window layoutIfNeeded]; // layout the window for the correct height
if (!_visible) {
_visible = YES;
[_window center];
if (!RCTRunningInTestEnvironment()) {
[NSApp runModalForWindow:_window];
}
else {
[NSApp activateIgnoringOtherApps:YES];
[[NSApp mainWindow] makeKeyAndOrderFront:_window];
}
}
}
}
- (void)dismiss
{
if (_visible) {
[NSApp stopModal];
[_window orderOut:self];
_visible = NO;
}
}
- (IBAction)dismiss:(__unused NSButton *)sender
{
[self dismiss];
}
- (IBAction)reload:(__unused NSButton *)sender
{
[_actionDelegate reloadFromRedBoxWindow:self];
}
- (IBAction)copyStack:(__unused NSButton *)sender
{
// TODO: This is copy/paste from the iOS implementation
NSMutableString *fullStackTrace;
if (_lastErrorMessage != nil) {
fullStackTrace = [_lastErrorMessage mutableCopy];
[fullStackTrace appendString:@"\n\n"];
}
else {
fullStackTrace = [NSMutableString string];
}
for (RCTJSStackFrame *stackFrame in _lastStackTrace) {
[fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]];
if (stackFrame.file) {
[fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]];
}
}
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
[pasteboard setString:fullStackTrace forType:NSPasteboardTypeString];
}
- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
{
// TODO: This is copy/paste from the iOS implementation
NSString *lineInfo = [NSString stringWithFormat:@"%@:%zd",
[stackFrame.file lastPathComponent],
stackFrame.lineNumber];
if (stackFrame.column != 0) {
lineInfo = [lineInfo stringByAppendingFormat:@":%zd", stackFrame.column];
}
return lineInfo;
}
#pragma mark - TableView
- (NSInteger)numberOfRowsInTableView:(__unused NSTableView *)tableView
{
return (_lastErrorMessage != nil) + _lastStackTrace.count;
}
- (BOOL)tableView:(__unused NSTableView *)tableView shouldSelectRow:(__unused NSInteger)row
{
return NO;
}
- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row
{
NSTableCellView *view = [tableView makeViewWithIdentifier:tableColumn.identifier owner:nil];
if (view == nil) {
view = [[NSTableCellView alloc] initWithFrame:NSZeroRect];
view.identifier = tableColumn.identifier;
NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect];
label.translatesAutoresizingMaskIntoConstraints = NO;
label.backgroundColor = [NSColor clearColor];
label.drawsBackground = NO;
label.bezeled = NO;
label.editable = NO;
[label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
[label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical];
[view addSubview:label];
view.textField = label;
[NSLayoutConstraint activateConstraints:@[
[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeLeading multiplier:1 constant:0],
[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeTop multiplier:1 constant:0],
[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeTrailing multiplier:1 constant:0],
[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeBottom multiplier:1 constant:0],
]];
}
view.textField.attributedStringValue = [self attributedStringForRow:row];
return view;
}
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
NSAttributedString *attributedString = [self attributedStringForRow:row];
NSRect boundingRect = [attributedString boundingRectWithSize:NSMakeSize(tableView.frame.size.width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin];
CGFloat height = ceilf(NSMaxY(boundingRect));
if (row == 0) {
height += 32;
}
return height;
}
- (NSAttributedString *)attributedStringForRow:(NSUInteger)row
{
if (_lastErrorMessage != nil) {
if (row == 0) {
NSDictionary<NSString *, id> *attributes = @{
NSForegroundColorAttributeName : [NSColor whiteColor],
NSFontAttributeName : [NSFont systemFontOfSize:16],
};
return [[NSAttributedString alloc] initWithString:_lastErrorMessage attributes:attributes];
}
--row;
}
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
NSMutableParagraphStyle *titleParagraphStyle = [NSMutableParagraphStyle new];
titleParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
NSDictionary<NSString *, id> *titleAttributes = @{
NSForegroundColorAttributeName : [NSColor colorWithWhite:1 alpha:0.9],
NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:14],
NSParagraphStyleAttributeName : titleParagraphStyle,
};
NSString *rawTitle = stackFrame.methodName ?: @"(unnamed method)";
NSAttributedString *title = [[NSAttributedString alloc] initWithString:rawTitle attributes:titleAttributes];
if (stackFrame.file == nil) {
return title;
}
NSMutableParagraphStyle *frameParagraphStyle = [NSMutableParagraphStyle new];
frameParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
NSDictionary<NSString *, id> *frameAttributes = @{
NSForegroundColorAttributeName : [NSColor colorWithWhite:1 alpha:0.7],
NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:11],
NSParagraphStyleAttributeName : frameParagraphStyle,
};
NSMutableAttributedString *frameSource = [[NSMutableAttributedString alloc] initWithString:[self formatFrameSource:stackFrame] attributes:frameAttributes];
[frameSource replaceCharactersInRange:NSMakeRange(0, 0) withString:@"\n"];
[frameSource insertAttributedString:title atIndex:0];
return frameSource;
}
@end
#endif // ]TODO(macOS ISS#2323203)
@interface RCTRedBox () <RCTInvalidating, RCTRedBoxWindowActionDelegate, RCTRedBoxExtraDataActionDelegate>
@end
@implementation RCTRedBox
{
RCTRedBoxWindow *_window;
NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
RCTRedBoxExtraDataViewController *_extraDataViewController;
}
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!self->_errorCustomizers) {
self->_errorCustomizers = [NSMutableArray array];
}
if (![self->_errorCustomizers containsObject:errorCustomizer]) {
[self->_errorCustomizers addObject:errorCustomizer];
}
});
}
// WARNING: Should only be called from the main thread/dispatch queue.
- (RCTErrorInfo *)_customizeError:(RCTErrorInfo *)error
{
RCTAssertMainQueue();
if (!self->_errorCustomizers) {
return error;
}
for (id<RCTErrorCustomizer> customizer in self->_errorCustomizers) {
RCTErrorInfo *newInfo = [customizer customizeErrorInfo:error];
if (newInfo) {
error = newInfo;
}
}
return error;
}
- (void)showError:(NSError *)error
{
[self showErrorMessage:error.localizedDescription
withDetails:error.localizedFailureReason
stack:error.userInfo[RCTJSStackTraceKey]];
}
- (void)showErrorMessage:(NSString *)message
{
[self showErrorMessage:message withParsedStack:nil isUpdate:NO];
}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
{
[self showErrorMessage:message withDetails:details stack:nil];
}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details stack:(NSArray<RCTJSStackFrame *> *)stack {
NSString *combinedMessage = message;
if (details) {
combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details];
}
[self showErrorMessage:combinedMessage withParsedStack:stack isUpdate:NO];
}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack
{
NSArray<RCTJSStackFrame *> *stack = [RCTJSStackFrame stackFramesWithLines:rawStack];
[self showErrorMessage:message withParsedStack:stack isUpdate:NO];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:NO];
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:YES];
}
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
{
[self showErrorMessage:message withParsedStack:stack isUpdate:NO];
}
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
{
[self showErrorMessage:message withParsedStack:stack isUpdate:YES];
}
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate
{
dispatch_async(dispatch_get_main_queue(), ^{
#if !TARGET_OS_OSX // TODO(macOS ISS#2323203)
if (self->_extraDataViewController == nil) {
self->_extraDataViewController = [RCTRedBoxExtraDataViewController new];
self->_extraDataViewController.actionDelegate = self;
}
#endif // TODO(macOS ISS#2323203)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[self->_bridge.eventDispatcher sendDeviceEventWithName:@"collectRedBoxExtraData" body:nil];
#pragma clang diagnostic pop
if (!self->_window) {
#if !TARGET_OS_OSX // TODO(macOS ISS#2323203)
self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
#else // [TODO(macOS ISS#2323203)
self->_window = [RCTRedBoxWindow new];
#endif // ]TODO(macOS ISS#2323203)
self->_window.actionDelegate = self;
}
RCTErrorInfo *errorInfo = [[RCTErrorInfo alloc] initWithErrorMessage:message
stack:stack];
errorInfo = [self _customizeError:errorInfo];
[self->_window showErrorMessage:errorInfo.errorMessage
withStack:errorInfo.stack
isUpdate:isUpdate];
});
}
- (void)loadExtraDataViewController {
dispatch_async(dispatch_get_main_queue(), ^{
// Make sure the CMD+E shortcut doesn't call this twice
#if !TARGET_OS_OSX // TODO(macOS ISS#2323203)
if (self->_extraDataViewController != nil && ![self->_window.rootViewController presentedViewController]) {
[self->_window.rootViewController presentViewController:self->_extraDataViewController animated:YES completion:nil];
}
#else // [TODO(macOS ISS#2323203)
if (self->_extraDataViewController != nil && [NSApp modalWindow] == nil) {
[[[NSApp keyWindow] contentViewController] presentViewControllerAsModalWindow:self->_extraDataViewController];
}
#endif // ]TODO(macOS ISS#2323203)
});
}
RCT_EXPORT_METHOD(setExtraData:(NSDictionary *)extraData forIdentifier:(NSString *)identifier) {
[_extraDataViewController addExtraData:extraData forIdentifier:identifier];
}
RCT_EXPORT_METHOD(dismiss)
{
#if TARGET_OS_OSX // [TODO(macOS ISS#2323203)
[self->_window performSelectorOnMainThread:@selector(dismiss) withObject:nil waitUntilDone:NO];
#else // ]TODO(macOS ISS#2323203)
dispatch_async(dispatch_get_main_queue(), ^{
[self->_window dismiss];
self->_window = nil; // TODO(OSS Candidate ISS#2710739): release _window now to ensure its UIKit ivars are dealloc'd on the main thread as the RCTRedBox can be dealloc'd on a background thread.
});
#endif // TODO(macOS ISS#2323203)
}
- (void)invalidate
{
[self dismiss];
}
- (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
{
NSURL *const bundleURL = _overrideBundleURL ?: _bridge.bundleURL;
if (![bundleURL.scheme hasPrefix:@"http"]) {
RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager.");
return;
}
NSData *stackFrameJSON = [RCTJSONStringify([stackFrame toDictionary], NULL) dataUsingEncoding:NSUTF8StringEncoding];
NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length];
NSMutableURLRequest *request = [NSMutableURLRequest new];
request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:bundleURL];
request.HTTPMethod = @"POST";
request.HTTPBody = stackFrameJSON;
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];
}
- (void)reload
{
// Window is not used and can be nil
[self reloadFromRedBoxWindow:nil];
}
- (void)reloadFromRedBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow
{
if (_overrideReloadAction) {
_overrideReloadAction();
} else {
[_bridge reload];
}
[self dismiss];
}
@end
@implementation RCTBridge (RCTRedBox)
- (RCTRedBox *)redBox
{
return [self moduleForClass:[RCTRedBox class]];
}
@end
#else // Disabled
@implementation RCTRedBox
+ (NSString *)moduleName { return nil; }
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer {}
- (void)showError:(NSError *)message {}
- (void)showErrorMessage:(NSString *)message {}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack {}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack {}
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack {}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate {}
- (void)dismiss {}
@end
@implementation RCTBridge (RCTRedBox)
- (RCTRedBox *)redBox { return nil; }
@end
#endif