mirror of
https://github.com/status-im/react-native.git
synced 2025-01-17 04:50:59 +00:00
5047f6f54c
Summary: Added a button to the iOS Red Box to enable copying of the error and stack trace to the clipboard to make it easier to paste the error and stack into bug reports or other text windows. Clicking the Copy button on the bottom of the RedBox screen or pressing Cmd-Option-C keyboard shortcut in the simulator will copy the full stack trace as text to the iOS pasteboard and when in the simulator you can then hit Command-C to get the text to the Mac's pasteboard for pasting in your favorite reporting/tracking app. No tests for this change - I don't see any tests to extend for the RCTRedBoxWindow class. No impact on APIs or core React Native functionality. I validated it by running test React Native apps with red box errors and both hitting the copy button or using the command key shortcut in the emulator to get the text of the stack to the iOS pasteboard. ![redboxcopybutton](https://cloud.githubusercontent.com/assets/7173455/15258992/7b1e849c-1903-11e6-8813-6e853db5db54.png) Closes https://github.com/facebook/react-native/pull/7557 Differential Revision: D3311743 Pulled By: javache fbshipit-source-id: 76d1ac8ab93f40696c6a2369dae2245194db095a
425 lines
15 KiB
Objective-C
425 lines
15 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "RCTRedBox.h"
|
|
|
|
#import "RCTBridge.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTDefines.h"
|
|
#import "RCTUtils.h"
|
|
|
|
#if RCT_DEBUG
|
|
|
|
@class RCTRedBoxWindow;
|
|
|
|
@protocol RCTRedBoxWindowActionDelegate <NSObject>
|
|
|
|
- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(NSDictionary *)stackFrame;
|
|
- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;
|
|
|
|
@end
|
|
|
|
@interface RCTRedBoxWindow : UIWindow <UITableViewDelegate, UITableViewDataSource>
|
|
@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
|
|
@end
|
|
|
|
@implementation RCTRedBoxWindow
|
|
{
|
|
UITableView *_stackTraceTableView;
|
|
NSString *_lastErrorMessage;
|
|
NSArray<NSDictionary *> *_lastStackTrace;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
{
|
|
if ((self = [super initWithFrame:frame])) {
|
|
self.windowLevel = UIWindowLevelAlert + 1000;
|
|
self.backgroundColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1];
|
|
self.hidden = YES;
|
|
|
|
UIViewController *rootController = [UIViewController new];
|
|
self.rootViewController = rootController;
|
|
UIView *rootView = rootController.view;
|
|
rootView.backgroundColor = [UIColor clearColor];
|
|
|
|
const CGFloat buttonHeight = 60;
|
|
|
|
CGRect detailsFrame = rootView.bounds;
|
|
detailsFrame.size.height -= buttonHeight;
|
|
|
|
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
|
|
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
_stackTraceTableView.delegate = self;
|
|
_stackTraceTableView.dataSource = self;
|
|
_stackTraceTableView.backgroundColor = [UIColor clearColor];
|
|
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
|
|
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
|
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
|
|
[rootView addSubview:_stackTraceTableView];
|
|
|
|
UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
dismissButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
|
|
dismissButton.accessibilityIdentifier = @"redbox-dismiss";
|
|
dismissButton.titleLabel.font = [UIFont systemFontOfSize:14];
|
|
[dismissButton setTitle:@"Dismiss (ESC)" forState:UIControlStateNormal];
|
|
[dismissButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal];
|
|
[dismissButton setTitleColor:[UIColor whiteColor] 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:14];
|
|
[reloadButton setTitle:@"Reload JS (\u2318R)" forState:UIControlStateNormal];
|
|
[reloadButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal];
|
|
[reloadButton setTitleColor:[UIColor whiteColor] 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:14];
|
|
[copyButton setTitle:@"Copy (\u2325\u2318C)" forState:UIControlStateNormal];
|
|
[copyButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal];
|
|
[copyButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
|
|
[copyButton addTarget:self action:@selector(copyStack) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
CGFloat buttonWidth = self.bounds.size.width / 3;
|
|
dismissButton.frame = CGRectMake(0, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
|
|
reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
|
|
copyButton.frame = CGRectMake(buttonWidth * 2, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
|
|
[rootView addSubview:dismissButton];
|
|
[rootView addSubview:reloadButton];
|
|
[rootView addSubview:copyButton];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|
|
|
- (void)dealloc
|
|
{
|
|
_stackTraceTableView.dataSource = nil;
|
|
_stackTraceTableView.delegate = nil;
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate
|
|
{
|
|
// Show if this is a new message, or if we're updating the previous message
|
|
if ((self.hidden && !isUpdate) || (!self.hidden && 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)];
|
|
|
|
[_stackTraceTableView reloadData];
|
|
|
|
if (self.hidden) {
|
|
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
|
|
atScrollPosition:UITableViewScrollPositionTop
|
|
animated:NO];
|
|
}
|
|
|
|
[self makeKeyAndVisible];
|
|
[self becomeFirstResponder];
|
|
}
|
|
}
|
|
|
|
- (void)dismiss
|
|
{
|
|
self.hidden = YES;
|
|
[self resignFirstResponder];
|
|
[RCTSharedApplication().delegate.window makeKeyWindow];
|
|
}
|
|
|
|
- (void)reload
|
|
{
|
|
[_actionDelegate reloadFromRedBoxWindow:self];
|
|
}
|
|
|
|
- (void)copyStack
|
|
{
|
|
NSMutableString *fullStackTrace;
|
|
|
|
if (_lastErrorMessage != nil) {
|
|
fullStackTrace = [_lastErrorMessage mutableCopy];
|
|
[fullStackTrace appendString:@"\n\n"];
|
|
}
|
|
else {
|
|
fullStackTrace = [NSMutableString string];
|
|
}
|
|
|
|
for (NSDictionary *stackFrame in _lastStackTrace) {
|
|
[fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame[@"methodName"]]];
|
|
if (stackFrame[@"file"]) {
|
|
NSString *lineInfo = [NSString stringWithFormat:@" %@ @ %zd:%zd\n",
|
|
[stackFrame[@"file"] lastPathComponent],
|
|
[stackFrame[@"lineNumber"] integerValue],
|
|
[stackFrame[@"column"] integerValue]];
|
|
[fullStackTrace appendString:lineInfo];
|
|
}
|
|
}
|
|
|
|
UIPasteboard *pb = [UIPasteboard generalPasteboard];
|
|
[pb setString:fullStackTrace];
|
|
}
|
|
|
|
#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;
|
|
NSDictionary *stackFrame = _lastStackTrace[index];
|
|
return [self reuseCell:cell forStackFrame:stackFrame];
|
|
}
|
|
|
|
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message
|
|
{
|
|
if (!cell) {
|
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle 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 clearColor];
|
|
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
|
}
|
|
|
|
cell.textLabel.text = message;
|
|
|
|
return cell;
|
|
}
|
|
|
|
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(NSDictionary *)stackFrame
|
|
{
|
|
if (!cell) {
|
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
|
|
cell.textLabel.textColor = [UIColor colorWithWhite:1 alpha:0.9];
|
|
cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14];
|
|
cell.textLabel.numberOfLines = 2;
|
|
cell.detailTextLabel.textColor = [UIColor colorWithWhite:1 alpha:0.7];
|
|
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"];
|
|
if (stackFrame[@"file"]) {
|
|
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ @ %zd:%zd",
|
|
[stackFrame[@"file"] lastPathComponent],
|
|
[stackFrame[@"lineNumber"] integerValue],
|
|
[stackFrame[@"column"] integerValue]];
|
|
} 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;
|
|
NSDictionary *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)]
|
|
|
|
];
|
|
}
|
|
|
|
- (BOOL)canBecomeFirstResponder
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface RCTRedBox () <RCTInvalidating, RCTRedBoxWindowActionDelegate>
|
|
@end
|
|
|
|
@implementation RCTRedBox
|
|
{
|
|
RCTRedBoxWindow *_window;
|
|
}
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
- (void)showError:(NSError *)error
|
|
{
|
|
[self showErrorMessage:error.localizedDescription withDetails:error.localizedFailureReason];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message
|
|
{
|
|
[self showErrorMessage:message withStack:nil isUpdate:NO];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
|
|
{
|
|
NSString *combinedMessage = message;
|
|
if (details) {
|
|
combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details];
|
|
}
|
|
[self showErrorMessage:combinedMessage];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
|
|
{
|
|
[self showErrorMessage:message withStack:stack isUpdate:NO];
|
|
}
|
|
|
|
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
|
|
{
|
|
[self showErrorMessage:message withStack:stack isUpdate:YES];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (!_window) {
|
|
_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
_window.actionDelegate = self;
|
|
}
|
|
[_window showErrorMessage:message withStack:stack isUpdate:isUpdate];
|
|
});
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(dismiss)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[_window dismiss];
|
|
});
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
[self dismiss];
|
|
}
|
|
|
|
- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(NSDictionary *)stackFrame
|
|
{
|
|
if (![_bridge.bundleURL.scheme hasPrefix:@"http"]) {
|
|
RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager.");
|
|
return;
|
|
}
|
|
|
|
NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, NULL) dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length];
|
|
NSMutableURLRequest *request = [NSMutableURLRequest new];
|
|
request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:_bridge.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)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow {
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTBridge (RCTRedBox)
|
|
|
|
- (RCTRedBox *)redBox
|
|
{
|
|
return [self moduleForClass:[RCTRedBox class]];
|
|
}
|
|
|
|
@end
|
|
|
|
#else // Disabled
|
|
|
|
@implementation RCTRedBox
|
|
|
|
+ (NSString *)moduleName { return nil; }
|
|
- (void)showError:(NSError *)message {}
|
|
- (void)showErrorMessage:(NSString *)message {}
|
|
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {}
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
|
|
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack showIfHidden:(BOOL)shouldShow {}
|
|
- (void)dismiss {}
|
|
|
|
@end
|
|
|
|
@implementation RCTBridge (RCTRedBox)
|
|
|
|
- (RCTRedBox *)redBox { return nil; }
|
|
|
|
@end
|
|
|
|
#endif
|