added files via upload

This commit is contained in:
Balackburn
2023-06-27 09:54:41 +02:00
commit 2ff6aac218
1420 changed files with 88898 additions and 0 deletions

View File

@@ -0,0 +1,417 @@
//
// FLEXKeyPathSearchController.m
// FLEX
//
// Created by Tanner on 3/23/17.
// Copyright © 2017 Tanner Bennett. All rights reserved.
//
#import "FLEXKeyPathSearchController.h"
#import "FLEXRuntimeKeyPathTokenizer.h"
#import "FLEXRuntimeController.h"
#import "NSString+FLEX.h"
#import "NSArray+FLEX.h"
#import "UITextField+Range.h"
#import "NSTimer+FLEX.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
@interface FLEXKeyPathSearchController ()
@property (nonatomic, readonly, weak) id<FLEXKeyPathSearchControllerDelegate> delegate;
@property (nonatomic) NSTimer *timer;
/// If \c keyPath is \c nil or if it only has a \c bundleKey, this is
/// a list of bundle key path components like \c UICatalog or \c UIKit\.framework
/// If \c keyPath has more than a \c bundleKey then it is a list of class names.
@property (nonatomic) NSArray<NSString *> *bundlesOrClasses;
/// nil when search bar is empty
@property (nonatomic) FLEXRuntimeKeyPath *keyPath;
@property (nonatomic, readonly) NSString *emptySuggestion;
/// Used to track which methods go with which classes. This is used in
/// two scenarios: (1) when the target class is absolute and has classes,
/// (this list will include the "leaf" class as well as parent classes in this case)
/// or (2) when the class key is a wildcard and we're searching methods in many
/// classes at once. Each list in \c classesToMethods correspnds to a class here.
@property (nonatomic) NSArray<NSString *> *classes;
/// A filtered version of \c classes used when searching for a specific attribute.
/// Classes with no matching ivars/properties/methods are not shown.
@property (nonatomic) NSArray<NSString *> *filteredClasses;
// We use this regardless of whether the target class is absolute, just as above
@property (nonatomic) NSArray<NSArray<FLEXMethod *> *> *classesToMethods;
@end
@implementation FLEXKeyPathSearchController
+ (instancetype)delegate:(id<FLEXKeyPathSearchControllerDelegate>)delegate {
FLEXKeyPathSearchController *controller = [self new];
controller->_bundlesOrClasses = [FLEXRuntimeController allBundleNames];
controller->_delegate = delegate;
controller->_emptySuggestion = NSBundle.mainBundle.executablePath.lastPathComponent;
NSParameterAssert(delegate.tableView);
NSParameterAssert(delegate.searchController);
delegate.tableView.delegate = controller;
delegate.tableView.dataSource = controller;
UISearchBar *searchBar = delegate.searchController.searchBar;
searchBar.delegate = controller;
searchBar.keyboardType = UIKeyboardTypeWebSearch;
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
if (@available(iOS 11, *)) {
searchBar.smartQuotesType = UITextSmartQuotesTypeNo;
searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
}
return controller;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
[self.delegate.searchController.searchBar resignFirstResponder];
}
}
- (void)setToolbar:(FLEXRuntimeBrowserToolbar *)toolbar {
_toolbar = toolbar;
self.delegate.searchController.searchBar.inputAccessoryView = toolbar;
}
- (NSArray<NSString *> *)classesOf:(NSString *)className {
Class baseClass = NSClassFromString(className);
if (!baseClass) {
return @[];
}
// Find classes
NSMutableArray<NSString*> *classes = [NSMutableArray arrayWithObject:className];
while ([baseClass superclass]) {
[classes addObject:NSStringFromClass([baseClass superclass])];
baseClass = [baseClass superclass];
}
return classes;
}
#pragma mark Key path stuff
- (void)didSelectKeyPathOption:(NSString *)text {
[_timer invalidate]; // Still might be waiting to refresh when method is selected
// Change "Bundle.fooba" to "Bundle.foobar."
NSString *orig = self.delegate.searchController.searchBar.text;
NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text];
self.delegate.searchController.searchBar.text = keyPath;
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath];
// Get classes if class was selected
if (self.keyPath.classKey.isAbsolute && self.keyPath.methodKey.isAny) {
[self didSelectAbsoluteClass:text];
} else {
self.classes = nil;
self.filteredClasses = nil;
}
[self updateTable];
}
- (void)didSelectAbsoluteClass:(NSString *)name {
self.classes = [self classesOf:name];
self.filteredClasses = self.classes;
self.bundlesOrClasses = nil;
self.classesToMethods = nil;
}
- (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar {
[self.toolbar setKeyPath:self.keyPath suggestions:nil];
// Available since at least iOS 9, still present in iOS 13
UITextField *field = [searchBar valueForKey:@"_searchBarTextField"];
if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) {
[field replaceRange:field.selectedTextRange withText:text];
}
}
- (NSArray<NSString *> *)suggestions {
if (self.bundlesOrClasses) {
if (self.classes) {
if (self.classesToMethods) {
// We have selected a class and are searching metadata
return nil;
}
// We are currently searching classes
return [self.filteredClasses flex_subArrayUpto:10];
}
if (!self.keyPath) {
// Search bar is empty
return @[self.emptySuggestion];
}
// We are currently searching bundles
return [self.bundlesOrClasses flex_subArrayUpto:10];
}
// We have nothing at all to even search
return nil;
}
#pragma mark - Filtering + UISearchBarDelegate
- (void)updateTable {
// Compute the method, class, or bundle lists on a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (self.classes) {
// Here, our class key is 'absolute'; .classes is a list of superclasses
// and we want to show the methods for those classes specifically
// TODO: add caching to this somehow
NSMutableArray *methods = [FLEXRuntimeController
methodsForToken:self.keyPath.methodKey
instance:self.keyPath.instanceMethods
inClasses:self.classes
].mutableCopy;
// Remove classes without results if we're searching for a method
//
// Note: this will remove classes without any methods or overrides
// even if the query doesn't specify a method, like `*.*.`
if (self.keyPath.methodKey) {
[self setNonEmptyMethodLists:methods withClasses:self.classes.mutableCopy];
} else {
self.filteredClasses = self.classes;
}
}
else {
FLEXRuntimeKeyPath *keyPath = self.keyPath;
NSArray *models = [FLEXRuntimeController dataForKeyPath:keyPath];
if (keyPath.methodKey) { // We're looking at methods
self.bundlesOrClasses = nil;
NSMutableArray *methods = models.mutableCopy;
NSMutableArray<NSString *> *classes = [
FLEXRuntimeController classesForKeyPath:keyPath
];
self.classes = classes;
[self setNonEmptyMethodLists:methods withClasses:classes];
} else { // We're looking at bundles or classes
self.bundlesOrClasses = models;
self.classesToMethods = nil;
}
}
// Finally, reload the table on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self updateToolbarButtons];
[self.delegate.tableView reloadData];
});
});
}
- (void)updateToolbarButtons {
// Update toolbar buttons
[self.toolbar setKeyPath:self.keyPath suggestions:self.suggestions];
}
/// Assign assign .filteredClasses and .classesToMethods after removing empty sections
- (void)setNonEmptyMethodLists:(NSMutableArray<NSArray<FLEXMethod *> *> *)methods
withClasses:(NSMutableArray<NSString *> *)classes {
// Remove sections with no methods
NSIndexSet *allEmpty = [methods indexesOfObjectsPassingTest:^BOOL(NSArray *list, NSUInteger idx, BOOL *stop) {
return list.count == 0;
}];
[methods removeObjectsAtIndexes:allEmpty];
[classes removeObjectsAtIndexes:allEmpty];
self.filteredClasses = classes;
self.classesToMethods = methods;
}
- (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
// Check if character is even legal
if (![FLEXRuntimeKeyPathTokenizer allowedInKeyPath:text]) {
return NO;
}
BOOL terminatedToken = NO;
BOOL isAppending = range.length == 0 && range.location == searchBar.text.length;
if (isAppending && [text isEqualToString:@"."]) {
terminatedToken = YES;
}
// Actually parse input
@try {
text = [searchBar.text stringByReplacingCharactersInRange:range withString:text] ?: text;
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:text];
if (self.keyPath.classKey.isAbsolute && terminatedToken) {
[self didSelectAbsoluteClass:self.keyPath.classKey.string];
}
} @catch (id e) {
return NO;
}
return YES;
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
[_timer invalidate];
// Schedule update timer
if (searchText.length) {
if (!self.keyPath.methodKey) {
self.classes = nil;
self.filteredClasses = nil;
}
self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{
[self updateTable];
}];
}
// ... or remove all rows
else {
_bundlesOrClasses = [FLEXRuntimeController allBundleNames];
_classesToMethods = nil;
_classes = nil;
_keyPath = nil;
[self updateToolbarButtons];
[self.delegate.tableView reloadData];
}
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
self.keyPath = FLEXRuntimeKeyPath.empty;
[self updateTable];
}
/// Restore key path when going "back" and activating search bar again
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
searchBar.text = self.keyPath.description;
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
[_timer invalidate];
[searchBar resignFirstResponder];
[self updateTable];
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.filteredClasses.count ?: self.bundlesOrClasses.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXMultilineDetailCell
forIndexPath:indexPath
];
if (self.bundlesOrClasses.count) {
cell.accessoryType = UITableViewCellAccessoryDetailButton;
cell.textLabel.text = self.bundlesOrClasses[indexPath.row];
cell.detailTextLabel.text = nil;
if (self.keyPath.classKey) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
}
// One row per section
else if (self.filteredClasses.count) {
NSArray<FLEXMethod *> *methods = self.classesToMethods[indexPath.row];
NSMutableString *summary = [NSMutableString new];
[methods enumerateObjectsUsingBlock:^(FLEXMethod *method, NSUInteger idx, BOOL *stop) {
NSString *format = nil;
if (idx == methods.count-1) {
format = @"%@%@";
*stop = YES;
} else if (idx < 3) {
format = @"%@%@\n";
} else {
format = @"%@%@\n…";
*stop = YES;
}
[summary appendFormat:format, method.isInstanceMethod ? @"-" : @"+", method.selectorString];
}];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = self.filteredClasses[indexPath.row];
if (@available(iOS 10, *)) {
cell.detailTextLabel.text = summary.length ? summary : nil;
}
}
else {
@throw NSInternalInconsistencyException;
}
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (self.filteredClasses || self.keyPath.methodKey) {
return @" ";
} else if (self.bundlesOrClasses) {
NSInteger count = self.bundlesOrClasses.count;
if (self.keyPath.classKey) {
return FLEXPluralString(count, @"classes", @"class");
} else {
return FLEXPluralString(count, @"bundles", @"bundle");
}
}
return [self.delegate tableView:tableView titleForHeaderInSection:section];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (self.filteredClasses || self.keyPath.methodKey) {
if (section == 0) {
return 55;
}
return 0;
}
return 55;
}
#pragma mark UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.bundlesOrClasses) {
NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
if (self.keyPath.classKey) {
NSParameterAssert(NSClassFromString(bundleSuffixOrClass));
[self.delegate didSelectClass:NSClassFromString(bundleSuffixOrClass)];
} else {
// Selected a bundle
[self didSelectKeyPathOption:bundleSuffixOrClass];
}
} else {
if (self.filteredClasses.count) {
Class cls = NSClassFromString(self.filteredClasses[indexPath.row]);
NSParameterAssert(cls);
[self.delegate didSelectClass:cls];
} else {
@throw NSInternalInconsistencyException;
}
}
}
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
NSString *imagePath = [FLEXRuntimeController imagePathWithShortName:bundleSuffixOrClass];
NSBundle *bundle = [NSBundle bundleWithPath:imagePath.stringByDeletingLastPathComponent];
if (bundle) {
[self.delegate didSelectBundle:bundle];
} else {
[self.delegate didSelectImagePath:imagePath shortName:bundleSuffixOrClass];
}
}
@end