Explorar el Código

Add files for runtime browser

Tanner Bennett hace 6 años
padre
commit
bbc64efb12
Se han modificado 27 ficheros con 2218 adiciones y 4 borrados
  1. 39 0
      Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntime.h
  2. 368 0
      Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntime.m
  3. 26 0
      Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntimeController.h
  4. 176 0
      Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntimeController.m
  5. 42 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPath.h
  6. 57 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPath.m
  7. 36 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathSearchController.h
  8. 305 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathSearchController.m
  9. 19 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathTokenizer.h
  10. 205 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathTokenizer.m
  11. 19 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathToolbar.h
  12. 87 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathToolbar.m
  13. 14 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathViewController.h
  14. 123 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathViewController.m
  15. 23 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyboardToolbar.h
  16. 172 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyboardToolbar.m
  17. 34 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBToken.h
  18. 74 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBToken.m
  19. 27 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBToolbarButton.h
  20. 100 0
      Classes/GlobalStateExplorers/RuntimeBrowser/TBToolbarButton.m
  21. 16 0
      Classes/Utility/Categories/NSString+KeyPaths.h
  22. 85 0
      Classes/Utility/Categories/NSString+KeyPaths.m
  23. 21 0
      Classes/Utility/Categories/UIGestureRecognizer+Blocks.h
  24. 36 0
      Classes/Utility/Categories/UIGestureRecognizer+Blocks.m
  25. 0 2
      Classes/Utility/Runtime/FLEXClassBuilder.m
  26. 0 2
      Classes/Utility/Runtime/FLEXMirror.m
  27. 114 0
      FLEX.xcodeproj/project.pbxproj

+ 39 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntime.h

@@ -0,0 +1,39 @@
+//
+//  TBRuntime.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBToken.h"
+@class FLEXMethod;
+
+
+/// Accepts runtime queries given a token.
+@interface TBRuntime : NSObject
+
++ (instancetype)runtime;
+
+/// Called automatically when \c TBRuntime is first used.
+/// You may call it again when you think a library has
+/// been loaded since this method was first called.
+- (void)reloadLibrariesList;
+
+/// An array of strings representing the currently loaded libraries.
+@property (nonatomic, readonly) NSArray<NSString*> *imageDisplayNames;
+
+- (NSString *)shortNameForImageName:(NSString *)imageName;
+
+/// @return Bundle names for the UI
+- (NSMutableArray<NSString*> *)bundleNamesForToken:(TBToken *)token;
+/// @return Bundle paths for more queries
+- (NSMutableArray<NSString*> *)bundlePathsForToken:(TBToken *)token;
+/// @return Class names
+- (NSMutableArray<NSString*> *)classesForToken:(TBToken *)token inBundles:(NSMutableArray<NSString*> *)bundlePaths;
+/// @return Actual methods
+- (NSMutableArray<FLEXMethod*> *)methodsForToken:(TBToken *)token
+                                      instance:(NSNumber *)onlyInstanceMethods
+                                     inClasses:(NSMutableArray<NSString*> *)classes;
+
+@end

+ 368 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntime.m

@@ -0,0 +1,368 @@
+//
+//  TBRuntime.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBRuntime.h"
+#import "NSObject+Reflection.h"
+#import "FLEXMethod.h"
+#import "NSArray+Functional.h"
+//#import "MKRuntimeSafety.h"
+
+
+#define TBEquals(a, b) ([a compare:b options:NSCaseInsensitiveSearch] == NSOrderedSame)
+#define TBContains(a, b) ([a rangeOfString:b options:NSCaseInsensitiveSearch].location != NSNotFound)
+#define TBHasPrefix(a, b) ([a rangeOfString:b options:NSCaseInsensitiveSearch].location == 0)
+#define TBHasSuffix(a, b) ([a rangeOfString:b options:NSCaseInsensitiveSearch].location == (a.length - b.length))
+
+
+@interface TBRuntime () {
+    NSMutableArray<NSString*> *_imageDisplayNames;
+}
+
+@property (nonatomic) NSMutableDictionary *bundles_pathToShort;
+@property (nonatomic) NSCache *bundles_pathToClassNames;
+@property (nonatomic) NSMutableArray<NSString*> *imagePaths;
+
+@end
+
+/// @return success if the map passes.
+static inline NSString * TBWildcardMap_(NSString *token, NSString *candidate, NSString *success, TBWildcardOptions options) {
+    switch (options) {
+        case TBWildcardOptionsNone:
+            // Only "if equals"
+            if (TBEquals(candidate, token)) {
+                return success;
+            }
+        default: {
+            // Only "if contains"
+            if (options & TBWildcardOptionsPrefix &&
+                options & TBWildcardOptionsSuffix) {
+                if (TBContains(candidate, token)) {
+                    return success;
+                }
+            }
+            // Only "if candidate ends with with token"
+            else if (options & TBWildcardOptionsPrefix) {
+                if (TBHasSuffix(candidate, token)) {
+                    return success;
+                }
+            }
+            // Only "if candidate starts with with token"
+            else if (options & TBWildcardOptionsSuffix) {
+                // Case like "Bundle." where we want "" to match anything
+                if (!token.length) {
+                    return success;
+                }
+                if (TBHasPrefix(candidate, token)) {
+                    return success;
+                }
+            }
+        }
+    }
+
+    return nil;
+}
+
+/// @return candidate if the map passes.
+static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBWildcardOptions options) {
+    return TBWildcardMap_(token, candidate, candidate, options);
+}
+
+@implementation TBRuntime
+
+#pragma mark - Initialization
+
++ (instancetype)runtime {
+    static TBRuntime *runtime;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        runtime = [self new];
+        [runtime reloadLibrariesList];
+    });
+
+    return runtime;
+}
+
+- (id)init {
+    self = [super init];
+    if (self) {
+        _imagePaths = [NSMutableArray array];
+        _bundles_pathToShort = [NSMutableDictionary dictionary];
+        _bundles_pathToClassNames = [NSCache new];
+    }
+
+    return self;
+}
+
+#pragma mark - Private
+
+- (void)reloadLibrariesList {
+    unsigned int imageCount = 0;
+    const char **imageNames = objc_copyImageNames(&imageCount);
+
+    if (imageNames) {
+        NSMutableArray *imageNameStrings = [NSMutableArray flex_forEachUpTo:imageCount map:^NSString *(NSUInteger i) {
+            return @(imageNames[i]);
+        }];
+
+        self.imagePaths = imageNameStrings;
+        free(imageNames);
+
+        // Sort alphabetically
+        [imageNameStrings sortUsingComparator:^NSComparisonResult(NSString *name1, NSString *name2) {
+            NSString *shortName1 = [self shortNameForImageName:name1];
+            NSString *shortName2 = [self shortNameForImageName:name2];
+            return [shortName1 caseInsensitiveCompare:shortName2];
+        }];
+
+        // Cache image display names
+        _imageDisplayNames = [imageNameStrings flex_mapped:^id(NSString *path, NSUInteger idx) {
+            return [self shortNameForImageName:path];
+        }];
+    }
+}
+
+- (NSString *)shortNameForImageName:(NSString *)imageName {
+    // Cache
+    NSString *shortName = _bundles_pathToShort[imageName];
+    if (shortName) {
+        return shortName;
+    }
+
+    NSArray *components = [imageName componentsSeparatedByString:@"/"];
+    if (components.count >= 2) {
+        NSString *parentDir = components[components.count - 2];
+        if ([parentDir hasSuffix:@".framework"] || [parentDir hasSuffix:@".axbundle"]) {
+            shortName = parentDir;
+        }
+    }
+
+    if (!shortName) {
+        shortName = imageName.lastPathComponent;
+    }
+
+    _bundles_pathToShort[imageName] = shortName;
+    return shortName;
+}
+
+- (NSMutableArray<NSString*> *)classNamesInImageAtPath:(NSString *)path {
+    // Check cache
+    NSMutableArray *classNameStrings = [_bundles_pathToClassNames objectForKey:path];
+    if (classNameStrings) {
+        return classNameStrings.mutableCopy;
+    }
+
+    unsigned int classCount = 0;
+    const char **classNames = objc_copyClassNamesForImage(path.UTF8String, &classCount);
+
+    if (classNames) {
+        classNameStrings = [NSMutableArray flex_forEachUpTo:classCount map:^id(NSUInteger i) {
+            return @(classNames[i]);
+        }];
+
+        free(classNames);
+
+        [classNameStrings sortUsingSelector:@selector(caseInsensitiveCompare:)];
+        [_bundles_pathToClassNames setObject:classNameStrings forKey:path];
+
+        return classNameStrings.mutableCopy;
+    }
+
+    return [NSMutableArray array];
+}
+
+#pragma mark - Public
+
+- (NSMutableArray<NSString*> *)bundleNamesForToken:(TBToken *)token {
+    if (self.imagePaths.count) {
+        TBWildcardOptions options = token.options;
+        NSString *query = token.string;
+
+        // Optimization, avoid a loop
+        if (options == TBWildcardOptionsAny) {
+            return _imageDisplayNames;
+        }
+
+        // No dot syntax because imageDisplayNames is only mutable internally
+        return [_imageDisplayNames flex_mapped:^id(NSString *binary, NSUInteger idx) {
+            NSString *UIName = [self shortNameForImageName:binary];
+            return TBWildcardMap(query, UIName, options);
+        }];
+    }
+
+    return [NSMutableArray array];
+}
+
+- (NSMutableArray<NSString*> *)bundlePathsForToken:(TBToken *)token {
+    if (self.imagePaths.count) {
+        TBWildcardOptions options = token.options;
+        NSString *query = token.string;
+
+        // Optimization, avoid a loop
+        if (options == TBWildcardOptionsAny) {
+            return self.imagePaths;
+        }
+
+        return [self.imagePaths flex_mapped:^id(NSString *binary, NSUInteger idx) {
+            NSString *UIName = [self shortNameForImageName:binary];
+            // If query == UIName, -> binary
+            return TBWildcardMap_(query, UIName, binary, options);
+        }];
+    }
+
+    return [NSMutableArray array];
+}
+
+- (NSMutableArray<NSString*> *)classesForToken:(TBToken *)token inBundles:(NSMutableArray<NSString*> *)bundles {
+    // Edge case where token is the class we want already
+    if (token.isAbsolute) {
+        if (MKClassIsSafe(NSClassFromString(token.string))) {
+            return [NSMutableArray arrayWithObject:token.string];
+        }
+
+        return [NSMutableArray array];
+    }
+
+    if (bundles.count) {
+        // Get class names, remove unsafe classes
+        NSMutableArray<NSString*> *names = [self _classesForToken:token inBundles:bundles];
+        return [names flex_mapped:^NSString *(NSString *cls, NSUInteger idx) {
+            NSSet *ignored = MKKnownUnsafeClassNames();
+            BOOL safe = ![ignored containsObject:cls];
+            return safe ? cls : nil;
+        }];
+    }
+
+    return [NSMutableArray array];
+}
+
+- (NSMutableArray<NSString*> *)_classesForToken:(TBToken *)token inBundles:(NSMutableArray<NSString*> *)bundles {
+    TBWildcardOptions options = token.options;
+    NSString *query = token.string;
+
+    // Optimization, avoid unnecessary sorting
+    if (bundles.count == 1) {
+        // Optimization, avoid a loop
+        if (options == TBWildcardOptionsAny) {
+            return [self classNamesInImageAtPath:bundles.firstObject];
+        }
+
+        return [[self classNamesInImageAtPath:bundles.firstObject] flex_mapped:^id(NSString *className, NSUInteger idx) {
+            return TBWildcardMap(query, className, options);
+        }];
+    }
+    else {
+        // Optimization, avoid a loop
+        if (options == TBWildcardOptionsAny) {
+            return [[bundles flex_mapped:^NSArray *(NSString *bundlePath, NSUInteger idx) {
+                return [self classNamesInImageAtPath:bundlePath];
+            }] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
+        }
+
+        return [[bundles flex_mapped:^NSArray *(NSString *bundlePath, NSUInteger idx) {
+            return [[self classNamesInImageAtPath:bundlePath] flex_mapped:^id(NSString *className, NSUInteger idx) {
+                return TBWildcardMap(query, className, options);
+            }];
+        }] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
+    }
+}
+
+- (NSMutableArray<FLEXMethod*> *)methodsForToken:(TBToken *)token
+                                      instance:(NSNumber *)checkInstance
+                                     inClasses:(NSMutableArray *)classes {
+    if (classes.count) {
+        TBWildcardOptions options = token.options;
+        BOOL instance = checkInstance.boolValue;
+        NSString *selector = token.string;
+
+        switch (options) {
+            /// In practice, I don't think this case is ever used with methods
+            case TBWildcardOptionsNone: {
+                SEL sel = (SEL)selector.UTF8String;
+                return [classes flex_mapped:^id(NSString *name, NSUInteger idx) {
+                    Class cls = NSClassFromString(name);
+
+                    // Method is absolute
+                    return [MKLazyMethod methodForSelector:sel class:cls instance:instance];
+                }];
+            }
+            case TBWildcardOptionsAny: {
+                return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) {
+                    // Any means `instance` was not specified
+                    Class cls = NSClassFromString(name);
+                    return [cls flex_allMethods];
+                }];
+            }
+            default: {
+                // Only "if contains"
+                if (options & TBWildcardOptionsPrefix &&
+                    options & TBWildcardOptionsSuffix) {
+                    return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) {
+                        Class cls = NSClassFromString(name);
+                        return [[cls flex_allMethods] flex_mapped:^id(FLEXMethod *method, NSUInteger idx) {
+
+                            // Method is a prefix-suffix wildcard
+                            if (TBContains(method.selectorString, selector)) {
+                                return method;
+                            }
+                            return nil;
+                        }];
+                    }];
+                }
+                // Only "if method ends with with selector"
+                else if (options & TBWildcardOptionsPrefix) {
+                    return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) {
+                        Class cls = NSClassFromString(name);
+
+                        return [[cls flex_allMethods] flex_mapped:^id(FLEXMethod *method, NSUInteger idx) {
+                            // Method is a prefix wildcard
+                            if (TBHasSuffix(method.selectorString, selector)) {
+                                return method;
+                            }
+                            return nil;
+                        }];
+                    }];
+                }
+                // Only "if method starts with with selector"
+                else if (options & TBWildcardOptionsSuffix) {
+                    assert(checkInstance);
+
+                    return [classes flex_mapped:^NSArray *(NSString *name, NSUInteger idx) {
+                        Class cls = NSClassFromString(name);
+
+                        // Case like "Bundle.class.-" where we want "-" to match anything
+                        if (!selector.length) {
+                            if (instance) {
+                                return [cls flex_allInstanceMethods];
+                            } else {
+                                return [cls flex_allClassMethods];
+                            }
+                        }
+
+                        id mapping = ^id(FLEXMethod *method) {
+                            // Method is a suffix wildcard
+                            if (TBHasPrefix(method.selectorString, selector)) {
+                                return method;
+                            }
+                            return nil;
+                        };
+
+                        if (instance) {
+                            return [[cls flex_allInstanceMethods] flex_mapped:mapping];
+                        } else {
+                            return [[cls flex_allClassMethods] flex_mapped:mapping];
+                        }
+                    }];
+                }
+            }
+        }
+    }
+    
+    return [NSMutableArray array];
+}
+
+@end

+ 26 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntimeController.h

@@ -0,0 +1,26 @@
+//
+//  TBRuntimeController.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/23/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyPath.h"
+
+
+@interface TBRuntimeController : NSObject
+
+/// @return An array of strings if the key path only evaluates
+///         to a class or bundle; otherwise, an array of FLEXMethods.
++ (NSArray *)dataForKeyPath:(TBKeyPath *)keyPath;
+
++ (NSDictionary *)methodsForToken:(TBToken *)token
+                         instance:(NSNumber *)onlyInstanceMethods
+                        inClasses:(NSArray<NSString*> *)classes;
+
++ (NSString *)shortBundleNameForClass:(NSString *)name;
+
++ (NSArray<NSString*> *)allBundleNames;
+
+@end

+ 176 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/DataSources/TBRuntimeController.m

@@ -0,0 +1,176 @@
+//
+//  TBRuntimeController.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/23/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBRuntimeController.h"
+#import "TBRuntime.h"
+#import "FLEXMethod.h"
+
+
+@interface TBRuntimeController ()
+@property (nonatomic, readonly) NSCache *bundlePathsCache;
+@property (nonatomic, readonly) NSCache *bundleNamesCache;
+@property (nonatomic, readonly) NSCache *classNamesCache;
+@property (nonatomic, readonly) NSCache *methodsCache;
+@end
+
+@implementation TBRuntimeController
+
+#pragma mark Initialization
+
+static TBRuntimeController *controller = nil;
++ (instancetype)shared {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        controller = [self new];
+    });
+
+    return controller;
+}
+
+- (id)init {
+    self = [super init];
+    if (self) {
+        _bundlePathsCache = [NSCache new];
+        _bundleNamesCache = [NSCache new];
+        _classNamesCache  = [NSCache new];
+        _methodsCache     = [NSCache new];
+    }
+
+    return self;
+}
+
+#pragma mark Public
+
++ (NSArray *)dataForKeyPath:(TBKeyPath *)keyPath {
+    if (keyPath.bundleKey) {
+        if (keyPath.classKey) {
+            if (keyPath.methodKey) {
+                return [[self shared] methodsForKeyPath:keyPath];
+            } else {
+                return [[self shared] classesForClassToken:keyPath.classKey andBundleToken:keyPath.bundleKey];
+            }
+        } else {
+            return [[self shared] bundleNamesForToken:keyPath.bundleKey];
+        }
+    } else {
+        return @[];
+    }
+}
+
++ (NSDictionary *)methodsForToken:(TBToken *)token
+                         instance:(NSNumber *)inst
+                        inClasses:(NSArray<NSString*> *)classes {
+    NSMutableDictionary *methods = [NSMutableDictionary dictionary];
+    for (NSString *className in classes) {
+        NSMutableArray *target = [NSMutableArray arrayWithObject:className];
+        methods[className] = [[TBRuntime runtime] methodsForToken:token
+                                                         instance:inst
+                                                        inClasses:target];
+    }
+
+    return methods;
+}
+
++ (NSString *)shortBundleNameForClass:(NSString *)name {
+    NSString *imagePath = @(class_getImageName(NSClassFromString(name)));
+    return [[TBRuntime runtime] shortNameForImageName:imagePath];
+}
+
++ (NSArray *)allBundleNames {
+    return [TBRuntime runtime].imageDisplayNames;
+}
+
+#pragma mark Private
+
+- (NSMutableArray *)bundlePathsForToken:(TBToken *)token {
+    // Only cache if no wildcard
+    BOOL shouldCache = token == TBWildcardOptionsNone;
+
+    if (shouldCache) {
+        NSMutableArray<NSString*> *cached = [self.bundlePathsCache objectForKey:token];
+        if (cached) {
+            return cached;
+        }
+
+        NSMutableArray<NSString*> *bundles = [[TBRuntime runtime] bundlePathsForToken:token];
+        [self.bundlePathsCache setObject:bundles forKey:token];
+        return bundles;
+    }
+    else {
+        return [[TBRuntime runtime] bundlePathsForToken:token];
+    }
+}
+
+- (NSMutableArray<NSString*> *)bundleNamesForToken:(TBToken *)token {
+    // Only cache if no wildcard
+    BOOL shouldCache = token == TBWildcardOptionsNone;
+
+    if (shouldCache) {
+        NSMutableArray<NSString*> *cached = [self.bundleNamesCache objectForKey:token];
+        if (cached) {
+            return cached;
+        }
+
+        NSMutableArray<NSString*> *bundles = [[TBRuntime runtime] bundleNamesForToken:token];
+        [self.bundleNamesCache setObject:bundles forKey:token];
+        return bundles;
+    }
+    else {
+        return [[TBRuntime runtime] bundleNamesForToken:token];
+    }
+}
+
+- (NSMutableArray<NSString*> *)classesForClassToken:(TBToken *)clsToken andBundleToken:(TBToken *)bundleToken {
+    // Only cache if no wildcard
+    BOOL shouldCache = bundleToken.options == 0 && clsToken.options == 0;
+    NSString *key = nil;
+
+    if (shouldCache) {
+        key = [@[bundleToken.description, clsToken.description] componentsJoinedByString:@"+"];
+        NSMutableArray<NSString*> *cached = [self.classNamesCache objectForKey:key];
+        if (cached) {
+            return cached;
+        }
+    }
+
+    NSMutableArray<NSString*> *bundles = [self bundlePathsForToken:bundleToken];
+    NSMutableArray<NSString*> *classes = [[TBRuntime runtime] classesForToken:clsToken inBundles:bundles];
+
+    if (shouldCache) {
+        [self.classNamesCache setObject:classes forKey:key];
+    }
+
+    return classes;
+}
+
+- (NSMutableArray<FLEXMethod*> *)methodsForKeyPath:(TBKeyPath *)keyPath {
+    // Only cache if no wildcard, but check cache anyway bc I'm lazy
+    NSMutableArray<FLEXMethod*> *cached = [self.methodsCache objectForKey:keyPath];
+    if (cached) {
+        return cached;
+    }
+
+    NSMutableArray<NSString*> *classes = [self classesForClassToken:keyPath.classKey andBundleToken:keyPath.bundleKey];
+    NSMutableArray<FLEXMethod*> *methods = [[TBRuntime runtime] methodsForToken:keyPath.methodKey
+                                                                     instance:keyPath.instanceMethods
+                                                                    inClasses:classes];
+
+    [methods sortUsingComparator:^NSComparisonResult(FLEXMethod *m1, FLEXMethod *m2) {
+        return [m1.description caseInsensitiveCompare:m2.description];
+    }];
+
+    // Only cache if no wildcard
+    if (keyPath.bundleKey.isAbsolute &&
+        keyPath.classKey.isAbsolute) {
+        [self.methodsCache setObject:methods forKey:keyPath];
+    }
+
+    return methods;
+}
+
+@end

+ 42 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPath.h

@@ -0,0 +1,42 @@
+//
+//  TBKeyPath.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBToken.h"
+@class FLEXMethod;
+
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A key path represents a query into a set of bundles or classes
+/// for a set of one or more methods. It is composed of three tokens:
+/// bundle, class, and method. A key path may be incomplete if it
+/// is missing any of the tokens. A key path is considered "absolute"
+/// if all tokens have no options and if methodKey.string begins
+/// with a + or a -.
+///
+/// The @code TBKeyPathTokenizer @endcode class is used to create
+/// a key path from a string.
+@interface TBKeyPath : NSObject
+
+/// @param method must start with either a wildcard or a + or -.
++ (instancetype)bundle:(TBToken *)bundle
+                 class:(TBToken *)cls
+                method:(TBToken *)method
+            isInstance:(NSNumber *)instance
+                string:(NSString *)keyPathString;
+
+@property (nonatomic, nullable, readonly) TBToken *bundleKey;
+@property (nonatomic, nullable, readonly) TBToken *classKey;
+@property (nonatomic, nullable, readonly) TBToken *methodKey;
+
+/// Indicates whether the method token specifies instance methods.
+/// Nil if not specified.
+@property (nonatomic, nullable, readonly) NSNumber *instanceMethods;
+
+@end
+NS_ASSUME_NONNULL_END

+ 57 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPath.m

@@ -0,0 +1,57 @@
+//
+//  TBKeyPath.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyPath.h"
+
+
+@interface TBKeyPath () {
+    NSString *tb_description;
+}
+@end
+
+@implementation TBKeyPath
+
++ (instancetype)bundle:(TBToken *)bundle
+                 class:(TBToken *)cls
+                method:(TBToken *)method
+            isInstance:(NSNumber *)instance
+                string:(NSString *)keyPathString {
+    TBKeyPath *keyPath  = [self new];
+    keyPath->_bundleKey = bundle;
+    keyPath->_classKey  = cls;
+    keyPath->_methodKey = method;
+
+    keyPath->_instanceMethods = instance;
+
+    // Remove irrelevant trailing '*' for equality purposes
+    if ([keyPathString hasSuffix:@"*"]) {
+        keyPathString = [keyPathString substringToIndex:keyPathString.length];
+    }
+    keyPath->tb_description = keyPathString;
+
+    return keyPath;
+}
+
+- (NSString *)description {
+    return tb_description;
+}
+
+- (NSUInteger)hash {
+    return tb_description.hash;
+}
+
+- (BOOL)isEqual:(id)object {
+    if ([object isKindOfClass:[TBKeyPath class]]) {
+        TBKeyPath *kp = object;
+        return [tb_description isEqualToString:kp->tb_description];
+    }
+
+    return NO;
+}
+
+@end

+ 36 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathSearchController.h

@@ -0,0 +1,36 @@
+//
+//  TBKeyPathSearchController.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/23/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import "TBKeyPathToolbar.h"
+#import "FLEXMethod.h"
+
+
+@protocol TBKeyPathSearchControllerDelegate <NSObject>
+@property (nonatomic, readonly) UITableView *tableView;
+@property (nonatomic, readonly) UISearchBar *searchBar;
+@property (nonatomic, readonly) UINavigationController *navigationController;
+
+@property (nonatomic, readonly) NSString *longPressItemSELPrefix;
+
+- (void)didSelectMethod:(FLEXMethod *)method;
+
+@end
+
+@interface TBKeyPathSearchController : NSObject <UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate>
+
++ (instancetype)delegate:(id<TBKeyPathSearchControllerDelegate>)delegate;
+
+@property (nonatomic) TBKeyPathToolbar *toolbar;
+
+- (void)longPressedRect:(CGRect)rect at:(NSIndexPath *)indexPath;
+- (void)didSelectKeyPathOption:(NSString *)text;
+- (void)didSelectSuperclass:(NSString *)name;
+- (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar;
+
+@end

+ 305 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathSearchController.m

@@ -0,0 +1,305 @@
+//
+//  TBKeyPathSearchController.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/23/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyPathSearchController.h"
+#import "TBKeyPathTokenizer.h"
+#import "TBRuntimeController.h"
+//#import "TBCodeFontCell.h"
+#import "NSString+KeyPaths.h"
+#import "Categories.h"
+
+#import "TBConfigureHookViewController.h"
+#import "TBTweakManager.h"
+#import "TBMethodHook.h"
+
+
+@interface TBKeyPathSearchController ()
+@property (nonatomic, readonly, weak) id<TBKeyPathSearchControllerDelegate> delegate;
+@property (nonatomic, readonly) NSTimer *timer;
+@property (nonatomic) NSArray<NSString*> *bundlesOrClasses;
+@property (nonatomic) TBKeyPath *keyPath;
+
+// We use this when the target class is not absolute
+@property (nonatomic) NSArray<FLEXMethod*> *methods;
+
+// We use these when the target class is absolute and has superclasses.
+// Contrary to the name, superclasses contains the origin class name as well.
+@property (nonatomic) NSArray<NSString*> *superclasses;
+@property (nonatomic) NSDictionary<NSString*, NSArray*> *classesToMethods;
+@end
+
+#warning TODO there's no code to handle refreshing the table after manually appending ".bar" to "Bundle"
+@implementation TBKeyPathSearchController
+
++ (instancetype)delegate:(id<TBKeyPathSearchControllerDelegate>)delegate {
+    TBKeyPathSearchController *controller = [self new];
+    controller->_bundlesOrClasses = [TBRuntimeController allBundleNames];
+    controller->_delegate         = delegate;
+
+    NSParameterAssert(delegate.tableView);
+    NSParameterAssert(delegate.searchBar);
+
+    delegate.tableView.delegate   = controller;
+    delegate.tableView.dataSource = controller;
+    delegate.searchBar.delegate   = controller;
+
+    return controller;
+}
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
+    if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
+        [self.delegate.searchBar resignFirstResponder];
+    }
+}
+
+#pragma mark Long press on class cell
+
+- (void)longPressedRect:(CGRect)rect at:(NSIndexPath *)indexPath {
+    UIMenuController *menuController = [UIMenuController sharedMenuController];
+    menuController.menuItems = [self menuItemsForRow:indexPath.row];
+    if (menuController.menuItems) {
+        [self.delegate.searchBar resignFirstResponder];
+        [menuController setTargetRect:rect inView:self.delegate.tableView];
+        [menuController setMenuVisible:YES animated:YES];
+    }
+}
+
+- (NSArray *)menuItemsForRow:(NSUInteger)row {
+    if (!self.keyPath.methodKey && self.keyPath.classKey) {
+        NSArray<NSString*> *superclasses = [self superclassesOf:self.bundlesOrClasses[row]];
+
+        // Map to UIMenuItems, will delegate call into didSelectKeyPathOption:
+        return [superclasses flex_mapped:^id(NSString *cls, NSUInteger idx) {
+            NSString *sel = [self.delegate.longPressItemSELPrefix stringByAppendingString:cls];
+            return [[UIMenuItem alloc] initWithTitle:cls action:NSSelectorFromString(sel)];
+        }];
+    }
+
+    return nil;
+}
+
+- (NSArray<NSString*> *)superclassesOf:(NSString *)className {
+    Class baseClass = NSClassFromString(className);
+
+    // Find superclasses
+    NSMutableArray<NSString*> *superclasses = [NSMutableArray array];
+    while ([baseClass superclass]) {
+        [superclasses addObject:NSStringFromClass([baseClass superclass])];
+        baseClass = [baseClass superclass];
+    }
+
+    return superclasses;
+}
+
+#pragma mark Key path stuff
+
+- (void)didSelectSuperclass:(NSString *)name {
+    NSString *bundle = [TBRuntimeController shortBundleNameForClass:name];
+    bundle = [bundle stringByReplacingOccurrencesOfString:@"." withString:@"\\."];
+    NSString *newText = [NSString stringWithFormat:@"%@.%@.", bundle, name];
+    self.delegate.searchBar.text = newText;
+
+    // Update list
+    self.keyPath = [TBKeyPathTokenizer tokenizeString:newText];
+    [self didSelectAbsoluteClass:name];
+    [self updateTable];
+}
+
+- (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.searchBar.text;
+    NSString *keyPath = [orig stringByReplacingLastKeyPathComponent:text];
+    self.delegate.searchBar.text = keyPath;
+
+    self.keyPath = [TBKeyPathTokenizer tokenizeString:keyPath];
+
+    // Get superclasses if class was selected
+    if (self.keyPath.classKey.isAbsolute && self.keyPath.methodKey.isAny) {
+        [self didSelectAbsoluteClass:text];
+    } else {
+        self.superclasses = nil;
+    }
+
+    [self updateTable];
+}
+
+- (void)didSelectMethod:(FLEXMethod *)method {
+    // If the user selects a method implemented only by a superclass,
+    // we're going to be adding a method. We need to take the given
+    // method and change it's target class to the base class.
+    Class target = NSClassFromString(self.keyPath.classKey.string);
+    if (self.keyPath.classKey.isAbsolute && method.targetClass != target) {
+        #warning TODO clean this up
+        method = [FLEXMethod method:method.objc_method class:target isInstanceMethod:method.isInstanceMethod];
+    }
+
+    [self.delegate didSelectMethod:method];
+}
+
+- (void)didSelectAbsoluteClass:(NSString *)name {
+    NSMutableArray *superclasses = [NSMutableArray array];
+    [superclasses addObject:name];
+    [superclasses addObjectsFromArray:[self superclassesOf:name]];
+    self.superclasses     = superclasses;
+    self.bundlesOrClasses = nil;
+    self.methods          = nil;
+}
+
+- (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar {
+    UITextField *field = [searchBar valueForKey:@"_searchField"];
+
+    if ([self searchBar:searchBar shouldChangeTextInRange:field.selectedRange replacementText:text]) {
+        [field replaceRange:field.selectedTextRange withText:text];
+    }
+}
+
+#pragma mark - Filtering + UISearchBarDelegate
+
+- (void)updateTable {
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+        if (self.superclasses) {
+            // Compute methods list, reload table
+            self.classesToMethods = [TBRuntimeController methodsForToken:_keyPath.methodKey
+                                                                instance:_keyPath.instanceMethods
+                                                               inClasses:_superclasses];
+            dispatch_async(dispatch_get_main_queue(), ^{
+                [self.delegate.tableView reloadData];
+            });
+        }
+        else {
+            NSArray *models = [TBRuntimeController dataForKeyPath:_keyPath];
+
+            dispatch_async(dispatch_get_main_queue(), ^{
+                if (_keyPath.methodKey) {
+                    _bundlesOrClasses = nil;
+                    _methods = models;
+                } else {
+                    _bundlesOrClasses = models;
+                    _methods = nil;
+                }
+
+                [self.delegate.tableView reloadData];
+            });
+        }
+    });
+}
+
+- (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
+    // Check if character is even legal
+    if (![TBKeyPathTokenizer allowedInKeyPath:text]) {
+        return NO;
+    }
+
+    // Actually parse input
+    @try {
+        text = [searchBar.text stringByReplacingCharactersInRange:range withString:text] ?: text;
+        self.keyPath = [TBKeyPathTokenizer tokenizeString:text];
+    } @catch (id e) {
+        return NO;
+    }
+
+    return YES;
+}
+
+- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
+    [_timer invalidate];
+
+    // Update toolbar buttons
+    [self.toolbar setKeyPath:self.keyPath animated:YES];
+
+    // Schedule update timer
+    if (searchText.length) {
+        if (!self.keyPath.methodKey) {
+            self.superclasses = nil;
+        }
+
+        _timer = [NSTimer fireSecondsFromNow:0.15 block:^{
+            [self updateTable];
+        }];
+    }
+    // ... or remove all rows
+    else {
+        _bundlesOrClasses = [TBRuntimeController allBundleNames];
+        _methods = nil;
+        [self.delegate.tableView reloadData];
+    }
+}
+
+- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
+    [_timer invalidate];
+    [searchBar resignFirstResponder];
+    [self updateTable];
+}
+
+#pragma mark UITableViewDataSource
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
+    return _superclasses.count ?: 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+    if (_superclasses) {
+        return _classesToMethods[_superclasses[section]].count;
+    }
+
+    NSArray *models = (id)_bundlesOrClasses ?: (id)_methods;
+    return models.count;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+    UITableViewCell *cell = [TBCodeFontCell dequeue:tableView indexPath:indexPath];
+    if (self.bundlesOrClasses) {
+        cell.accessoryType        = UITableViewCellAccessoryNone;
+        cell.textLabel.text       = self.bundlesOrClasses[indexPath.row];
+        cell.detailTextLabel.text = nil;
+    }
+    else if (self.superclasses) {
+        NSString *className       = self.superclasses[indexPath.section];
+        FLEXMethod *method          = self.classesToMethods[className][indexPath.row];
+        cell.accessoryType        = UITableViewCellAccessoryDisclosureIndicator;
+        cell.textLabel.text       = method.fullName;
+        cell.detailTextLabel.text = method.selectorString;
+    }
+    else {
+        cell.accessoryType        = UITableViewCellAccessoryDisclosureIndicator;
+        cell.textLabel.text       = self.methods[indexPath.row].fullName;
+        cell.detailTextLabel.text = self.methods[indexPath.row].selectorString;
+    }
+
+    return cell;
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
+    if (self.superclasses) {
+        return [self.superclasses[section] stringByAppendingString:@" methods"];
+    }
+
+    return nil;
+}
+
+#pragma mark UITableViewDelegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+    if (self.bundlesOrClasses) {
+        tableView.contentOffset = CGPointMake(0, - self.delegate.searchBar.frame.size.height - 20);
+        [self didSelectKeyPathOption:self.bundlesOrClasses[indexPath.row]];
+    } else {
+        if (self.superclasses) {
+            NSString *superclass = self.superclasses[indexPath.section];
+            [self didSelectMethod:self.classesToMethods[superclass][indexPath.row]];
+        } else {
+            assert(indexPath.section == 0);
+            [self didSelectMethod:self.methods[indexPath.row]];
+        }
+    }
+}
+
+@end
+

+ 19 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathTokenizer.h

@@ -0,0 +1,19 @@
+//
+//  TBKeyPathTokenizer.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyPath.h"
+
+
+@interface TBKeyPathTokenizer : NSObject
+
++ (NSUInteger)tokenCountOfString:(NSString *)userInput;
++ (TBKeyPath *)tokenizeString:(NSString *)userInput;
+
++ (BOOL)allowedInKeyPath:(NSString *)text;
+
+@end

+ 205 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathTokenizer.m

@@ -0,0 +1,205 @@
+//
+//  TBKeyPathTokenizer.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyPathTokenizer.h"
+
+#define TBCountOfStringOccurence(target, str) ([target componentsSeparatedByString:str].count - 1)
+
+
+@implementation TBKeyPathTokenizer
+
+#pragma mark Initialization
+
+static NSCharacterSet *firstAllowed      = nil;
+static NSCharacterSet *identifierAllowed = nil;
+static NSCharacterSet *filenameAllowed   = nil;
+static NSCharacterSet *keyPathDisallowed = nil;
+static NSCharacterSet *methodAllowed     = nil;
++ (void)initialize {
+    if (self == [self class]) {
+        NSString *_methodFirstAllowed    = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
+        NSString *_identifierAllowed     = [_methodFirstAllowed stringByAppendingString:@"1234567890"];
+        NSString *_methodAllowedSansType = [_identifierAllowed stringByAppendingString:@":"];
+        NSString *_filenameNameAllowed   = [_identifierAllowed stringByAppendingString:@"-+?!"];
+        firstAllowed      = [NSCharacterSet characterSetWithCharactersInString:_methodFirstAllowed];
+        identifierAllowed = [NSCharacterSet characterSetWithCharactersInString:_identifierAllowed];
+        filenameAllowed   = [NSCharacterSet characterSetWithCharactersInString:_filenameNameAllowed];
+        methodAllowed     = [NSCharacterSet characterSetWithCharactersInString:_methodAllowedSansType];
+
+        NSString *_kpDisallowed = [_identifierAllowed stringByAppendingString:@"-+:\\.*"];
+        keyPathDisallowed = [NSCharacterSet characterSetWithCharactersInString:_kpDisallowed].invertedSet;
+    }
+}
+
+#pragma mark Public
+
++ (TBKeyPath *)tokenizeString:(NSString *)userInput {
+    if (!userInput.length) {
+        return nil;
+    }
+
+    NSUInteger tokens = [self tokenCountOfString:userInput];
+    if (tokens == 0) {
+        return nil;
+    }
+
+    if ([userInput containsString:@"**"]) {
+        @throw NSInternalInconsistencyException;
+    }
+
+    NSNumber *instance = nil;
+    NSScanner *scanner = [NSScanner scannerWithString:userInput];
+    TBToken *bundle    = [self scanToken:scanner allowed:filenameAllowed first:filenameAllowed];
+    TBToken *cls       = [self scanToken:scanner allowed:identifierAllowed first:firstAllowed];
+    TBToken *method    = tokens > 2 ? [self scanMethodToken:scanner instance:&instance] : nil;
+
+    return [TBKeyPath bundle:bundle
+                       class:cls
+                      method:method
+                  isInstance:instance
+                      string:userInput];
+}
+
++ (BOOL)allowedInKeyPath:(NSString *)text {
+    if (!text.length) {
+        return YES;
+    }
+    
+    return [text rangeOfCharacterFromSet:keyPathDisallowed].location == NSNotFound;
+}
+
+#pragma mark Private
+
++ (NSUInteger)tokenCountOfString:(NSString *)userInput {
+    NSUInteger escapedCount = TBCountOfStringOccurence(userInput, @"\\.");
+    NSUInteger tokenCount  = TBCountOfStringOccurence(userInput, @".") - escapedCount + 1;
+
+    return tokenCount;
+}
+
++ (TBToken *)scanToken:(NSScanner *)scanner allowed:(NSCharacterSet *)allowedChars first:(NSCharacterSet *)first {
+    if (scanner.isAtEnd) {
+        if ([scanner.string hasSuffix:@"."]) {
+            return [TBToken string:nil options:TBWildcardOptionsAny];
+        }
+        return nil;
+    }
+
+    TBWildcardOptions options = TBWildcardOptionsNone;
+    NSMutableString *token = [NSMutableString string];
+
+    // Token cannot start with '.'
+    if ([scanner scanString:@"." intoString:nil]) {
+        @throw NSInternalInconsistencyException;
+    }
+
+    if ([scanner scanString:@"*." intoString:nil]) {
+        options = TBWildcardOptionsAny;
+        return [TBToken string:nil options:TBWildcardOptionsAny];
+    } else if ([scanner scanString:@"*" intoString:nil]) {
+        if (scanner.isAtEnd) {
+            options = TBWildcardOptionsAny;
+            return [TBToken string:nil options:TBWildcardOptionsAny];
+        }
+        
+        options |= TBWildcardOptionsPrefix;
+    }
+
+    NSString *tmp = nil;
+    BOOL stop = NO, didScanDelimiter = NO, didScanFirstAllowed = NO;
+    NSCharacterSet *disallowed = allowedChars.invertedSet;
+    while (!stop && ![scanner scanString:@"." intoString:&tmp] && !scanner.isAtEnd) {
+        // Scan word chars
+        if (!didScanFirstAllowed) {
+            if ([scanner scanCharactersFromSet:first intoString:&tmp]) {
+                [token appendString:tmp];
+                didScanFirstAllowed = YES;
+            } else {
+                // Token starts with a number or something else not allowed
+                @throw NSInternalInconsistencyException;
+            }
+        } else if ([scanner scanCharactersFromSet:allowedChars intoString:&tmp]) {
+            [token appendString:tmp];
+        }
+        // Scan '\.'
+        else if ([scanner scanString:@"\\" intoString:nil]) {
+            if ([scanner scanString:@"." intoString:nil]) {
+                [token appendString:@"."];
+            } else {
+                // Invalid token, forward slash not followed by period
+                @throw NSInternalInconsistencyException;
+            }
+        }
+        // Scan '*.'
+        else if ([scanner scanString:@"*." intoString:nil]) {
+            options |= TBWildcardOptionsSuffix;
+            stop = YES;
+            didScanDelimiter = YES;
+        }
+        // Scan '*' not followed by .
+        else if ([scanner scanString:@"*" intoString:nil]) {
+            if (!scanner.isAtEnd) {
+                // Invalid token, wildcard in middle of token
+                @throw NSInternalInconsistencyException;
+            }
+        } else if ([scanner scanCharactersFromSet:disallowed intoString:nil]) {
+            // Invalid token, invalid characters
+            @throw NSInternalInconsistencyException;
+        }
+    }
+
+    if ([tmp isEqualToString:@"."]) {
+        didScanDelimiter = YES;
+    }
+
+    if (!didScanDelimiter) {
+        options |= TBWildcardOptionsSuffix;
+    }
+
+    return [TBToken string:token options:options];
+}
+
++ (TBToken *)scanMethodToken:(NSScanner *)scanner instance:(NSNumber **)instance {
+    if (scanner.isAtEnd) {
+        if ([scanner.string hasSuffix:@"."]) {
+            return [TBToken string:nil options:TBWildcardOptionsAny];
+        }
+        return nil;
+    }
+
+    if ([scanner.string hasSuffix:@"."] && ![scanner.string hasSuffix:@"\\."]) {
+        // Methods cannot end with '.' except for '\.'
+        @throw NSInternalInconsistencyException;
+    }
+    
+    if ([scanner scanString:@"-" intoString:nil]) {
+        *instance = @YES;
+    } else if ([scanner scanString:@"+" intoString:nil]) {
+        *instance = @NO;
+    } else {
+        if ([scanner scanString:@"*" intoString:nil]) {
+            // Just checking... It has to start with one of these three!
+            scanner.scanLocation--;
+        } else {
+            @throw NSInternalInconsistencyException;
+        }
+    }
+
+    // -*foo not allowed
+    if (*instance && [scanner scanString:@"*" intoString:nil]) {
+        @throw NSInternalInconsistencyException;
+    }
+
+    if (scanner.isAtEnd) {
+        return [TBToken string:@"" options:TBWildcardOptionsSuffix];
+    }
+
+    return [self scanToken:scanner allowed:methodAllowed first:firstAllowed];
+}
+
+@end

+ 19 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathToolbar.h

@@ -0,0 +1,19 @@
+//
+//  TBKeyPathToolbar.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 6/11/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyboardToolbar.h"
+#import "TBKeyPath.h"
+
+
+@interface TBKeyPathToolbar : TBKeyboardToolbar
+
++ (instancetype)toolbarWithHandler:(TBToolbarAction)tapHandler;
+
+- (void)setKeyPath:(TBKeyPath *)keyPath animated:(BOOL)animated;
+
+@end

+ 87 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathToolbar.m

@@ -0,0 +1,87 @@
+//
+//  TBKeyPathToolbar.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 6/11/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyPathToolbar.h"
+#import "TBKeyPathTokenizer.h"
+
+
+@interface TBKeyPathToolbar ()
+@property (nonatomic, copy) TBToolbarAction tapHandler;
+@end
+
+@implementation TBKeyPathToolbar
+
++ (instancetype)toolbarWithHandler:(TBToolbarAction)tapHandler {
+    TBKeyPath *emptyKeyPath = [TBKeyPathTokenizer tokenizeString:@""];
+    NSArray *buttons = [self buttonsForKeyPath:emptyKeyPath handler:tapHandler];
+
+    TBKeyPathToolbar *me = [self toolbarWithButtons:buttons];
+    me.tapHandler = tapHandler;
+    return me;
+}
+
++ (NSArray<TBToolbarButton*> *)buttonsForKeyPath:(TBKeyPath *)keyPath handler:(TBToolbarAction)handler {
+    NSMutableArray *buttons = [NSMutableArray array];
+    TBToken *lastKey = nil;
+    BOOL lastKeyIsMethod = NO;
+
+    if (keyPath.methodKey) {
+        lastKey = keyPath.methodKey;
+        lastKeyIsMethod = YES;
+    } else {
+        lastKey = keyPath.classKey ?: keyPath.bundleKey;
+    }
+
+    switch (lastKey.options) {
+        case TBWildcardOptionsNone:
+        case TBWildcardOptionsAny:
+            if (lastKeyIsMethod) {
+                if (!keyPath.instanceMethods) {
+                    [buttons addObject:[TBToolbarButton buttonWithTitle:@"-" action:handler]];
+                    [buttons addObject:[TBToolbarButton buttonWithTitle:@"+" action:handler]];
+                }
+                [buttons addObject:[TBToolbarButton buttonWithTitle:@"*" action:handler]];
+            } else {
+                [buttons addObject:[TBToolbarButton buttonWithTitle:@"*." action:handler]];
+                [buttons addObject:[TBToolbarButton buttonWithTitle:@"*" action:handler]];
+                [buttons addObject:[TBToolbarButton buttonWithTitle:@"." action:handler]];
+            }
+            break;
+
+        default: {
+            if (lastKey.options & TBWildcardOptionsPrefix) {
+                if (lastKeyIsMethod) {
+                    if (lastKey.string.length) {
+                        [buttons addObject:[TBToolbarButton buttonWithTitle:@"*" action:handler]];
+                    }
+                } else {
+                    if (lastKey.string.length) {
+                        [buttons addObject:[TBToolbarButton buttonWithTitle:@"*." action:handler]];
+                    }
+                    [buttons addObject:[TBToolbarButton buttonWithTitle:@"." action:handler]];
+                }
+            }
+
+            else if (lastKey.options & TBWildcardOptionsSuffix) {
+                if (!lastKeyIsMethod) {
+                    [buttons addObject:[TBToolbarButton buttonWithTitle:@"*." action:handler]];
+                    [buttons addObject:[TBToolbarButton buttonWithTitle:@"." action:handler]];
+                }
+            }
+        }
+    }
+
+    return buttons;
+}
+
+- (void)setKeyPath:(TBKeyPath *)keyPath animated:(BOOL)animated {
+    NSArray *buttons = [TBKeyPathToolbar buttonsForKeyPath:keyPath handler:self.tapHandler];
+    [self setButtons:buttons animated:animated];
+}
+
+@end

+ 14 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathViewController.h

@@ -0,0 +1,14 @@
+//
+//  TBKeyPathViewController.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/23/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+
+@interface TBKeyPathViewController : UIViewController
+
+@end

+ 123 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyPathViewController.m

@@ -0,0 +1,123 @@
+//
+//  TBKeyPathViewController.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/23/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBKeyPathViewController.h"
+#import "TBKeyPathSearchController.h"
+#import "TBKeyPathToolbar.h"
+#import "UIGestureRecognizer+Blocks.h"
+//#import "TBCodeFontCell.h"
+
+
+@interface TBKeyPathViewController () <TBKeyPathSearchControllerDelegate>
+
+@property (nonatomic, readonly ) TBKeyPathSearchController *searchController;
+@property (nonatomic, readonly ) UIView *promptView;
+@property (nonatomic, readwrite) UITableView *tableView;
+@property (nonatomic, readwrite) UISearchBar *searchBar;
+
+// .@property (nonatomic, readonly) void (^callback)();
+
+@end
+
+@implementation TBKeyPathViewController
+@dynamic navigationController;
+
+#pragma mark - Setup, view events
+
+- (void)loadView {
+    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
+    self.searchBar = [UISearchBar new];
+    self.view = self.tableView;
+    [self.searchBar sizeToFit];
+
+    self.tableView.tableHeaderView = self.searchBar;
+}
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+
+    self.title = @"Choose Hook";
+
+    // Search controller stuff
+    _searchController = [TBKeyPathSearchController delegate:self];
+    _searchController.toolbar = [TBKeyPathToolbar toolbarWithHandler:^(NSString *buttonTitle) {
+        [self.searchController didPressButton:buttonTitle insertInto:self.searchBar];
+    }];
+
+    // Search bar stuff
+    self.searchBar.delegate    = self.searchController;
+    self.searchBar.placeholder = @"UIKit*.UIView.-setFrame:";
+    self.searchBar.inputAccessoryView = self.searchController.toolbar;
+
+    // Table view stuff
+    self.tableView.rowHeight = UITableViewAutomaticDimension;
+//    [self.tableView registerCell:[TBCodeFontCell class]];
+
+    // Long press gesture for classes
+    [self.tableView addGestureRecognizer:[UILongPressGestureRecognizer action:^(UIGestureRecognizer *gesture) {
+        if (gesture.state == UIGestureRecognizerStateBegan) {
+            NSIndexPath *ip = [self.tableView indexPathForRowAtPoint:[gesture locationInView:self.tableView]];
+            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:ip];
+            [self.searchController longPressedRect:cell.frame at:ip];
+        }
+    }]];
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+    [super viewWillAppear:animated];
+    [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
+    [self.searchBar becomeFirstResponder];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+    [super viewWillDisappear:animated];
+    [self.searchBar resignFirstResponder];
+}
+
+#pragma mark Delegate stuff
+
+- (void)didSelectMethod:(FLEXMethod *)method {
+
+}
+
+
+#pragma mark Long press action
+
+- (NSString *)longPressItemSELPrefix { return @"tb_"; }
+
+- (void)didSelectSuperclass:(NSString *)title {
+    [self.searchController didSelectSuperclass:title];
+}
+
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
+    return [@((char*)(void*)action) hasPrefix:self.longPressItemSELPrefix];
+}
+
+- (BOOL)canBecomeFirstResponder {
+    return YES;
+}
+
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
+    if ([super methodSignatureForSelector:sel]) {
+        return [super methodSignatureForSelector:sel];
+    }
+
+    return [super methodSignatureForSelector:@selector(didSelectSuperclass:)];
+}
+
+- (void)forwardInvocation:(NSInvocation *)invocation {
+    NSString *title = NSStringFromSelector([invocation selector]);
+    NSRange match = [title rangeOfString:self.longPressItemSELPrefix];
+    if (match.location == 0) {
+        [self didSelectSuperclass:[title substringFromIndex:3]];
+    } else {
+        [super forwardInvocation:invocation];
+    }
+}
+
+@end

+ 23 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyboardToolbar.h

@@ -0,0 +1,23 @@
+//
+//  TBKeyboardToolbar.h
+//
+//  Created by Rudd Fawcett on 12/3/13.
+//  Copyright (c) 2013 Rudd Fawcett. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#include <AvailabilityMacros.h>
+
+#import "TBToolbarButton.h"
+
+
+@interface TBKeyboardToolbar : UIView
+
++ (instancetype)toolbarWithButtons:(NSArray *)buttons;
+
+- (void)setButtons:(NSArray<TBToolbarButton*> *)buttons animated:(BOOL)animated;
+
+@property (nonatomic) NSArray<TBToolbarButton*> *buttons;
+@property (nonatomic) UIKeyboardAppearance appearance;
+
+@end

+ 172 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBKeyboardToolbar.m

@@ -0,0 +1,172 @@
+//
+//  TBKeyboardToolbar.m
+//
+//  Created by Rudd Fawcett on 12/3/13.
+//  Copyright (c) 2013 Rudd Fawcett. All rights reserved.
+//
+
+#import "TBKeyboardToolbar.h"
+
+#define kToolbarHeight 44
+
+
+@interface TBKeyboardToolbar ()
+
+/// The fake top border to replicate the toolbar.
+@property (nonatomic) CALayer      *topBorder;
+@property (nonatomic) UIView       *toolbarView;
+@property (nonatomic) UIScrollView *scrollView;
+@property (nonatomic) UIVisualEffectView *blurView;
+@end
+
+@implementation TBKeyboardToolbar
+
++ (instancetype)toolbarWithButtons:(NSArray *)buttons {
+    return [[self alloc] initWithButtons:buttons];
+}
+
+- (id)initWithButtons:(NSArray *)buttons {
+    self = [super initWithFrame:CGRectMake(0, 0, self.window.rootViewController.view.bounds.size.width, kToolbarHeight)];
+    if (self) {
+        _buttons = [buttons copy];
+        
+        self.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
+        self.appearance = UIKeyboardAppearanceLight;
+    }
+    
+    return self;
+}
+
+- (void)setAppearance:(UIKeyboardAppearance)appearance {
+    _appearance = appearance;
+    
+    if (self.toolbarView) {
+        [self.toolbarView removeFromSuperview];
+    }
+    
+    [self addSubview:self.inputAccessoryView];
+}
+
+- (void)layoutSubviews {
+    CGRect frame = _toolbarView.bounds;
+    frame.size.height = 0.5f;
+    
+    _topBorder.frame = frame;
+}
+
+- (UIView *)inputAccessoryView {
+    _topBorder       = [CALayer layer];
+    _topBorder.frame = CGRectMake(0.0f, 0.0f, self.bounds.size.width, 0.5f);
+    
+    switch (_appearance) {
+        case UIKeyboardAppearanceDefault:
+        case UIKeyboardAppearanceLight: {
+            _toolbarView = [UIView new];
+            _toolbarView.backgroundColor = [UIColor colorWithRed:0.799 green:0.814 blue:0.847 alpha:1.000];
+            _topBorder.backgroundColor   = [UIColor clearColor].CGColor;
+            [_toolbarView.layer addSublayer:_topBorder];
+            [_toolbarView addSubview:[self fakeToolbar]];
+            break;
+        }
+        case UIKeyboardAppearanceDark: {
+            UIVisualEffect *darkBlur = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
+            _blurView = [[UIVisualEffectView alloc] initWithEffect:darkBlur];
+            _toolbarView = _blurView;
+            _topBorder.backgroundColor = [UIColor colorWithWhite:0.100 alpha:1.000].CGColor;
+            [_blurView.contentView.layer addSublayer:_topBorder];
+            [_blurView.contentView addSubview:[self fakeToolbar]];
+            break;
+        }
+    }
+    
+    _toolbarView.frame = CGRectMake(0, 0, self.bounds.size.width, kToolbarHeight);
+    _toolbarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    
+    return _toolbarView;
+}
+
+- (UIScrollView *)fakeToolbar {
+    _scrollView                  = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, kToolbarHeight)];
+    _scrollView.backgroundColor  = [UIColor clearColor];
+    _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    _scrollView.contentInset     = UIEdgeInsetsMake(8.0f, 0.0f, 4.0f, 6.0f);
+    _scrollView.showsHorizontalScrollIndicator = NO;
+    
+    [self addButtons];
+    
+    return _scrollView;
+}
+
+- (void)addButtons {
+    NSUInteger spacing = 6;
+    NSUInteger originX = spacing;
+    
+    CGRect originFrame;
+    CGFloat top    = _scrollView.contentInset.top;
+    CGFloat bottom = _scrollView.contentInset.bottom;
+    
+    for (TBToolbarButton *button in _buttons) {
+        button.appearance = self.appearance;
+        
+        originFrame             = button.frame;
+        originFrame.origin.x    = originX;
+        originFrame.origin.y    = 0;
+        originFrame.size.height = kToolbarHeight - (top + bottom);
+        button.frame            = originFrame;
+        
+        [_scrollView addSubview:button];
+        
+        originX += button.bounds.size.width + spacing;
+    }
+    
+    CGSize contentSize = _scrollView.contentSize;
+    contentSize.width  = originX - spacing;
+    _scrollView.contentSize = contentSize;
+}
+
+- (void)setButtons:(NSArray<TBToolbarButton*> *)buttons {
+    [_buttons makeObjectsPerformSelector:@selector(removeFromSuperview)];
+    _buttons = buttons.copy;
+    
+    [self addButtons];
+}
+
+- (void)setButtons:(NSArray<TBToolbarButton*> *)buttons animated:(BOOL)animated {
+    if (!animated) {
+        self.buttons = buttons;
+        return;
+    }
+    
+    NSMutableSet *buttonstoRemove = [NSMutableSet setWithArray:_buttons];
+    [buttonstoRemove minusSet:[NSSet setWithArray:buttons]];
+
+    NSMutableSet *buttonsToAdd = [NSMutableSet setWithArray:buttons];
+    [buttonsToAdd minusSet:[NSSet setWithArray:_buttons]];
+
+    if (!buttonstoRemove.count && !buttonsToAdd.count) {
+        return;
+    }
+
+    // New buttons are invisible at first
+    for (TBToolbarButton *button in buttons) {
+        button.alpha = 0;
+    }
+
+    [UIView animateWithDuration:0.1 animations:^{
+        // Fade out old buttons
+        for (TBToolbarButton *button in _buttons) {
+            button.alpha = 0;
+        }
+    } completion:^(BOOL finished) {
+        // Remove old, add new
+        self.buttons = buttons;
+        [UIView animateWithDuration:0.1 animations:^{
+            // Fade in new buttons
+            for (TBToolbarButton *button in buttons) {
+                button.alpha = 1;
+            }
+        }];
+    }];
+}
+
+@end

+ 34 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBToken.h

@@ -0,0 +1,34 @@
+//
+//  TBToken.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+
+typedef NS_OPTIONS(NSUInteger, TBWildcardOptions)
+{
+    TBWildcardOptionsNone   = 0,
+    TBWildcardOptionsAny    = 1,
+    TBWildcardOptionsPrefix = 1 << 1,
+    TBWildcardOptionsSuffix = 1 << 2,
+};
+
+/// A token may contain wildcards at one or either end,
+/// but not in the middle of the token (as of now).
+@interface TBToken : NSObject
+
++ (instancetype)string:(NSString *)string options:(TBWildcardOptions)options;
+
+/// Will not contain the wildcard (*) symbol
+@property (nonatomic, readonly) NSString *string;
+@property (nonatomic, readonly) TBWildcardOptions options;
+
+/// Opposite of "is ambiguous"
+@property (nonatomic, readonly) BOOL isAbsolute;
+@property (nonatomic, readonly) BOOL isAny;
+
+@end

+ 74 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBToken.m

@@ -0,0 +1,74 @@
+//
+//  TBToken.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/22/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "TBToken.h"
+
+
+@interface TBToken () {
+    NSString *tb_description;
+}
+@end
+@implementation TBToken
+
++ (instancetype)string:(NSString *)string options:(TBWildcardOptions)options {
+    TBToken *token  = [self new];
+    token->_string  = string;
+    token->_options = options;
+    return token;
+}
+
+- (BOOL)isAbsolute {
+    return _options == TBWildcardOptionsNone;
+}
+
+- (BOOL)isAny {
+    return _options == TBWildcardOptionsAny;
+}
+
+- (NSString *)description {
+    if (tb_description) {
+        return tb_description;
+    }
+
+    switch (_options) {
+        case TBWildcardOptionsNone:
+            tb_description = _string;
+            break;
+        case TBWildcardOptionsAny:
+            tb_description = @"*";
+            break;
+        default: {
+            NSMutableString *desc = [NSMutableString string];
+            if (_options & TBWildcardOptionsPrefix) {
+                [desc appendString:@"*"];
+            }
+            [desc appendString:_string];
+            if (_options & TBWildcardOptionsSuffix) {
+                [desc appendString:@"*"];
+            }
+            tb_description = desc;
+        }
+    }
+
+    return tb_description;
+}
+
+- (NSUInteger)hash {
+    return self.description.hash;
+}
+
+- (BOOL)isEqual:(id)object {
+    if ([object isKindOfClass:[TBToken class]]) {
+        TBToken *token = object;
+        return [_string isEqualToString:token->_string] && _options == token->_options;
+    }
+
+    return NO;
+}
+
+@end

+ 27 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBToolbarButton.h

@@ -0,0 +1,27 @@
+//
+//  TBToolbarButton.h
+//
+//  Created by Rudd Fawcett on 12/3/13.
+//  Copyright (c) 2013 Rudd Fawcett. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+typedef void (^TBToolbarAction)(NSString *buttonTitle);
+
+
+@interface TBToolbarButton : UIButton
+
+@property (nonatomic) UIKeyboardAppearance appearance;
+
++ (instancetype)buttonWithTitle:(NSString *)title;
++ (instancetype)buttonWithTitle:(NSString *)title action:(TBToolbarAction)eventHandler;
++ (instancetype)buttonWithTitle:(NSString *)title action:(TBToolbarAction)action forControlEvents:(UIControlEvents)controlEvents;
+
+/// Adds the event handler for the button.
+///
+/// @param eventHandler The event handler block.
+/// @param controlEvent The type of event.
+- (void)addEventHandler:(TBToolbarAction)eventHandler forControlEvents:(UIControlEvents)controlEvents;
+
+@end

+ 100 - 0
Classes/GlobalStateExplorers/RuntimeBrowser/TBToolbarButton.m

@@ -0,0 +1,100 @@
+//
+//  TBToolbarButton.m
+//
+//  Created by Rudd Fawcett on 12/3/13.
+//  Copyright (c) 2013 Rudd Fawcett. All rights reserved.
+//
+
+#import "TBToolbarButton.h"
+#import "UIFont+FLEX.h"
+
+
+@interface TBToolbarButton ()
+@property (nonatomic      ) NSString *title;
+@property (nonatomic, copy) TBToolbarAction buttonPressBlock;
+@end
+
+@implementation TBToolbarButton
+
++ (instancetype)buttonWithTitle:(NSString *)title {
+    return [[self alloc] initWithTitle:title];
+}
+
++ (instancetype)buttonWithTitle:(NSString *)title action:(TBToolbarAction)eventHandler forControlEvents:(UIControlEvents)controlEvent {
+    TBToolbarButton *newButton = [TBToolbarButton buttonWithTitle:title];
+    [newButton addEventHandler:eventHandler forControlEvents:controlEvent];
+    return newButton;
+}
+
++ (instancetype)buttonWithTitle:(NSString *)title action:(TBToolbarAction)eventHandler {
+    return [self buttonWithTitle:title action:eventHandler forControlEvents:UIControlEventTouchUpInside];
+}
+
+- (id)initWithTitle:(NSString *)title {
+    self = [super init];
+    if (self) {
+        _title = title;
+        self.layer.cornerRadius = 5.0f;
+        self.layer.borderWidth  = 1.0f;
+        self.titleLabel.font    = [UIFont flex_codeFont];
+        [self setTitle:self.title forState:UIControlStateNormal];
+        [self sizeToFit];
+        CGRect frame = self.frame;
+        frame.size.width  += 40;
+        frame.size.height += 10;
+        self.frame = frame;
+        self.appearance = UIKeyboardAppearanceLight;
+    }
+    
+    return self;
+}
+
+- (void)addEventHandler:(TBToolbarAction)eventHandler forControlEvents:(UIControlEvents)controlEvent {
+    self.buttonPressBlock = eventHandler;
+    [self addTarget:self action:@selector(buttonPressed) forControlEvents:controlEvent];
+}
+
+- (void)buttonPressed {
+    self.buttonPressBlock(self.title);
+}
+
+- (void)setAppearance:(UIKeyboardAppearance)appearance {
+    _appearance = appearance;
+    
+    switch (_appearance) {
+        case UIKeyboardAppearanceDefault:
+        case UIKeyboardAppearanceLight:
+            self.backgroundColor      = [UIColor whiteColor];
+            self.layer.borderColor    = [UIColor colorWithWhite:1.000 alpha:0.500].CGColor;
+            self.titleLabel.textColor = [UIColor blackColor];
+            [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
+            break;
+        case UIKeyboardAppearanceDark:
+            self.backgroundColor      = [UIColor colorWithWhite:0.336 alpha:1.000];
+            self.layer.borderColor    = [UIColor clearColor].CGColor;
+            self.titleLabel.textColor = [UIColor whiteColor];
+            [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
+            break;
+            
+        default:
+            self.backgroundColor      = [UIColor colorWithWhite:0.9 alpha:1.0];
+            self.layer.borderColor    = [UIColor colorWithWhite:0.8 alpha:1.0].CGColor;
+            self.titleLabel.textColor = [UIColor colorWithWhite:0.5 alpha:1.0];
+            [self setTitleColor:[UIColor colorWithWhite:0.5 alpha:1.0] forState:UIControlStateNormal];
+            break;
+    }
+}
+
+- (BOOL)isEqual:(id)object {
+    if ([object isKindOfClass:[TBToolbarButton class]]) {
+        return [self.title isEqualToString:[object title]];
+    }
+
+    return NO;
+}
+
+- (NSUInteger)hash {
+    return self.title.hash;
+}
+
+@end

+ 16 - 0
Classes/Utility/Categories/NSString+KeyPaths.h

@@ -0,0 +1,16 @@
+//
+//  NSString+KeyPaths.h
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/26/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface NSString (KeyPaths)
+
+- (NSString *)stringByRemovingLastKeyPathComponent;
+- (NSString *)stringByReplacingLastKeyPathComponent:(NSString *)replacement;
+
+@end

+ 85 - 0
Classes/Utility/Categories/NSString+KeyPaths.m

@@ -0,0 +1,85 @@
+//
+//  NSString+KeyPaths.m
+//  TBTweakViewController
+//
+//  Created by Tanner on 3/26/17.
+//  Copyright © 2017 Tanner Bennett. All rights reserved.
+//
+
+#import "NSString+KeyPaths.h"
+
+@interface NSMutableString (Replacement)
+- (void)replaceOccurencesOfString:(NSString *)string with:(NSString *)replacement;
+- (void)removeLastKeyPathComponent;
+@end
+
+@implementation NSMutableString (Replacement)
+
+- (void)replaceOccurencesOfString:(NSString *)string with:(NSString *)replacement {
+    [self replaceOccurrencesOfString:string withString:replacement options:0 range:NSMakeRange(0, self.length)];
+}
+
+- (void)removeLastKeyPathComponent {
+    if (![self containsString:@"."]) {
+        [self deleteCharactersInRange:NSMakeRange(0, self.length)];
+        return;
+    }
+
+    BOOL putEscapesBack = NO;
+    if ([self containsString:@"\\."]) {
+        [self replaceOccurencesOfString:@"\\." with:@"\\~"];
+
+        // Case like "UIKit\.framework"
+        if (![self containsString:@"."]) {
+            [self deleteCharactersInRange:NSMakeRange(0, self.length)];
+            return;
+        }
+
+        putEscapesBack = YES;
+    }
+
+    // Case like "Bund" or "Bundle.cla"
+    if (![self hasSuffix:@"."]) {
+        NSUInteger len = self.pathExtension.length;
+        [self deleteCharactersInRange:NSMakeRange(self.length-len, len)];
+    }
+
+    if (putEscapesBack) {
+        [self replaceOccurencesOfString:@"\\~" with:@"\\."];
+    }
+}
+
+@end
+
+@implementation NSString (KeyPaths)
+
+- (NSString *)stringByRemovingLastKeyPathComponent {
+    if (![self containsString:@"."]) {
+        return @"";
+    }
+
+    NSMutableString *mself = self.mutableCopy;
+    [mself removeLastKeyPathComponent];
+    return mself;
+}
+
+- (NSString *)stringByReplacingLastKeyPathComponent:(NSString *)replacement {
+    // replacement should not have any escaped '.' in it,
+    // so we escape all '.'
+    if ([replacement containsString:@"."]) {
+        replacement = [replacement stringByReplacingOccurrencesOfString:@"." withString:@"\\."];
+    }
+
+    // Case like "Foo"
+    if (![self containsString:@"."]) {
+        return [replacement stringByAppendingString:@"."];
+    }
+
+    NSMutableString *mself = self.mutableCopy;
+    [mself removeLastKeyPathComponent];
+    [mself appendString:replacement];
+    [mself appendString:@"."];
+    return mself;
+}
+
+@end

+ 21 - 0
Classes/Utility/Categories/UIGestureRecognizer+Blocks.h

@@ -0,0 +1,21 @@
+//
+//  UIGestureRecognizer+Blocks.h
+//  FLEX
+//
+//  Created by Tanner Bennett on 12/20/19.
+//Copyright © 2019 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+typedef void (^GestureBlock)(UIGestureRecognizer *gesture);
+
+
+@interface UIGestureRecognizer (Blocks)
+
++ (instancetype)action:(GestureBlock)action;
+
+@property (nonatomic) GestureBlock action;
+
+@end
+

+ 36 - 0
Classes/Utility/Categories/UIGestureRecognizer+Blocks.m

@@ -0,0 +1,36 @@
+//
+//  UIGestureRecognizer+Blocks.m
+//  FLEX
+//
+//  Created by Tanner Bennett on 12/20/19.
+//Copyright © 2019 Flipboard. All rights reserved.
+//
+
+#import "UIGestureRecognizer+Blocks.h"
+#import <objc/runtime.h>
+
+
+@implementation UIGestureRecognizer (Blocks)
+
+static void * actionKey;
+
++ (instancetype)action:(GestureBlock)action {
+    UIGestureRecognizer *gesture = [[self alloc] initWithTarget:nil action:nil];
+    [gesture addTarget:gesture action:@selector(tb_invoke)];
+    gesture.action = action;
+    return gesture;
+}
+
+- (void)tb_invoke {
+    self.action(self);
+}
+
+- (GestureBlock)action {
+    return objc_getAssociatedObject(self, &actionKey);
+}
+
+- (void)setAction:(GestureBlock)action {
+    objc_setAssociatedObject(self, &actionKey, action, OBJC_ASSOCIATION_COPY);
+}
+
+@end

+ 0 - 2
Classes/Utility/Runtime/FLEXClassBuilder.m

@@ -7,8 +7,6 @@
 //  Copyright (c) 2015 Tanner Bennett. All rights reserved.
 //
 
-//#import "MirrorKit.h"
-//#import "NSString+ObjcRuntime.h"
 #import "FLEXClassBuilder.h"
 #import "FLEXProperty.h"
 #import "FLEXMethodBase.h"

+ 0 - 2
Classes/Utility/Runtime/FLEXMirror.m

@@ -13,8 +13,6 @@
 #import "FLEXIvar.h"
 #import "FLEXProtocol.h"
 #import "FLEXUtility.h"
-//#import "MirrorKit.h"
-//#import "NSObject+Reflection.h"
 
 
 #pragma mark FLEXMirror

+ 114 - 0
FLEX.xcodeproj/project.pbxproj

@@ -204,8 +204,32 @@
 		C38EF26323A2FCD20047A7EC /* FLEXViewControllerShortcuts.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF26123A2FCD20047A7EC /* FLEXViewControllerShortcuts.h */; };
 		C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = C38F3F2F230C958F004E3731 /* FLEXAlert.h */; };
 		C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = C38F3F30230C958F004E3731 /* FLEXAlert.m */; };
+		C398624D23AD6C67007E6793 /* TBKeyPathSearchController.h in Headers */ = {isa = PBXBuildFile; fileRef = C398624323AD6C67007E6793 /* TBKeyPathSearchController.h */; };
+		C398624E23AD6C67007E6793 /* TBKeyPath.m in Sources */ = {isa = PBXBuildFile; fileRef = C398624423AD6C67007E6793 /* TBKeyPath.m */; };
+		C398624F23AD6C67007E6793 /* TBKeyPathTokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = C398624523AD6C67007E6793 /* TBKeyPathTokenizer.h */; };
+		C398625023AD6C67007E6793 /* TBKeyPathViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C398624623AD6C67007E6793 /* TBKeyPathViewController.m */; };
+		C398625123AD6C67007E6793 /* TBKeyPath.h in Headers */ = {isa = PBXBuildFile; fileRef = C398624723AD6C67007E6793 /* TBKeyPath.h */; };
+		C398625223AD6C67007E6793 /* TBKeyPathSearchController.m in Sources */ = {isa = PBXBuildFile; fileRef = C398624823AD6C67007E6793 /* TBKeyPathSearchController.m */; };
+		C398625323AD6C67007E6793 /* TBKeyPathTokenizer.m in Sources */ = {isa = PBXBuildFile; fileRef = C398624923AD6C67007E6793 /* TBKeyPathTokenizer.m */; };
+		C398625423AD6C67007E6793 /* TBKeyPathToolbar.h in Headers */ = {isa = PBXBuildFile; fileRef = C398624A23AD6C67007E6793 /* TBKeyPathToolbar.h */; };
+		C398625523AD6C67007E6793 /* TBKeyPathViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C398624B23AD6C67007E6793 /* TBKeyPathViewController.h */; };
+		C398625623AD6C67007E6793 /* TBKeyPathToolbar.m in Sources */ = {isa = PBXBuildFile; fileRef = C398624C23AD6C67007E6793 /* TBKeyPathToolbar.m */; };
+		C398625923AD6C88007E6793 /* TBToken.h in Headers */ = {isa = PBXBuildFile; fileRef = C398625723AD6C88007E6793 /* TBToken.h */; };
+		C398625A23AD6C88007E6793 /* TBToken.m in Sources */ = {isa = PBXBuildFile; fileRef = C398625823AD6C88007E6793 /* TBToken.m */; };
 		C398625D23AD6E90007E6793 /* UIFont+FLEX.h in Headers */ = {isa = PBXBuildFile; fileRef = C398625B23AD6E90007E6793 /* UIFont+FLEX.h */; };
 		C398625E23AD6E90007E6793 /* UIFont+FLEX.m in Sources */ = {isa = PBXBuildFile; fileRef = C398625C23AD6E90007E6793 /* UIFont+FLEX.m */; };
+		C398626123AD70DF007E6793 /* TBKeyboardToolbar.h in Headers */ = {isa = PBXBuildFile; fileRef = C398625F23AD70DF007E6793 /* TBKeyboardToolbar.h */; };
+		C398626223AD70DF007E6793 /* TBKeyboardToolbar.m in Sources */ = {isa = PBXBuildFile; fileRef = C398626023AD70DF007E6793 /* TBKeyboardToolbar.m */; };
+		C398626523AD70F5007E6793 /* TBToolbarButton.h in Headers */ = {isa = PBXBuildFile; fileRef = C398626323AD70F5007E6793 /* TBToolbarButton.h */; };
+		C398626623AD70F5007E6793 /* TBToolbarButton.m in Sources */ = {isa = PBXBuildFile; fileRef = C398626423AD70F5007E6793 /* TBToolbarButton.m */; };
+		C398626B23AD71C1007E6793 /* TBRuntime.h in Headers */ = {isa = PBXBuildFile; fileRef = C398626723AD71C1007E6793 /* TBRuntime.h */; };
+		C398626C23AD71C1007E6793 /* TBRuntimeController.h in Headers */ = {isa = PBXBuildFile; fileRef = C398626823AD71C1007E6793 /* TBRuntimeController.h */; };
+		C398626D23AD71C1007E6793 /* TBRuntimeController.m in Sources */ = {isa = PBXBuildFile; fileRef = C398626923AD71C1007E6793 /* TBRuntimeController.m */; };
+		C398626E23AD71C1007E6793 /* TBRuntime.m in Sources */ = {isa = PBXBuildFile; fileRef = C398626A23AD71C1007E6793 /* TBRuntime.m */; };
+		C398627223AD7951007E6793 /* UIGestureRecognizer+Blocks.h in Headers */ = {isa = PBXBuildFile; fileRef = C398627023AD7951007E6793 /* UIGestureRecognizer+Blocks.h */; };
+		C398627323AD7951007E6793 /* UIGestureRecognizer+Blocks.m in Sources */ = {isa = PBXBuildFile; fileRef = C398627123AD7951007E6793 /* UIGestureRecognizer+Blocks.m */; };
+		C398627623AD79B7007E6793 /* NSString+KeyPaths.h in Headers */ = {isa = PBXBuildFile; fileRef = C398627423AD79B6007E6793 /* NSString+KeyPaths.h */; };
+		C398627723AD79B7007E6793 /* NSString+KeyPaths.m in Sources */ = {isa = PBXBuildFile; fileRef = C398627523AD79B7007E6793 /* NSString+KeyPaths.m */; };
 		C398682523AC359600E9E391 /* FLEXShortcutsFactory+Defaults.m in Sources */ = {isa = PBXBuildFile; fileRef = C398682323AC359600E9E391 /* FLEXShortcutsFactory+Defaults.m */; };
 		C398682623AC359600E9E391 /* FLEXShortcutsFactory+Defaults.h in Headers */ = {isa = PBXBuildFile; fileRef = C398682423AC359600E9E391 /* FLEXShortcutsFactory+Defaults.h */; };
 		C398682823AC36EC00E9E391 /* FLEXViewShortcuts.m in Sources */ = {isa = PBXBuildFile; fileRef = C3F527C4231891F6009CBA07 /* FLEXViewShortcuts.m */; };
@@ -464,8 +488,32 @@
 		C38EF26123A2FCD20047A7EC /* FLEXViewControllerShortcuts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXViewControllerShortcuts.h; sourceTree = "<group>"; };
 		C38F3F2F230C958F004E3731 /* FLEXAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXAlert.h; sourceTree = "<group>"; };
 		C38F3F30230C958F004E3731 /* FLEXAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXAlert.m; sourceTree = "<group>"; };
+		C398624323AD6C67007E6793 /* TBKeyPathSearchController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyPathSearchController.h; sourceTree = "<group>"; };
+		C398624423AD6C67007E6793 /* TBKeyPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBKeyPath.m; sourceTree = "<group>"; };
+		C398624523AD6C67007E6793 /* TBKeyPathTokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyPathTokenizer.h; sourceTree = "<group>"; };
+		C398624623AD6C67007E6793 /* TBKeyPathViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBKeyPathViewController.m; sourceTree = "<group>"; };
+		C398624723AD6C67007E6793 /* TBKeyPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyPath.h; sourceTree = "<group>"; };
+		C398624823AD6C67007E6793 /* TBKeyPathSearchController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBKeyPathSearchController.m; sourceTree = "<group>"; };
+		C398624923AD6C67007E6793 /* TBKeyPathTokenizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBKeyPathTokenizer.m; sourceTree = "<group>"; };
+		C398624A23AD6C67007E6793 /* TBKeyPathToolbar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyPathToolbar.h; sourceTree = "<group>"; };
+		C398624B23AD6C67007E6793 /* TBKeyPathViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyPathViewController.h; sourceTree = "<group>"; };
+		C398624C23AD6C67007E6793 /* TBKeyPathToolbar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBKeyPathToolbar.m; sourceTree = "<group>"; };
+		C398625723AD6C88007E6793 /* TBToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBToken.h; sourceTree = "<group>"; };
+		C398625823AD6C88007E6793 /* TBToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBToken.m; sourceTree = "<group>"; };
 		C398625B23AD6E90007E6793 /* UIFont+FLEX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+FLEX.h"; sourceTree = "<group>"; };
 		C398625C23AD6E90007E6793 /* UIFont+FLEX.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+FLEX.m"; sourceTree = "<group>"; };
+		C398625F23AD70DF007E6793 /* TBKeyboardToolbar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBKeyboardToolbar.h; sourceTree = "<group>"; };
+		C398626023AD70DF007E6793 /* TBKeyboardToolbar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBKeyboardToolbar.m; sourceTree = "<group>"; };
+		C398626323AD70F5007E6793 /* TBToolbarButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBToolbarButton.h; sourceTree = "<group>"; };
+		C398626423AD70F5007E6793 /* TBToolbarButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBToolbarButton.m; sourceTree = "<group>"; };
+		C398626723AD71C1007E6793 /* TBRuntime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBRuntime.h; sourceTree = "<group>"; };
+		C398626823AD71C1007E6793 /* TBRuntimeController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBRuntimeController.h; sourceTree = "<group>"; };
+		C398626923AD71C1007E6793 /* TBRuntimeController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBRuntimeController.m; sourceTree = "<group>"; };
+		C398626A23AD71C1007E6793 /* TBRuntime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBRuntime.m; sourceTree = "<group>"; };
+		C398627023AD7951007E6793 /* UIGestureRecognizer+Blocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIGestureRecognizer+Blocks.h"; sourceTree = "<group>"; };
+		C398627123AD7951007E6793 /* UIGestureRecognizer+Blocks.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIGestureRecognizer+Blocks.m"; sourceTree = "<group>"; };
+		C398627423AD79B6007E6793 /* NSString+KeyPaths.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+KeyPaths.h"; sourceTree = "<group>"; };
+		C398627523AD79B7007E6793 /* NSString+KeyPaths.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+KeyPaths.m"; sourceTree = "<group>"; };
 		C398682323AC359600E9E391 /* FLEXShortcutsFactory+Defaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FLEXShortcutsFactory+Defaults.m"; sourceTree = "<group>"; };
 		C398682423AC359600E9E391 /* FLEXShortcutsFactory+Defaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FLEXShortcutsFactory+Defaults.h"; sourceTree = "<group>"; };
 		C39ED92622D63F3200B5773A /* FLEXAddressExplorerCoordinator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXAddressExplorerCoordinator.h; sourceTree = "<group>"; };
@@ -703,6 +751,7 @@
 		3A4C949A1B5B21410088C3F2 /* GlobalStateExplorers */ = {
 			isa = PBXGroup;
 			children = (
+				C398624223AD6B92007E6793 /* RuntimeBrowser */,
 				71E1C2092307FBB700F5032A /* Keychain */,
 				C3511B8E22D7C9740057BAB7 /* Globals */,
 				779B1EBF1C0C4D7C001F5E49 /* DatabaseBrowser */,
@@ -903,6 +952,8 @@
 			children = (
 				C387C88122E0D24A00750E58 /* UIView+FLEX_Layout.h */,
 				C387C88222E0D24A00750E58 /* UIView+FLEX_Layout.m */,
+				C398627023AD7951007E6793 /* UIGestureRecognizer+Blocks.h */,
+				C398627123AD7951007E6793 /* UIGestureRecognizer+Blocks.m */,
 				C3F646BF239EAA8F00D4A011 /* UIPasteboard+FLEX.h */,
 				C3F646C0239EAA8F00D4A011 /* UIPasteboard+FLEX.m */,
 				C398625B23AD6E90007E6793 /* UIFont+FLEX.h */,
@@ -915,6 +966,8 @@
 				C3F9777E2311B38E0032776D /* NSDictionary+ObjcRuntime.m */,
 				C3F9777D2311B38E0032776D /* NSString+ObjcRuntime.h */,
 				C3F977802311B38F0032776D /* NSString+ObjcRuntime.m */,
+				C398627423AD79B6007E6793 /* NSString+KeyPaths.h */,
+				C398627523AD79B7007E6793 /* NSString+KeyPaths.m */,
 				C3E5D9FB2316E83700E655DB /* FLEXRuntime+Compare.h */,
 				C3E5D9FC2316E83700E655DB /* FLEXRuntime+Compare.m */,
 				C34C9BDB23A7F2740031CA3E /* FLEXRuntime+UIKitHelpers.h */,
@@ -938,6 +991,41 @@
 			path = Core;
 			sourceTree = "<group>";
 		};
+		C398624223AD6B92007E6793 /* RuntimeBrowser */ = {
+			isa = PBXGroup;
+			children = (
+				C398626F23AD71C6007E6793 /* DataSources */,
+				C398625723AD6C88007E6793 /* TBToken.h */,
+				C398625823AD6C88007E6793 /* TBToken.m */,
+				C398624723AD6C67007E6793 /* TBKeyPath.h */,
+				C398624423AD6C67007E6793 /* TBKeyPath.m */,
+				C398624323AD6C67007E6793 /* TBKeyPathSearchController.h */,
+				C398624823AD6C67007E6793 /* TBKeyPathSearchController.m */,
+				C398624523AD6C67007E6793 /* TBKeyPathTokenizer.h */,
+				C398624923AD6C67007E6793 /* TBKeyPathTokenizer.m */,
+				C398624A23AD6C67007E6793 /* TBKeyPathToolbar.h */,
+				C398624C23AD6C67007E6793 /* TBKeyPathToolbar.m */,
+				C398624B23AD6C67007E6793 /* TBKeyPathViewController.h */,
+				C398624623AD6C67007E6793 /* TBKeyPathViewController.m */,
+				C398625F23AD70DF007E6793 /* TBKeyboardToolbar.h */,
+				C398626023AD70DF007E6793 /* TBKeyboardToolbar.m */,
+				C398626323AD70F5007E6793 /* TBToolbarButton.h */,
+				C398626423AD70F5007E6793 /* TBToolbarButton.m */,
+			);
+			path = RuntimeBrowser;
+			sourceTree = "<group>";
+		};
+		C398626F23AD71C6007E6793 /* DataSources */ = {
+			isa = PBXGroup;
+			children = (
+				C398626723AD71C1007E6793 /* TBRuntime.h */,
+				C398626A23AD71C1007E6793 /* TBRuntime.m */,
+				C398626823AD71C1007E6793 /* TBRuntimeController.h */,
+				C398626923AD71C1007E6793 /* TBRuntimeController.m */,
+			);
+			path = DataSources;
+			sourceTree = "<group>";
+		};
 		C3D43E9F231D79F60079F6A8 /* Shortcuts */ = {
 			isa = PBXGroup;
 			children = (
@@ -1010,6 +1098,7 @@
 				3A4C94EB1B5B21410088C3F2 /* FLEXImagePreviewViewController.h in Headers */,
 				C34D4EB823A2B17900C1F903 /* FLEXBundleShortcuts.h in Headers */,
 				C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */,
+				C398624D23AD6C67007E6793 /* TBKeyPathSearchController.h in Headers */,
 				3A4C95381B5B21410088C3F2 /* FLEXNetworkRecorder.h in Headers */,
 				3A4C94251B5B20570088C3F2 /* FLEX.h in Headers */,
 				C3F527C12318670F009CBA07 /* FLEXImageShortcuts.h in Headers */,
@@ -1023,6 +1112,7 @@
 				C37A0C93218BAC9600848CA7 /* FLEXObjcInternal.h in Headers */,
 				3A4C953E1B5B21410088C3F2 /* FLEXNetworkTransactionDetailTableViewController.h in Headers */,
 				3A4C95301B5B21410088C3F2 /* FLEXSystemLogMessage.h in Headers */,
+				C398625523AD6C67007E6793 /* TBKeyPathViewController.h in Headers */,
 				C3E5DA02231700EE00E655DB /* FLEXExplorerSection.h in Headers */,
 				C34D4EB023A2ABD900C1F903 /* FLEXLayerShortcuts.h in Headers */,
 				C3F646F623A04A7500D4A011 /* FLEXShortcut.h in Headers */,
@@ -1030,14 +1120,17 @@
 				3A4C95361B5B21410088C3F2 /* FLEXNetworkHistoryTableViewController.h in Headers */,
 				3A4C94DD1B5B21410088C3F2 /* FLEXHeapEnumerator.h in Headers */,
 				C3F527BD2318603F009CBA07 /* FLEXShortcutsSection.h in Headers */,
+				C398626B23AD71C1007E6793 /* TBRuntime.h in Headers */,
 				3A4C95321B5B21410088C3F2 /* FLEXSystemLogTableViewCell.h in Headers */,
 				C3F977852311B38F0032776D /* NSDictionary+ObjcRuntime.h in Headers */,
+				C398625123AD6C67007E6793 /* TBKeyPath.h in Headers */,
 				3A4C94F91B5B21410088C3F2 /* FLEXArgumentInputNumberView.h in Headers */,
 				C3F646F223A045DB00D4A011 /* FLEXClassShortcuts.h in Headers */,
 				C387C87A22DFCD6A00750E58 /* FLEXCarouselCell.h in Headers */,
 				C3511B9122D7C99E0057BAB7 /* FLEXTableViewSection.h in Headers */,
 				C398625D23AD6E90007E6793 /* UIFont+FLEX.h in Headers */,
 				3A4C953A1B5B21410088C3F2 /* FLEXNetworkSettingsTableViewController.h in Headers */,
+				C398626C23AD71C1007E6793 /* TBRuntimeController.h in Headers */,
 				779B1ED01C0C4D7C001F5E49 /* FLEXMultiColumnTableView.h in Headers */,
 				3A4C94D31B5B21410088C3F2 /* FLEXObjectExplorerFactory.h in Headers */,
 				C38DF0EA22CFE4370077B4AD /* FLEXTableViewController.h in Headers */,
@@ -1046,6 +1139,7 @@
 				3A4C94E31B5B21410088C3F2 /* FLEXRuntimeUtility.h in Headers */,
 				3A4C95341B5B21410088C3F2 /* FLEXSystemLogTableViewController.h in Headers */,
 				C34C9BDD23A7F2740031CA3E /* FLEXRuntime+UIKitHelpers.h in Headers */,
+				C398624F23AD6C67007E6793 /* TBKeyPathTokenizer.h in Headers */,
 				3A4C95091B5B21410088C3F2 /* FLEXFieldEditorView.h in Headers */,
 				C3E5D9FD2316E83700E655DB /* FLEXRuntime+Compare.h in Headers */,
 				C3490E1F233BDD73002AE200 /* FLEXSingleRowSection.h in Headers */,
@@ -1060,9 +1154,11 @@
 				C33E46AF223B02CD004BD0E6 /* FLEXASLLogController.h in Headers */,
 				C34EE30821CB23CC00BD3A7C /* FLEXOSLogController.h in Headers */,
 				3A4C94FF1B5B21410088C3F2 /* FLEXArgumentInputSwitchView.h in Headers */,
+				C398625423AD6C67007E6793 /* TBKeyPathToolbar.h in Headers */,
 				3A4C94E71B5B21410088C3F2 /* FLEXHierarchyTableViewCell.h in Headers */,
 				224D49AA1C673AB5000EAB86 /* FLEXSQLiteDatabaseManager.h in Headers */,
 				3A4C95031B5B21410088C3F2 /* FLEXArgumentInputView.h in Headers */,
+				C398627623AD79B7007E6793 /* NSString+KeyPaths.h in Headers */,
 				94A5151D1C4CA1F10063292F /* FLEXExplorerViewController.h in Headers */,
 				C3F31D3F2267D883003C991A /* FLEXMultilineTableViewCell.h in Headers */,
 				71E1C2132307FBB800F5032A /* FLEXKeychain.h in Headers */,
@@ -1096,9 +1192,11 @@
 				3A4C953C1B5B21410088C3F2 /* FLEXNetworkTransaction.h in Headers */,
 				3A4C952E1B5B21410088C3F2 /* FLEXWebViewController.h in Headers */,
 				C3EE76BF22DFC63600EC0AA0 /* FLEXScopeCarousel.h in Headers */,
+				C398627223AD7951007E6793 /* UIGestureRecognizer+Blocks.h in Headers */,
 				3A4C94E91B5B21410088C3F2 /* FLEXHierarchyTableViewController.h in Headers */,
 				3A4C94F31B5B21410088C3F2 /* FLEXArgumentInputFontView.h in Headers */,
 				3A4C95261B5B21410088C3F2 /* FLEXGlobalsTableViewController.h in Headers */,
+				C398625923AD6C88007E6793 /* TBToken.h in Headers */,
 				C33C825E2316DC8600DD2451 /* FLEXObjectExplorer.h in Headers */,
 				C34D4EB423A2AF2A00C1F903 /* FLEXColorPreviewSection.h in Headers */,
 				224D49A81C673AB5000EAB86 /* FLEXRealmDatabaseManager.h in Headers */,
@@ -1114,10 +1212,12 @@
 				C3F977872311B38F0032776D /* NSObject+Reflection.h in Headers */,
 				C3DB9F642107FC9600B46809 /* FLEXObjectRef.h in Headers */,
 				3A4C95401B5B21410088C3F2 /* FLEXNetworkTransactionTableViewCell.h in Headers */,
+				C398626523AD70F5007E6793 /* TBToolbarButton.h in Headers */,
 				3A4C95241B5B21410088C3F2 /* FLEXFileBrowserTableViewController.h in Headers */,
 				94AAF0381BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.h in Headers */,
 				71E1C2152307FBB800F5032A /* FLEXKeychainQuery.h in Headers */,
 				C36FBFD6230F3B98008D95D5 /* FLEXIvar.h in Headers */,
+				C398626123AD70DF007E6793 /* TBKeyboardToolbar.h in Headers */,
 				94AAF03A1BAF2F0300DE8760 /* FLEXKeyboardShortcutManager.h in Headers */,
 				94A515181C4CA1D70063292F /* FLEXManager+Private.h in Headers */,
 				C39ED92822D63F3200B5773A /* FLEXAddressExplorerCoordinator.h in Headers */,
@@ -1244,8 +1344,10 @@
 				C31C4A6A23342A2200C35F12 /* FLEXMetadataSection.m in Sources */,
 				224D49A91C673AB5000EAB86 /* FLEXRealmDatabaseManager.m in Sources */,
 				C39ED92922D63F3200B5773A /* FLEXAddressExplorerCoordinator.m in Sources */,
+				C398626223AD70DF007E6793 /* TBKeyboardToolbar.m in Sources */,
 				C34C9BDE23A7F2740031CA3E /* FLEXRuntime+UIKitHelpers.m in Sources */,
 				2EF6B04D1D494BE50006BDA5 /* FLEXNetworkCurlLogger.m in Sources */,
+				C398625323AD6C67007E6793 /* TBKeyPathTokenizer.m in Sources */,
 				94A515201C4CA1F10063292F /* FLEXWindow.m in Sources */,
 				C38EF26223A2FCD20047A7EC /* FLEXViewControllerShortcuts.m in Sources */,
 				C398682523AC359600E9E391 /* FLEXShortcutsFactory+Defaults.m in Sources */,
@@ -1260,33 +1362,40 @@
 				C3878DBA23A749960038FDBE /* FLEXVariableEditorViewController.m in Sources */,
 				94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */,
 				3A4C951F1B5B21410088C3F2 /* FLEXClassesTableViewController.m in Sources */,
+				C398624E23AD6C67007E6793 /* TBKeyPath.m in Sources */,
 				C36FBFD4230F3B98008D95D5 /* FLEXProtocol.m in Sources */,
 				C34D4EB123A2ABD900C1F903 /* FLEXLayerShortcuts.m in Sources */,
 				C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */,
 				3A4C95081B5B21410088C3F2 /* FLEXDefaultEditorViewController.m in Sources */,
 				779B1ED11C0C4D7C001F5E49 /* FLEXMultiColumnTableView.m in Sources */,
 				C36FBFD0230F3B98008D95D5 /* FLEXProperty.m in Sources */,
+				C398625623AD6C67007E6793 /* TBKeyPathToolbar.m in Sources */,
 				C3F31D412267D883003C991A /* FLEXMultilineTableViewCell.m in Sources */,
 				3A4C94EE1B5B21410088C3F2 /* FLEXArgumentInputColorView.m in Sources */,
 				C38DF0EB22CFE4370077B4AD /* FLEXTableViewController.m in Sources */,
 				C387C87B22DFCD6A00750E58 /* FLEXCarouselCell.m in Sources */,
 				3A4C94F01B5B21410088C3F2 /* FLEXArgumentInputDateView.m in Sources */,
+				C398627723AD79B7007E6793 /* NSString+KeyPaths.m in Sources */,
 				C36FBFDC230F3B98008D95D5 /* FLEXIvar.m in Sources */,
 				C3F527C22318670F009CBA07 /* FLEXImageShortcuts.m in Sources */,
 				679F64871BD53B7B00A8C94C /* FLEXCookiesTableViewController.m in Sources */,
 				3A4C94CE1B5B21410088C3F2 /* FLEXGlobalsEntry.m in Sources */,
+				C398625223AD6C67007E6793 /* TBKeyPathSearchController.m in Sources */,
 				C3E5D9FE2316E83700E655DB /* FLEXRuntime+Compare.m in Sources */,
 				71E1C2192307FBB800F5032A /* FLEXKeychainQuery.m in Sources */,
 				C3E5DA03231700EE00E655DB /* FLEXExplorerSection.m in Sources */,
+				C398627323AD7951007E6793 /* UIGestureRecognizer+Blocks.m in Sources */,
 				C3F646F723A04A7500D4A011 /* FLEXShortcut.m in Sources */,
 				3A4C94FE1B5B21410088C3F2 /* FLEXArgumentInputStructView.m in Sources */,
 				C3F31D402267D883003C991A /* FLEXSubtitleTableViewCell.m in Sources */,
+				C398626D23AD71C1007E6793 /* TBRuntimeController.m in Sources */,
 				C36FBFD9230F3B98008D95D5 /* FLEXProtocolBuilder.m in Sources */,
 				3A4C95431B5B21410088C3F2 /* FLEXNetworkObserver.m in Sources */,
 				779B1EDB1C0C4D7C001F5E49 /* FLEXTableListViewController.m in Sources */,
 				3A4C94E41B5B21410088C3F2 /* FLEXRuntimeUtility.m in Sources */,
 				3A4C94D41B5B21410088C3F2 /* FLEXObjectExplorerFactory.m in Sources */,
 				94A515171C4CA1D70063292F /* FLEXManager.m in Sources */,
+				C398626623AD70F5007E6793 /* TBToolbarButton.m in Sources */,
 				3A4C952F1B5B21410088C3F2 /* FLEXWebViewController.m in Sources */,
 				3A4C95041B5B21410088C3F2 /* FLEXArgumentInputView.m in Sources */,
 				C3F977842311B38F0032776D /* NSDictionary+ObjcRuntime.m in Sources */,
@@ -1339,6 +1448,8 @@
 				C34D4EB923A2B17900C1F903 /* FLEXBundleShortcuts.m in Sources */,
 				3A4C952D1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.m in Sources */,
 				C33E46B0223B02CD004BD0E6 /* FLEXASLLogController.m in Sources */,
+				C398625023AD6C67007E6793 /* TBKeyPathViewController.m in Sources */,
+				C398626E23AD71C1007E6793 /* TBRuntime.m in Sources */,
 				3A4C953D1B5B21410088C3F2 /* FLEXNetworkTransaction.m in Sources */,
 				3A4C94E81B5B21410088C3F2 /* FLEXHierarchyTableViewCell.m in Sources */,
 				3A4C950A1B5B21410088C3F2 /* FLEXFieldEditorView.m in Sources */,
@@ -1347,6 +1458,7 @@
 				3A4C952B1B5B21410088C3F2 /* FLEXLibrariesTableViewController.m in Sources */,
 				71E1C2182307FBB800F5032A /* FLEXKeychainTableViewController.m in Sources */,
 				94A5151E1C4CA1F10063292F /* FLEXExplorerViewController.m in Sources */,
+				C398625A23AD6C88007E6793 /* TBToken.m in Sources */,
 				3A4C95371B5B21410088C3F2 /* FLEXNetworkHistoryTableViewController.m in Sources */,
 				C3511B9222D7C99E0057BAB7 /* FLEXTableViewSection.m in Sources */,
 				C32A19632317378C00EB02AC /* FLEXDefaultsContentSection.m in Sources */,
@@ -1514,6 +1626,7 @@
 		3A4C94361B5B20570088C3F2 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO;
 				CLANG_WARN_STRICT_PROTOTYPES = NO;
 				CODE_SIGN_IDENTITY = "";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
@@ -1542,6 +1655,7 @@
 		3A4C94371B5B20570088C3F2 /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO;
 				CLANG_WARN_STRICT_PROTOTYPES = NO;
 				CODE_SIGN_IDENTITY = "";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";