FLEXKeyPathSearchController.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. //
  2. // FLEXKeyPathSearchController.m
  3. // FLEX
  4. //
  5. // Created by Tanner on 3/23/17.
  6. // Copyright © 2017 Tanner Bennett. All rights reserved.
  7. //
  8. #import "FLEXKeyPathSearchController.h"
  9. #import "FLEXRuntimeKeyPathTokenizer.h"
  10. #import "FLEXRuntimeController.h"
  11. #import "NSString+FLEX.h"
  12. #import "NSArray+FLEX.h"
  13. #import "UITextField+Range.h"
  14. #import "NSTimer+FLEX.h"
  15. #import "FLEXTableView.h"
  16. #import "FLEXUtility.h"
  17. #import "FLEXObjectExplorerFactory.h"
  18. @interface FLEXKeyPathSearchController ()
  19. @property (nonatomic, readonly, weak) id<FLEXKeyPathSearchControllerDelegate> delegate;
  20. @property (nonatomic) NSTimer *timer;
  21. /// If \c keyPath is \c nil or if it only has a \c bundleKey, this is
  22. /// a list of bundle key path components like \c UICatalog or \c UIKit\.framework
  23. /// If \c keyPath has more than a \c bundleKey then it is a list of class names.
  24. @property (nonatomic) NSArray<NSString *> *bundlesOrClasses;
  25. /// nil when search bar is empty
  26. @property (nonatomic) FLEXRuntimeKeyPath *keyPath;
  27. @property (nonatomic, readonly) NSString *emptySuggestion;
  28. /// Used to track which methods go with which classes. This is used in
  29. /// two scenarios: (1) when the target class is absolute and has classes,
  30. /// (this list will include the "leaf" class as well as parent classes in this case)
  31. /// or (2) when the class key is a wildcard and we're searching methods in many
  32. /// classes at once. Each list in \c classesToMethods correspnds to a class here.
  33. @property (nonatomic) NSArray<NSString *> *classes;
  34. /// A filtered version of \c classes used when searching for a specific attribute.
  35. /// Classes with no matching ivars/properties/methods are not shown.
  36. @property (nonatomic) NSArray<NSString *> *filteredClasses;
  37. // We use this regardless of whether the target class is absolute, just as above
  38. @property (nonatomic) NSArray<NSArray<FLEXMethod *> *> *classesToMethods;
  39. @end
  40. @implementation FLEXKeyPathSearchController
  41. + (instancetype)delegate:(id<FLEXKeyPathSearchControllerDelegate>)delegate {
  42. FLEXKeyPathSearchController *controller = [self new];
  43. controller->_bundlesOrClasses = [FLEXRuntimeController allBundleNames];
  44. controller->_delegate = delegate;
  45. controller->_emptySuggestion = NSBundle.mainBundle.executablePath.lastPathComponent;
  46. NSParameterAssert(delegate.tableView);
  47. NSParameterAssert(delegate.searchController);
  48. delegate.tableView.delegate = controller;
  49. delegate.tableView.dataSource = controller;
  50. UISearchBar *searchBar = delegate.searchController.searchBar;
  51. searchBar.delegate = controller;
  52. searchBar.keyboardType = UIKeyboardTypeWebSearch;
  53. searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
  54. if (@available(iOS 11, *)) {
  55. searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
  56. }
  57. return controller;
  58. }
  59. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  60. if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
  61. [self.delegate.searchController.searchBar resignFirstResponder];
  62. }
  63. }
  64. - (void)setToolbar:(FLEXRuntimeBrowserToolbar *)toolbar {
  65. _toolbar = toolbar;
  66. self.delegate.searchController.searchBar.inputAccessoryView = toolbar;
  67. }
  68. - (NSArray<NSString *> *)classesOf:(NSString *)className {
  69. Class baseClass = NSClassFromString(className);
  70. if (!baseClass) {
  71. return @[];
  72. }
  73. // Find classes
  74. NSMutableArray<NSString*> *classes = [NSMutableArray arrayWithObject:className];
  75. while ([baseClass superclass]) {
  76. [classes addObject:NSStringFromClass([baseClass superclass])];
  77. baseClass = [baseClass superclass];
  78. }
  79. return classes;
  80. }
  81. #pragma mark Key path stuff
  82. - (void)didSelectKeyPathOption:(NSString *)text {
  83. [_timer invalidate]; // Still might be waiting to refresh when method is selected
  84. // Change "Bundle.fooba" to "Bundle.foobar."
  85. NSString *orig = self.delegate.searchController.searchBar.text;
  86. NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text];
  87. self.delegate.searchController.searchBar.text = keyPath;
  88. self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath];
  89. // Get classes if class was selected
  90. if (self.keyPath.classKey.isAbsolute && self.keyPath.methodKey.isAny) {
  91. [self didSelectAbsoluteClass:text];
  92. } else {
  93. self.classes = nil;
  94. self.filteredClasses = nil;
  95. }
  96. [self updateTable];
  97. }
  98. - (void)didSelectAbsoluteClass:(NSString *)name {
  99. self.classes = [self classesOf:name];
  100. self.filteredClasses = self.classes;
  101. self.bundlesOrClasses = nil;
  102. self.classesToMethods = nil;
  103. }
  104. - (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar {
  105. [self.toolbar setKeyPath:self.keyPath suggestions:nil];
  106. // Available since at least iOS 9, still present in iOS 13
  107. UITextField *field = [searchBar valueForKey:@"_searchBarTextField"];
  108. if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) {
  109. [field replaceRange:field.selectedTextRange withText:text];
  110. }
  111. }
  112. - (NSArray<NSString *> *)suggestions {
  113. if (self.bundlesOrClasses) {
  114. if (self.classes) {
  115. if (self.classesToMethods) {
  116. // We have selected a class and are searching metadata
  117. return nil;
  118. }
  119. // We are currently searching classes
  120. return [self.filteredClasses flex_subArrayUpto:10];
  121. }
  122. if (!self.keyPath) {
  123. // Search bar is empty
  124. return @[self.emptySuggestion];
  125. }
  126. // We are currently searching bundles
  127. return [self.bundlesOrClasses flex_subArrayUpto:10];
  128. }
  129. // We have nothing at all to even search
  130. return nil;
  131. }
  132. #pragma mark - Filtering + UISearchBarDelegate
  133. - (void)updateTable {
  134. // Compute the method, class, or bundle lists on a background thread
  135. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  136. if (self.classes) {
  137. // Here, our class key is 'absolute'; .classes is a list of superclasses
  138. // and we want to show the methods for those classes specifically
  139. // TODO: add caching to this somehow
  140. NSMutableArray *methods = [FLEXRuntimeController
  141. methodsForToken:self.keyPath.methodKey
  142. instance:self.keyPath.instanceMethods
  143. inClasses:self.classes
  144. ].mutableCopy;
  145. // Remove classes without results if we're searching for a method
  146. //
  147. // Note: this will remove classes without any methods or overrides
  148. // even if the query doesn't specify a method, like `*.*.`
  149. if (self.keyPath.methodKey) {
  150. [self setNonEmptyMethodLists:methods withClasses:self.classes.mutableCopy];
  151. } else {
  152. self.filteredClasses = self.classes;
  153. }
  154. }
  155. else {
  156. FLEXRuntimeKeyPath *keyPath = self.keyPath;
  157. NSArray *models = [FLEXRuntimeController dataForKeyPath:keyPath];
  158. if (keyPath.methodKey) { // We're looking at methods
  159. self.bundlesOrClasses = nil;
  160. NSMutableArray *methods = models.mutableCopy;
  161. NSMutableArray<NSString *> *classes = [
  162. FLEXRuntimeController classesForKeyPath:keyPath
  163. ];
  164. self.classes = classes;
  165. [self setNonEmptyMethodLists:methods withClasses:classes];
  166. } else { // We're looking at bundles or classes
  167. self.bundlesOrClasses = models;
  168. self.classesToMethods = nil;
  169. }
  170. }
  171. // Finally, reload the table on the main thread
  172. dispatch_async(dispatch_get_main_queue(), ^{
  173. [self updateToolbarButtons];
  174. [self.delegate.tableView reloadData];
  175. });
  176. });
  177. }
  178. - (void)updateToolbarButtons {
  179. // Update toolbar buttons
  180. [self.toolbar setKeyPath:self.keyPath suggestions:self.suggestions];
  181. }
  182. /// Assign assign .filteredClasses and .classesToMethods after removing empty sections
  183. - (void)setNonEmptyMethodLists:(NSMutableArray<NSArray<FLEXMethod *> *> *)methods
  184. withClasses:(NSMutableArray<NSString *> *)classes {
  185. // Remove sections with no methods
  186. NSIndexSet *allEmpty = [methods indexesOfObjectsPassingTest:^BOOL(NSArray *list, NSUInteger idx, BOOL *stop) {
  187. return list.count == 0;
  188. }];
  189. [methods removeObjectsAtIndexes:allEmpty];
  190. [classes removeObjectsAtIndexes:allEmpty];
  191. self.filteredClasses = classes;
  192. self.classesToMethods = methods;
  193. }
  194. - (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
  195. // Check if character is even legal
  196. if (![FLEXRuntimeKeyPathTokenizer allowedInKeyPath:text]) {
  197. return NO;
  198. }
  199. BOOL terminatedToken = NO;
  200. BOOL isAppending = range.length == 0 && range.location == searchBar.text.length;
  201. if (isAppending && [text isEqualToString:@"."]) {
  202. terminatedToken = YES;
  203. }
  204. // Actually parse input
  205. @try {
  206. text = [searchBar.text stringByReplacingCharactersInRange:range withString:text] ?: text;
  207. self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:text];
  208. if (self.keyPath.classKey.isAbsolute && terminatedToken) {
  209. [self didSelectAbsoluteClass:self.keyPath.classKey.string];
  210. }
  211. } @catch (id e) {
  212. return NO;
  213. }
  214. return YES;
  215. }
  216. - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
  217. [_timer invalidate];
  218. // Schedule update timer
  219. if (searchText.length) {
  220. if (!self.keyPath.methodKey) {
  221. self.classes = nil;
  222. self.filteredClasses = nil;
  223. }
  224. self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{
  225. [self updateTable];
  226. }];
  227. }
  228. // ... or remove all rows
  229. else {
  230. _bundlesOrClasses = [FLEXRuntimeController allBundleNames];
  231. _classesToMethods = nil;
  232. _classes = nil;
  233. _keyPath = nil;
  234. [self updateToolbarButtons];
  235. [self.delegate.tableView reloadData];
  236. }
  237. }
  238. - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
  239. self.keyPath = FLEXRuntimeKeyPath.empty;
  240. [self updateTable];
  241. }
  242. /// Restore key path when going "back" and activating search bar again
  243. - (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
  244. searchBar.text = self.keyPath.description;
  245. }
  246. - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
  247. [_timer invalidate];
  248. [searchBar resignFirstResponder];
  249. [self updateTable];
  250. }
  251. #pragma mark UITableViewDataSource
  252. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  253. return self.filteredClasses.count ?: self.bundlesOrClasses.count;
  254. }
  255. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  256. UITableViewCell *cell = [tableView
  257. dequeueReusableCellWithIdentifier:kFLEXMultilineDetailCell
  258. forIndexPath:indexPath
  259. ];
  260. if (self.bundlesOrClasses.count) {
  261. #if !TARGET_OS_TV
  262. cell.accessoryType = UITableViewCellAccessoryDetailButton;
  263. #else
  264. cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  265. #endif
  266. cell.textLabel.text = self.bundlesOrClasses[indexPath.row];
  267. cell.detailTextLabel.text = nil;
  268. if (self.keyPath.classKey) {
  269. cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  270. }
  271. }
  272. // One row per section
  273. else if (self.filteredClasses.count) {
  274. NSArray<FLEXMethod *> *methods = self.classesToMethods[indexPath.row];
  275. NSMutableString *summary = [NSMutableString new];
  276. [methods enumerateObjectsUsingBlock:^(FLEXMethod *method, NSUInteger idx, BOOL *stop) {
  277. NSString *format = nil;
  278. if (idx == methods.count-1) {
  279. format = @"%@%@";
  280. *stop = YES;
  281. } else if (idx < 3) {
  282. format = @"%@%@\n";
  283. } else {
  284. format = @"%@%@\n…";
  285. *stop = YES;
  286. }
  287. [summary appendFormat:format, method.isInstanceMethod ? @"-" : @"+", method.selectorString];
  288. }];
  289. cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  290. cell.textLabel.text = self.filteredClasses[indexPath.row];
  291. if (@available(iOS 10, *)) {
  292. cell.detailTextLabel.text = summary.length ? summary : nil;
  293. }
  294. }
  295. else {
  296. @throw NSInternalInconsistencyException;
  297. }
  298. return cell;
  299. }
  300. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
  301. if (self.filteredClasses || self.keyPath.methodKey) {
  302. return @" ";
  303. } else if (self.bundlesOrClasses) {
  304. NSInteger count = self.bundlesOrClasses.count;
  305. if (self.keyPath.classKey) {
  306. return FLEXPluralString(count, @"classes", @"class");
  307. } else {
  308. return FLEXPluralString(count, @"bundles", @"bundle");
  309. }
  310. }
  311. return [self.delegate tableView:tableView titleForHeaderInSection:section];
  312. }
  313. - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
  314. if (self.filteredClasses || self.keyPath.methodKey) {
  315. if (section == 0) {
  316. return 55;
  317. }
  318. return 0;
  319. }
  320. return 55;
  321. }
  322. #pragma mark UITableViewDelegate
  323. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  324. if (self.bundlesOrClasses) {
  325. NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
  326. if (self.keyPath.classKey) {
  327. NSParameterAssert(NSClassFromString(bundleSuffixOrClass));
  328. [self.delegate didSelectClass:NSClassFromString(bundleSuffixOrClass)];
  329. } else {
  330. // Selected a bundle
  331. [self didSelectKeyPathOption:bundleSuffixOrClass];
  332. }
  333. } else {
  334. if (self.filteredClasses.count) {
  335. Class cls = NSClassFromString(self.filteredClasses[indexPath.row]);
  336. NSParameterAssert(cls);
  337. [self.delegate didSelectClass:cls];
  338. } else {
  339. @throw NSInternalInconsistencyException;
  340. }
  341. }
  342. }
  343. - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
  344. NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
  345. NSString *imagePath = [FLEXRuntimeController imagePathWithShortName:bundleSuffixOrClass];
  346. NSBundle *bundle = [NSBundle bundleWithPath:imagePath.stringByDeletingLastPathComponent];
  347. if (bundle) {
  348. [self.delegate didSelectBundle:bundle];
  349. } else {
  350. [self.delegate didSelectImagePath:imagePath shortName:bundleSuffixOrClass];
  351. }
  352. }
  353. @end