├── .gitignore ├── HighlightSelectedString.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── HighlightSelectedString.xccheckout │ └── xcuserdata │ │ ├── chars.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ ├── lixiangyang.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── lixy.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── HighlightSelectedString.xcscheme └── xcuserdata │ ├── chars.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── lixiangyang.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── lixy.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── HighlightSelectedString ├── Aspects.h ├── Aspects.m ├── HighlightSelectedString.h ├── HighlightSelectedString.m ├── Info.plist ├── NSLayoutManager+SelectedSymbols.h └── NSLayoutManager+SelectedSymbols.m ├── README.md ├── demo.png └── menu.png /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepyounger/HighlightSelectedString/f9044434e9e5fe11405abc52e944925fd3346504/.gitignore -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5A3A68F51A72447200AAD00A /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A3A68F41A72447200AAD00A /* AppKit.framework */; }; 11 | 5A3A68F71A72447200AAD00A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A3A68F61A72447200AAD00A /* Foundation.framework */; }; 12 | 5A3A68FC1A72447200AAD00A /* HighlightSelectedString.xcscheme in Resources */ = {isa = PBXBuildFile; fileRef = 5A3A68FB1A72447200AAD00A /* HighlightSelectedString.xcscheme */; }; 13 | 5A3A68FF1A72447200AAD00A /* HighlightSelectedString.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A3A68FE1A72447200AAD00A /* HighlightSelectedString.m */; }; 14 | 5A5613E31C76B6AC00933888 /* NSLayoutManager+SelectedSymbols.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A5613E21C76B6AC00933888 /* NSLayoutManager+SelectedSymbols.m */; }; 15 | 5A745BA91CA26EC800EE4061 /* Aspects.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A745BA81CA26EC800EE4061 /* Aspects.m */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 5A3A68F11A72447200AAD00A /* HighlightSelectedString.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HighlightSelectedString.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 5A3A68F41A72447200AAD00A /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 21 | 5A3A68F61A72447200AAD00A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 22 | 5A3A68FA1A72447200AAD00A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | 5A3A68FB1A72447200AAD00A /* HighlightSelectedString.xcscheme */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = HighlightSelectedString.xcscheme; path = HighlightSelectedString.xcodeproj/xcshareddata/xcschemes/HighlightSelectedString.xcscheme; sourceTree = SOURCE_ROOT; }; 24 | 5A3A68FD1A72447200AAD00A /* HighlightSelectedString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HighlightSelectedString.h; sourceTree = ""; }; 25 | 5A3A68FE1A72447200AAD00A /* HighlightSelectedString.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HighlightSelectedString.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 26 | 5A5613E11C76B6AC00933888 /* NSLayoutManager+SelectedSymbols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSLayoutManager+SelectedSymbols.h"; sourceTree = ""; }; 27 | 5A5613E21C76B6AC00933888 /* NSLayoutManager+SelectedSymbols.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSLayoutManager+SelectedSymbols.m"; sourceTree = ""; }; 28 | 5A745BA71CA26EC800EE4061 /* Aspects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Aspects.h; sourceTree = ""; }; 29 | 5A745BA81CA26EC800EE4061 /* Aspects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Aspects.m; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 5A3A68EF1A72447200AAD00A /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | 5A3A68F51A72447200AAD00A /* AppKit.framework in Frameworks */, 38 | 5A3A68F71A72447200AAD00A /* Foundation.framework in Frameworks */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | 5A3A68E81A72447200AAD00A = { 46 | isa = PBXGroup; 47 | children = ( 48 | 5A3A68F81A72447200AAD00A /* HighlightSelectedString */, 49 | 5A3A68F31A72447200AAD00A /* Frameworks */, 50 | 5A3A68F21A72447200AAD00A /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 5A3A68F21A72447200AAD00A /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 5A3A68F11A72447200AAD00A /* HighlightSelectedString.xcplugin */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 5A3A68F31A72447200AAD00A /* Frameworks */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 5A3A68F41A72447200AAD00A /* AppKit.framework */, 66 | 5A3A68F61A72447200AAD00A /* Foundation.framework */, 67 | ); 68 | name = Frameworks; 69 | sourceTree = ""; 70 | }; 71 | 5A3A68F81A72447200AAD00A /* HighlightSelectedString */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 5A3A68FD1A72447200AAD00A /* HighlightSelectedString.h */, 75 | 5A3A68FE1A72447200AAD00A /* HighlightSelectedString.m */, 76 | 5A5613E11C76B6AC00933888 /* NSLayoutManager+SelectedSymbols.h */, 77 | 5A5613E21C76B6AC00933888 /* NSLayoutManager+SelectedSymbols.m */, 78 | 5A745BA71CA26EC800EE4061 /* Aspects.h */, 79 | 5A745BA81CA26EC800EE4061 /* Aspects.m */, 80 | 5A3A68F91A72447200AAD00A /* Supporting Files */, 81 | ); 82 | path = HighlightSelectedString; 83 | sourceTree = ""; 84 | }; 85 | 5A3A68F91A72447200AAD00A /* Supporting Files */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 5A3A68FA1A72447200AAD00A /* Info.plist */, 89 | 5A3A68FB1A72447200AAD00A /* HighlightSelectedString.xcscheme */, 90 | ); 91 | name = "Supporting Files"; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | 5A3A68F01A72447200AAD00A /* HighlightSelectedString */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = 5A3A69021A72447200AAD00A /* Build configuration list for PBXNativeTarget "HighlightSelectedString" */; 100 | buildPhases = ( 101 | 5A3A68ED1A72447200AAD00A /* Sources */, 102 | 5A3A68EE1A72447200AAD00A /* Resources */, 103 | 5A3A68EF1A72447200AAD00A /* Frameworks */, 104 | 5A4716C51CA22F2A005CA933 /* ShellScript */, 105 | ); 106 | buildRules = ( 107 | ); 108 | dependencies = ( 109 | ); 110 | name = HighlightSelectedString; 111 | productName = HighlightSelectedString; 112 | productReference = 5A3A68F11A72447200AAD00A /* HighlightSelectedString.xcplugin */; 113 | productType = "com.apple.product-type.bundle"; 114 | }; 115 | /* End PBXNativeTarget section */ 116 | 117 | /* Begin PBXProject section */ 118 | 5A3A68E91A72447200AAD00A /* Project object */ = { 119 | isa = PBXProject; 120 | attributes = { 121 | LastUpgradeCheck = 0710; 122 | ORGANIZATIONNAME = lixy; 123 | TargetAttributes = { 124 | 5A3A68F01A72447200AAD00A = { 125 | CreatedOnToolsVersion = 6.1.1; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = 5A3A68EC1A72447200AAD00A /* Build configuration list for PBXProject "HighlightSelectedString" */; 130 | compatibilityVersion = "Xcode 3.2"; 131 | developmentRegion = English; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | ); 136 | mainGroup = 5A3A68E81A72447200AAD00A; 137 | productRefGroup = 5A3A68F21A72447200AAD00A /* Products */; 138 | projectDirPath = ""; 139 | projectRoot = ""; 140 | targets = ( 141 | 5A3A68F01A72447200AAD00A /* HighlightSelectedString */, 142 | ); 143 | }; 144 | /* End PBXProject section */ 145 | 146 | /* Begin PBXResourcesBuildPhase section */ 147 | 5A3A68EE1A72447200AAD00A /* Resources */ = { 148 | isa = PBXResourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 5A3A68FC1A72447200AAD00A /* HighlightSelectedString.xcscheme in Resources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXResourcesBuildPhase section */ 156 | 157 | /* Begin PBXShellScriptBuildPhase section */ 158 | 5A4716C51CA22F2A005CA933 /* ShellScript */ = { 159 | isa = PBXShellScriptBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | ); 163 | inputPaths = ( 164 | ); 165 | outputPaths = ( 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | shellPath = /usr/bin/python; 169 | shellScript = "# coding:utf-8\n# Thanks OMColorSense https://github.com/omz/ColorSense-for-Xcode\n# Get the compatibility UUID from the current version of Xcode and update\n# the plugin's Info.plist accordingly:\n\nimport os\nimport plistlib\nimport subprocess\n\n#获取XCode路径 如果有多个Xcode值获取select的XCode的路径\ndev_path = subprocess.check_output(['xcode-select', '-p']).strip()\n#获取XCode info 的路径\nxcode_info_path = os.path.abspath(os.path.join(dev_path, '../Info'))\n#获取XCode的uuid\ncompat_uuid = subprocess.check_output(['defaults', 'read', xcode_info_path, 'DVTPlugInCompatibilityUUID']).strip()\n#获取插件的路径\ninfo_plist_path = os.path.join(os.getenv('BUILT_PRODUCTS_DIR'), 'HighlightSelectedString.xcplugin/Contents/Info.plist')\n#获取插件的info\ninfo = plistlib.readPlist(info_plist_path)\n#获取当前的uuids\nuuids = info['DVTPlugInCompatibilityUUIDs']\n#判断是否存在当前的uuid\ncounts = uuids.count(compat_uuid)\n#存在就删除\nif counts != 0:\n uuids.remove(compat_uuid)\n#添加当前的uuid\nuuids.append(compat_uuid)\n#重新赋值回去\ninfo['DVTPlugInCompatibilityUUIDs'] = uuids\n#写出到文件\nplistlib.writePlist(info, info_plist_path)\n"; 170 | }; 171 | /* End PBXShellScriptBuildPhase section */ 172 | 173 | /* Begin PBXSourcesBuildPhase section */ 174 | 5A3A68ED1A72447200AAD00A /* Sources */ = { 175 | isa = PBXSourcesBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | 5A3A68FF1A72447200AAD00A /* HighlightSelectedString.m in Sources */, 179 | 5A5613E31C76B6AC00933888 /* NSLayoutManager+SelectedSymbols.m in Sources */, 180 | 5A745BA91CA26EC800EE4061 /* Aspects.m in Sources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXSourcesBuildPhase section */ 185 | 186 | /* Begin XCBuildConfiguration section */ 187 | 5A3A69001A72447200AAD00A /* Debug */ = { 188 | isa = XCBuildConfiguration; 189 | buildSettings = { 190 | ALWAYS_SEARCH_USER_PATHS = NO; 191 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 192 | CLANG_CXX_LIBRARY = "libc++"; 193 | CLANG_ENABLE_MODULES = YES; 194 | CLANG_ENABLE_OBJC_ARC = YES; 195 | CLANG_WARN_BOOL_CONVERSION = YES; 196 | CLANG_WARN_CONSTANT_CONVERSION = YES; 197 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 198 | CLANG_WARN_EMPTY_BODY = YES; 199 | CLANG_WARN_ENUM_CONVERSION = YES; 200 | CLANG_WARN_INT_CONVERSION = YES; 201 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 202 | CLANG_WARN_UNREACHABLE_CODE = YES; 203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 204 | COPY_PHASE_STRIP = NO; 205 | ENABLE_STRICT_OBJC_MSGSEND = YES; 206 | ENABLE_TESTABILITY = YES; 207 | GCC_C_LANGUAGE_STANDARD = gnu99; 208 | GCC_DYNAMIC_NO_PIC = NO; 209 | GCC_OPTIMIZATION_LEVEL = 0; 210 | GCC_PREPROCESSOR_DEFINITIONS = ( 211 | "DEBUG=1", 212 | "$(inherited)", 213 | ); 214 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 217 | GCC_WARN_UNDECLARED_SELECTOR = YES; 218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 219 | GCC_WARN_UNUSED_FUNCTION = YES; 220 | GCC_WARN_UNUSED_VARIABLE = YES; 221 | MTL_ENABLE_DEBUG_INFO = YES; 222 | ONLY_ACTIVE_ARCH = YES; 223 | }; 224 | name = Debug; 225 | }; 226 | 5A3A69011A72447200AAD00A /* Release */ = { 227 | isa = XCBuildConfiguration; 228 | buildSettings = { 229 | ALWAYS_SEARCH_USER_PATHS = NO; 230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 231 | CLANG_CXX_LIBRARY = "libc++"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_WARN_BOOL_CONVERSION = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 237 | CLANG_WARN_EMPTY_BODY = YES; 238 | CLANG_WARN_ENUM_CONVERSION = YES; 239 | CLANG_WARN_INT_CONVERSION = YES; 240 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 241 | CLANG_WARN_UNREACHABLE_CODE = YES; 242 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 243 | COPY_PHASE_STRIP = YES; 244 | ENABLE_NS_ASSERTIONS = NO; 245 | ENABLE_STRICT_OBJC_MSGSEND = YES; 246 | GCC_C_LANGUAGE_STANDARD = gnu99; 247 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 248 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 249 | GCC_WARN_UNDECLARED_SELECTOR = YES; 250 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 251 | GCC_WARN_UNUSED_FUNCTION = YES; 252 | GCC_WARN_UNUSED_VARIABLE = YES; 253 | MTL_ENABLE_DEBUG_INFO = NO; 254 | }; 255 | name = Release; 256 | }; 257 | 5A3A69031A72447200AAD00A /* Debug */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | COMBINE_HIDPI_IMAGES = YES; 261 | DEPLOYMENT_LOCATION = YES; 262 | DSTROOT = "$(HOME)"; 263 | INFOPLIST_FILE = HighlightSelectedString/Info.plist; 264 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 265 | MACOSX_DEPLOYMENT_TARGET = 10.10; 266 | PRODUCT_BUNDLE_IDENTIFIER = "com.lixy.$(PRODUCT_NAME:rfc1034identifier)"; 267 | PRODUCT_NAME = "$(TARGET_NAME)"; 268 | WRAPPER_EXTENSION = xcplugin; 269 | }; 270 | name = Debug; 271 | }; 272 | 5A3A69041A72447200AAD00A /* Release */ = { 273 | isa = XCBuildConfiguration; 274 | buildSettings = { 275 | COMBINE_HIDPI_IMAGES = YES; 276 | DEPLOYMENT_LOCATION = YES; 277 | DSTROOT = "$(HOME)"; 278 | INFOPLIST_FILE = HighlightSelectedString/Info.plist; 279 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 280 | MACOSX_DEPLOYMENT_TARGET = 10.10; 281 | PRODUCT_BUNDLE_IDENTIFIER = "com.lixy.$(PRODUCT_NAME:rfc1034identifier)"; 282 | PRODUCT_NAME = "$(TARGET_NAME)"; 283 | WRAPPER_EXTENSION = xcplugin; 284 | }; 285 | name = Release; 286 | }; 287 | /* End XCBuildConfiguration section */ 288 | 289 | /* Begin XCConfigurationList section */ 290 | 5A3A68EC1A72447200AAD00A /* Build configuration list for PBXProject "HighlightSelectedString" */ = { 291 | isa = XCConfigurationList; 292 | buildConfigurations = ( 293 | 5A3A69001A72447200AAD00A /* Debug */, 294 | 5A3A69011A72447200AAD00A /* Release */, 295 | ); 296 | defaultConfigurationIsVisible = 0; 297 | defaultConfigurationName = Release; 298 | }; 299 | 5A3A69021A72447200AAD00A /* Build configuration list for PBXNativeTarget "HighlightSelectedString" */ = { 300 | isa = XCConfigurationList; 301 | buildConfigurations = ( 302 | 5A3A69031A72447200AAD00A /* Debug */, 303 | 5A3A69041A72447200AAD00A /* Release */, 304 | ); 305 | defaultConfigurationIsVisible = 0; 306 | defaultConfigurationName = Release; 307 | }; 308 | /* End XCConfigurationList section */ 309 | }; 310 | rootObject = 5A3A68E91A72447200AAD00A /* Project object */; 311 | } 312 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/project.xcworkspace/xcshareddata/HighlightSelectedString.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | AD6D9883-5E79-4FF2-8ECA-07A08F729904 9 | IDESourceControlProjectName 10 | HighlightSelectedString 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 42162399AB7E90C17FBC7C2818887F5596B18321 14 | https://github.com/keepyounger/HighlightSelectedString.git 15 | 16 | IDESourceControlProjectPath 17 | HighlightSelectedString.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 42162399AB7E90C17FBC7C2818887F5596B18321 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/keepyounger/HighlightSelectedString.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 42162399AB7E90C17FBC7C2818887F5596B18321 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 42162399AB7E90C17FBC7C2818887F5596B18321 36 | IDESourceControlWCCName 37 | HighlightSelectedString 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/project.xcworkspace/xcuserdata/chars.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepyounger/HighlightSelectedString/f9044434e9e5fe11405abc52e944925fd3346504/HighlightSelectedString.xcodeproj/project.xcworkspace/xcuserdata/chars.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/project.xcworkspace/xcuserdata/lixiangyang.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepyounger/HighlightSelectedString/f9044434e9e5fe11405abc52e944925fd3346504/HighlightSelectedString.xcodeproj/project.xcworkspace/xcuserdata/lixiangyang.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/project.xcworkspace/xcuserdata/lixy.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepyounger/HighlightSelectedString/f9044434e9e5fe11405abc52e944925fd3346504/HighlightSelectedString.xcodeproj/project.xcworkspace/xcuserdata/lixy.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/xcshareddata/xcschemes/HighlightSelectedString.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 61 | 64 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/xcuserdata/chars.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SuppressBuildableAutocreation 6 | 7 | 5A3A68F01A72447200AAD00A 8 | 9 | primary 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/xcuserdata/lixiangyang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/xcuserdata/lixiangyang.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SuppressBuildableAutocreation 6 | 7 | 5A3A68F01A72447200AAD00A 8 | 9 | primary 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/xcuserdata/lixy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /HighlightSelectedString.xcodeproj/xcuserdata/lixy.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SuppressBuildableAutocreation 6 | 7 | 5A3A68F01A72447200AAD00A 8 | 9 | primary 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /HighlightSelectedString/Aspects.h: -------------------------------------------------------------------------------- 1 | // 2 | // Aspects.h 3 | // Aspects - A delightful, simple library for aspect oriented programming. 4 | // 5 | // Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. 6 | // 7 | 8 | #import 9 | 10 | typedef NS_OPTIONS(NSUInteger, AspectOptions) { 11 | AspectPositionAfter = 0, /// Called after the original implementation (default) 12 | AspectPositionInstead = 1, /// Will replace the original implementation. 13 | AspectPositionBefore = 2, /// Called before the original implementation. 14 | 15 | AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution. 16 | }; 17 | 18 | /// Opaque Aspect Token that allows to deregister the hook. 19 | @protocol AspectToken 20 | 21 | /// Deregisters an aspect. 22 | /// @return YES if deregistration is successful, otherwise NO. 23 | - (BOOL)remove; 24 | 25 | @end 26 | 27 | /// The AspectInfo protocol is the first parameter of our block syntax. 28 | @protocol AspectInfo 29 | 30 | /// The instance that is currently hooked. 31 | - (id)instance; 32 | 33 | /// The original invocation of the hooked method. 34 | - (NSInvocation *)originalInvocation; 35 | 36 | /// All method arguments, boxed. This is lazily evaluated. 37 | - (NSArray *)arguments; 38 | 39 | @end 40 | 41 | /** 42 | Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second. 43 | 44 | Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe. 45 | */ 46 | @interface NSObject (Aspects) 47 | 48 | /// Adds a block of code before/instead/after the current `selector` for a specific class. 49 | /// 50 | /// @param block Aspects replicates the type signature of the method being hooked. 51 | /// The first parameter will be `id`, followed by all parameters of the method. 52 | /// These parameters are optional and will be filled to match the block signature. 53 | /// You can even use an empty block, or one that simple gets `id`. 54 | /// 55 | /// @note Hooking static methods is not supported. 56 | /// @return A token which allows to later deregister the aspect. 57 | + (id)aspect_hookSelector:(SEL)selector 58 | withOptions:(AspectOptions)options 59 | usingBlock:(id)block 60 | error:(NSError **)error; 61 | 62 | /// Adds a block of code before/instead/after the current `selector` for a specific instance. 63 | - (id)aspect_hookSelector:(SEL)selector 64 | withOptions:(AspectOptions)options 65 | usingBlock:(id)block 66 | error:(NSError **)error; 67 | 68 | @end 69 | 70 | 71 | typedef NS_ENUM(NSUInteger, AspectErrorCode) { 72 | AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted. 73 | AspectErrorDoesNotRespondToSelector, /// Selector could not be found. 74 | AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed. 75 | AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed. 76 | AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair. 77 | AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called. 78 | AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large. 79 | 80 | AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated. 81 | }; 82 | 83 | extern NSString *const AspectErrorDomain; 84 | -------------------------------------------------------------------------------- /HighlightSelectedString/Aspects.m: -------------------------------------------------------------------------------- 1 | // 2 | // Aspects.m 3 | // Aspects - A delightful, simple library for aspect oriented programming. 4 | // 5 | // Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. 6 | // 7 | 8 | #import "Aspects.h" 9 | #import 10 | #import 11 | #import 12 | 13 | #define AspectLog(...) 14 | //#define AspectLog(...) do { NSLog(__VA_ARGS__); }while(0) 15 | #define AspectLogError(...) do { NSLog(__VA_ARGS__); }while(0) 16 | 17 | // Block internals. 18 | typedef NS_OPTIONS(int, AspectBlockFlags) { 19 | AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), 20 | AspectBlockFlagsHasSignature = (1 << 30) 21 | }; 22 | typedef struct _AspectBlock { 23 | __unused Class isa; 24 | AspectBlockFlags flags; 25 | __unused int reserved; 26 | void (__unused *invoke)(struct _AspectBlock *block, ...); 27 | struct { 28 | unsigned long int reserved; 29 | unsigned long int size; 30 | // requires AspectBlockFlagsHasCopyDisposeHelpers 31 | void (*copy)(void *dst, const void *src); 32 | void (*dispose)(const void *); 33 | // requires AspectBlockFlagsHasSignature 34 | const char *signature; 35 | const char *layout; 36 | } *descriptor; 37 | // imported variables 38 | } *AspectBlockRef; 39 | 40 | @interface AspectInfo : NSObject 41 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation; 42 | @property (nonatomic, unsafe_unretained, readonly) id instance; 43 | @property (nonatomic, strong, readonly) NSArray *arguments; 44 | @property (nonatomic, strong, readonly) NSInvocation *originalInvocation; 45 | @end 46 | 47 | // Tracks a single aspect. 48 | @interface AspectIdentifier : NSObject 49 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error; 50 | - (BOOL)invokeWithInfo:(id)info; 51 | @property (nonatomic, assign) SEL selector; 52 | @property (nonatomic, strong) id block; 53 | @property (nonatomic, strong) NSMethodSignature *blockSignature; 54 | @property (nonatomic, weak) id object; 55 | @property (nonatomic, assign) AspectOptions options; 56 | @end 57 | 58 | // Tracks all aspects for an object/class. 59 | @interface AspectsContainer : NSObject 60 | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition; 61 | - (BOOL)removeAspect:(id)aspect; 62 | - (BOOL)hasAspects; 63 | @property (atomic, copy) NSArray *beforeAspects; 64 | @property (atomic, copy) NSArray *insteadAspects; 65 | @property (atomic, copy) NSArray *afterAspects; 66 | @end 67 | 68 | @interface AspectTracker : NSObject 69 | - (id)initWithTrackedClass:(Class)trackedClass; 70 | @property (nonatomic, strong) Class trackedClass; 71 | @property (nonatomic, readonly) NSString *trackedClassName; 72 | @property (nonatomic, strong) NSMutableSet *selectorNames; 73 | @property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers; 74 | - (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName; 75 | - (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName; 76 | - (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName; 77 | - (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName; 78 | @end 79 | 80 | @interface NSInvocation (Aspects) 81 | - (NSArray *)aspects_arguments; 82 | @end 83 | 84 | #define AspectPositionFilter 0x07 85 | 86 | #define AspectError(errorCode, errorDescription) do { \ 87 | AspectLogError(@"Aspects: %@", errorDescription); \ 88 | if (error) { *error = [NSError errorWithDomain:AspectErrorDomain code:errorCode userInfo:@{NSLocalizedDescriptionKey: errorDescription}]; }}while(0) 89 | 90 | NSString *const AspectErrorDomain = @"AspectErrorDomain"; 91 | static NSString *const AspectsSubclassSuffix = @"_Aspects_"; 92 | static NSString *const AspectsMessagePrefix = @"aspects_"; 93 | 94 | @implementation NSObject (Aspects) 95 | 96 | /////////////////////////////////////////////////////////////////////////////////////////// 97 | #pragma mark - Public Aspects API 98 | 99 | + (id)aspect_hookSelector:(SEL)selector 100 | withOptions:(AspectOptions)options 101 | usingBlock:(id)block 102 | error:(NSError **)error { 103 | return aspect_add((id)self, selector, options, block, error); 104 | } 105 | 106 | /// @return A token which allows to later deregister the aspect. 107 | - (id)aspect_hookSelector:(SEL)selector 108 | withOptions:(AspectOptions)options 109 | usingBlock:(id)block 110 | error:(NSError **)error { 111 | return aspect_add(self, selector, options, block, error); 112 | } 113 | 114 | /////////////////////////////////////////////////////////////////////////////////////////// 115 | #pragma mark - Private Helper 116 | 117 | static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { 118 | NSCParameterAssert(self); 119 | NSCParameterAssert(selector); 120 | NSCParameterAssert(block); 121 | 122 | __block AspectIdentifier *identifier = nil; 123 | aspect_performLocked(^{ 124 | if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { 125 | AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); 126 | identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; 127 | if (identifier) { 128 | [aspectContainer addAspect:identifier withOptions:options]; 129 | 130 | // Modify the class to allow message interception. 131 | aspect_prepareClassAndHookSelector(self, selector, error); 132 | } 133 | } 134 | }); 135 | return identifier; 136 | } 137 | 138 | static BOOL aspect_remove(AspectIdentifier *aspect, NSError **error) { 139 | NSCAssert([aspect isKindOfClass:AspectIdentifier.class], @"Must have correct type."); 140 | 141 | __block BOOL success = NO; 142 | aspect_performLocked(^{ 143 | id self = aspect.object; // strongify 144 | if (self) { 145 | AspectsContainer *aspectContainer = aspect_getContainerForObject(self, aspect.selector); 146 | success = [aspectContainer removeAspect:aspect]; 147 | 148 | aspect_cleanupHookedClassAndSelector(self, aspect.selector); 149 | // destroy token 150 | aspect.object = nil; 151 | aspect.block = nil; 152 | aspect.selector = NULL; 153 | }else { 154 | NSString *errrorDesc = [NSString stringWithFormat:@"Unable to deregister hook. Object already deallocated: %@", aspect]; 155 | AspectError(AspectErrorRemoveObjectAlreadyDeallocated, errrorDesc); 156 | } 157 | }); 158 | return success; 159 | } 160 | 161 | static void aspect_performLocked(dispatch_block_t block) { 162 | static OSSpinLock aspect_lock = OS_SPINLOCK_INIT; 163 | OSSpinLockLock(&aspect_lock); 164 | block(); 165 | OSSpinLockUnlock(&aspect_lock); 166 | } 167 | 168 | static SEL aspect_aliasForSelector(SEL selector) { 169 | NSCParameterAssert(selector); 170 | return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]); 171 | } 172 | 173 | static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) { 174 | AspectBlockRef layout = (__bridge void *)block; 175 | if (!(layout->flags & AspectBlockFlagsHasSignature)) { 176 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; 177 | AspectError(AspectErrorMissingBlockSignature, description); 178 | return nil; 179 | } 180 | void *desc = layout->descriptor; 181 | desc += 2 * sizeof(unsigned long int); 182 | if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { 183 | desc += 2 * sizeof(void *); 184 | } 185 | if (!desc) { 186 | NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; 187 | AspectError(AspectErrorMissingBlockSignature, description); 188 | return nil; 189 | } 190 | const char *signature = (*(const char **)desc); 191 | return [NSMethodSignature signatureWithObjCTypes:signature]; 192 | } 193 | 194 | static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) { 195 | NSCParameterAssert(blockSignature); 196 | NSCParameterAssert(object); 197 | NSCParameterAssert(selector); 198 | 199 | BOOL signaturesMatch = YES; 200 | NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector]; 201 | if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) { 202 | signaturesMatch = NO; 203 | }else { 204 | if (blockSignature.numberOfArguments > 1) { 205 | const char *blockType = [blockSignature getArgumentTypeAtIndex:1]; 206 | if (blockType[0] != '@') { 207 | signaturesMatch = NO; 208 | } 209 | } 210 | // Argument 0 is self/block, argument 1 is SEL or id. We start comparing at argument 2. 211 | // The block can have less arguments than the method, that's ok. 212 | if (signaturesMatch) { 213 | for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) { 214 | const char *methodType = [methodSignature getArgumentTypeAtIndex:idx]; 215 | const char *blockType = [blockSignature getArgumentTypeAtIndex:idx]; 216 | // Only compare parameter, not the optional type data. 217 | if (!methodType || !blockType || methodType[0] != blockType[0]) { 218 | signaturesMatch = NO; break; 219 | } 220 | } 221 | } 222 | } 223 | 224 | if (!signaturesMatch) { 225 | NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature]; 226 | AspectError(AspectErrorIncompatibleBlockSignature, description); 227 | return NO; 228 | } 229 | return YES; 230 | } 231 | 232 | /////////////////////////////////////////////////////////////////////////////////////////// 233 | #pragma mark - Class + Selector Preparation 234 | 235 | static BOOL aspect_isMsgForwardIMP(IMP impl) { 236 | return impl == _objc_msgForward 237 | #if !defined(__arm64__) 238 | || impl == (IMP)_objc_msgForward_stret 239 | #endif 240 | ; 241 | } 242 | 243 | static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) { 244 | IMP msgForwardIMP = _objc_msgForward; 245 | #if !defined(__arm64__) 246 | // As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id. 247 | // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html 248 | // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783 249 | // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4) 250 | Method method = class_getInstanceMethod(self.class, selector); 251 | const char *encoding = method_getTypeEncoding(method); 252 | BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B; 253 | if (methodReturnsStructValue) { 254 | @try { 255 | NSUInteger valueSize = 0; 256 | NSGetSizeAndAlignment(encoding, &valueSize, NULL); 257 | 258 | if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) { 259 | methodReturnsStructValue = NO; 260 | } 261 | } @catch (__unused NSException *e) {} 262 | } 263 | if (methodReturnsStructValue) { 264 | msgForwardIMP = (IMP)_objc_msgForward_stret; 265 | } 266 | #endif 267 | return msgForwardIMP; 268 | } 269 | 270 | static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { 271 | NSCParameterAssert(selector); 272 | Class klass = aspect_hookClass(self, error); 273 | Method targetMethod = class_getInstanceMethod(klass, selector); 274 | IMP targetMethodIMP = method_getImplementation(targetMethod); 275 | if (!aspect_isMsgForwardIMP(targetMethodIMP)) { 276 | // Make a method alias for the existing method implementation, it not already copied. 277 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 278 | SEL aliasSelector = aspect_aliasForSelector(selector); 279 | if (![klass instancesRespondToSelector:aliasSelector]) { 280 | __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); 281 | NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 282 | } 283 | 284 | // We use forwardInvocation to hook in. 285 | class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); 286 | AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 287 | } 288 | } 289 | 290 | // Will undo the runtime changes made. 291 | static void aspect_cleanupHookedClassAndSelector(NSObject *self, SEL selector) { 292 | NSCParameterAssert(self); 293 | NSCParameterAssert(selector); 294 | 295 | Class klass = object_getClass(self); 296 | BOOL isMetaClass = class_isMetaClass(klass); 297 | if (isMetaClass) { 298 | klass = (Class)self; 299 | } 300 | 301 | // Check if the method is marked as forwarded and undo that. 302 | Method targetMethod = class_getInstanceMethod(klass, selector); 303 | IMP targetMethodIMP = method_getImplementation(targetMethod); 304 | if (aspect_isMsgForwardIMP(targetMethodIMP)) { 305 | // Restore the original method implementation. 306 | const char *typeEncoding = method_getTypeEncoding(targetMethod); 307 | SEL aliasSelector = aspect_aliasForSelector(selector); 308 | Method originalMethod = class_getInstanceMethod(klass, aliasSelector); 309 | IMP originalIMP = method_getImplementation(originalMethod); 310 | NSCAssert(originalMethod, @"Original implementation for %@ not found %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); 311 | 312 | class_replaceMethod(klass, selector, originalIMP, typeEncoding); 313 | AspectLog(@"Aspects: Removed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); 314 | } 315 | 316 | // Deregister global tracked selector 317 | aspect_deregisterTrackedSelector(self, selector); 318 | 319 | // Get the aspect container and check if there are any hooks remaining. Clean up if there are not. 320 | AspectsContainer *container = aspect_getContainerForObject(self, selector); 321 | if (!container.hasAspects) { 322 | // Destroy the container 323 | aspect_destroyContainerForObject(self, selector); 324 | 325 | // Figure out how the class was modified to undo the changes. 326 | NSString *className = NSStringFromClass(klass); 327 | if ([className hasSuffix:AspectsSubclassSuffix]) { 328 | Class originalClass = NSClassFromString([className stringByReplacingOccurrencesOfString:AspectsSubclassSuffix withString:@""]); 329 | NSCAssert(originalClass != nil, @"Original class must exist"); 330 | object_setClass(self, originalClass); 331 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(originalClass)); 332 | 333 | // We can only dispose the class pair if we can ensure that no instances exist using our subclass. 334 | // Since we don't globally track this, we can't ensure this - but there's also not much overhead in keeping it around. 335 | //objc_disposeClassPair(object.class); 336 | }else { 337 | // Class is most likely swizzled in place. Undo that. 338 | if (isMetaClass) { 339 | aspect_undoSwizzleClassInPlace((Class)self); 340 | }else if (self.class != klass) { 341 | aspect_undoSwizzleClassInPlace(klass); 342 | } 343 | } 344 | } 345 | } 346 | 347 | /////////////////////////////////////////////////////////////////////////////////////////// 348 | #pragma mark - Hook Class 349 | 350 | static Class aspect_hookClass(NSObject *self, NSError **error) { 351 | NSCParameterAssert(self); 352 | Class statedClass = self.class; 353 | Class baseClass = object_getClass(self); 354 | NSString *className = NSStringFromClass(baseClass); 355 | 356 | // Already subclassed 357 | if ([className hasSuffix:AspectsSubclassSuffix]) { 358 | return baseClass; 359 | 360 | // We swizzle a class object, not a single object. 361 | }else if (class_isMetaClass(baseClass)) { 362 | return aspect_swizzleClassInPlace((Class)self); 363 | // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place. 364 | }else if (statedClass != baseClass) { 365 | return aspect_swizzleClassInPlace(baseClass); 366 | } 367 | 368 | // Default case. Create dynamic subclass. 369 | const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; 370 | Class subclass = objc_getClass(subclassName); 371 | 372 | if (subclass == nil) { 373 | subclass = objc_allocateClassPair(baseClass, subclassName, 0); 374 | if (subclass == nil) { 375 | NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; 376 | AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); 377 | return nil; 378 | } 379 | 380 | aspect_swizzleForwardInvocation(subclass); 381 | aspect_hookedGetClass(subclass, statedClass); 382 | aspect_hookedGetClass(object_getClass(subclass), statedClass); 383 | objc_registerClassPair(subclass); 384 | } 385 | 386 | object_setClass(self, subclass); 387 | return subclass; 388 | } 389 | 390 | static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; 391 | static void aspect_swizzleForwardInvocation(Class klass) { 392 | NSCParameterAssert(klass); 393 | // If there is no method, replace will act like class_addMethod. 394 | IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); 395 | if (originalImplementation) { 396 | class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); 397 | } 398 | AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); 399 | } 400 | 401 | static void aspect_undoSwizzleForwardInvocation(Class klass) { 402 | NSCParameterAssert(klass); 403 | Method originalMethod = class_getInstanceMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName)); 404 | Method objectMethod = class_getInstanceMethod(NSObject.class, @selector(forwardInvocation:)); 405 | // There is no class_removeMethod, so the best we can do is to retore the original implementation, or use a dummy. 406 | IMP originalImplementation = method_getImplementation(originalMethod ?: objectMethod); 407 | class_replaceMethod(klass, @selector(forwardInvocation:), originalImplementation, "v@:@"); 408 | 409 | AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(klass)); 410 | } 411 | 412 | static void aspect_hookedGetClass(Class class, Class statedClass) { 413 | NSCParameterAssert(class); 414 | NSCParameterAssert(statedClass); 415 | Method method = class_getInstanceMethod(class, @selector(class)); 416 | IMP newIMP = imp_implementationWithBlock(^(id self) { 417 | return statedClass; 418 | }); 419 | class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method)); 420 | } 421 | 422 | /////////////////////////////////////////////////////////////////////////////////////////// 423 | #pragma mark - Swizzle Class In Place 424 | 425 | static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) { 426 | static NSMutableSet *swizzledClasses; 427 | static dispatch_once_t pred; 428 | dispatch_once(&pred, ^{ 429 | swizzledClasses = [NSMutableSet new]; 430 | }); 431 | @synchronized(swizzledClasses) { 432 | block(swizzledClasses); 433 | } 434 | } 435 | 436 | static Class aspect_swizzleClassInPlace(Class klass) { 437 | NSCParameterAssert(klass); 438 | NSString *className = NSStringFromClass(klass); 439 | 440 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 441 | if (![swizzledClasses containsObject:className]) { 442 | aspect_swizzleForwardInvocation(klass); 443 | [swizzledClasses addObject:className]; 444 | } 445 | }); 446 | return klass; 447 | } 448 | 449 | static void aspect_undoSwizzleClassInPlace(Class klass) { 450 | NSCParameterAssert(klass); 451 | NSString *className = NSStringFromClass(klass); 452 | 453 | _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { 454 | if ([swizzledClasses containsObject:className]) { 455 | aspect_undoSwizzleForwardInvocation(klass); 456 | [swizzledClasses removeObject:className]; 457 | } 458 | }); 459 | } 460 | 461 | /////////////////////////////////////////////////////////////////////////////////////////// 462 | #pragma mark - Aspect Invoke Point 463 | 464 | // This is a macro so we get a cleaner stack trace. 465 | #define aspect_invoke(aspects, info) \ 466 | for (AspectIdentifier *aspect in aspects) {\ 467 | [aspect invokeWithInfo:info];\ 468 | if (aspect.options & AspectOptionAutomaticRemoval) { \ 469 | aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \ 470 | } \ 471 | } 472 | 473 | // This is the swizzled forwardInvocation: method. 474 | static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { 475 | NSCParameterAssert(self); 476 | NSCParameterAssert(invocation); 477 | SEL originalSelector = invocation.selector; 478 | SEL aliasSelector = aspect_aliasForSelector(invocation.selector); 479 | invocation.selector = aliasSelector; 480 | AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector); 481 | AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector); 482 | AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation]; 483 | NSArray *aspectsToRemove = nil; 484 | 485 | // Before hooks. 486 | aspect_invoke(classContainer.beforeAspects, info); 487 | aspect_invoke(objectContainer.beforeAspects, info); 488 | 489 | // Instead hooks. 490 | BOOL respondsToAlias = YES; 491 | if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { 492 | aspect_invoke(classContainer.insteadAspects, info); 493 | aspect_invoke(objectContainer.insteadAspects, info); 494 | }else { 495 | Class klass = object_getClass(invocation.target); 496 | do { 497 | if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { 498 | [invocation invoke]; 499 | break; 500 | } 501 | }while (!respondsToAlias && (klass = class_getSuperclass(klass))); 502 | } 503 | 504 | // After hooks. 505 | aspect_invoke(classContainer.afterAspects, info); 506 | aspect_invoke(objectContainer.afterAspects, info); 507 | 508 | // If no hooks are installed, call original implementation (usually to throw an exception) 509 | if (!respondsToAlias) { 510 | invocation.selector = originalSelector; 511 | SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); 512 | if ([self respondsToSelector:originalForwardInvocationSEL]) { 513 | ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); 514 | }else { 515 | [self doesNotRecognizeSelector:invocation.selector]; 516 | } 517 | } 518 | 519 | // Remove any hooks that are queued for deregistration. 520 | [aspectsToRemove makeObjectsPerformSelector:@selector(remove)]; 521 | } 522 | #undef aspect_invoke 523 | 524 | /////////////////////////////////////////////////////////////////////////////////////////// 525 | #pragma mark - Aspect Container Management 526 | 527 | // Loads or creates the aspect container. 528 | static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) { 529 | NSCParameterAssert(self); 530 | SEL aliasSelector = aspect_aliasForSelector(selector); 531 | AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector); 532 | if (!aspectContainer) { 533 | aspectContainer = [AspectsContainer new]; 534 | objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN); 535 | } 536 | return aspectContainer; 537 | } 538 | 539 | static AspectsContainer *aspect_getContainerForClass(Class klass, SEL selector) { 540 | NSCParameterAssert(klass); 541 | AspectsContainer *classContainer = nil; 542 | do { 543 | classContainer = objc_getAssociatedObject(klass, selector); 544 | if (classContainer.hasAspects) break; 545 | }while ((klass = class_getSuperclass(klass))); 546 | 547 | return classContainer; 548 | } 549 | 550 | static void aspect_destroyContainerForObject(id self, SEL selector) { 551 | NSCParameterAssert(self); 552 | SEL aliasSelector = aspect_aliasForSelector(selector); 553 | objc_setAssociatedObject(self, aliasSelector, nil, OBJC_ASSOCIATION_RETAIN); 554 | } 555 | 556 | /////////////////////////////////////////////////////////////////////////////////////////// 557 | #pragma mark - Selector Blacklist Checking 558 | 559 | static NSMutableDictionary *aspect_getSwizzledClassesDict() { 560 | static NSMutableDictionary *swizzledClassesDict; 561 | static dispatch_once_t pred; 562 | dispatch_once(&pred, ^{ 563 | swizzledClassesDict = [NSMutableDictionary new]; 564 | }); 565 | return swizzledClassesDict; 566 | } 567 | 568 | static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) { 569 | static NSSet *disallowedSelectorList; 570 | static dispatch_once_t pred; 571 | dispatch_once(&pred, ^{ 572 | disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil]; 573 | }); 574 | 575 | // Check against the blacklist. 576 | NSString *selectorName = NSStringFromSelector(selector); 577 | if ([disallowedSelectorList containsObject:selectorName]) { 578 | NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName]; 579 | AspectError(AspectErrorSelectorBlacklisted, errorDescription); 580 | return NO; 581 | } 582 | 583 | // Additional checks. 584 | AspectOptions position = options&AspectPositionFilter; 585 | if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) { 586 | NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc."; 587 | AspectError(AspectErrorSelectorDeallocPosition, errorDesc); 588 | return NO; 589 | } 590 | 591 | if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) { 592 | NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName]; 593 | AspectError(AspectErrorDoesNotRespondToSelector, errorDesc); 594 | return NO; 595 | } 596 | 597 | // Search for the current class and the class hierarchy IF we are modifying a class object 598 | if (class_isMetaClass(object_getClass(self))) { 599 | Class klass = [self class]; 600 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 601 | Class currentClass = [self class]; 602 | 603 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 604 | if ([tracker subclassHasHookedSelectorName:selectorName]) { 605 | NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName]; 606 | NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"]; 607 | NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames]; 608 | AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); 609 | return NO; 610 | } 611 | 612 | do { 613 | tracker = swizzledClassesDict[currentClass]; 614 | if ([tracker.selectorNames containsObject:selectorName]) { 615 | if (klass == currentClass) { 616 | // Already modified and topmost! 617 | return YES; 618 | } 619 | NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)]; 620 | AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); 621 | return NO; 622 | } 623 | } while ((currentClass = class_getSuperclass(currentClass))); 624 | 625 | // Add the selector as being modified. 626 | currentClass = klass; 627 | AspectTracker *subclassTracker = nil; 628 | do { 629 | tracker = swizzledClassesDict[currentClass]; 630 | if (!tracker) { 631 | tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass]; 632 | swizzledClassesDict[(id)currentClass] = tracker; 633 | } 634 | if (subclassTracker) { 635 | [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName]; 636 | } else { 637 | [tracker.selectorNames addObject:selectorName]; 638 | } 639 | 640 | // All superclasses get marked as having a subclass that is modified. 641 | subclassTracker = tracker; 642 | }while ((currentClass = class_getSuperclass(currentClass))); 643 | } else { 644 | return YES; 645 | } 646 | 647 | return YES; 648 | } 649 | 650 | static void aspect_deregisterTrackedSelector(id self, SEL selector) { 651 | if (!class_isMetaClass(object_getClass(self))) return; 652 | 653 | NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); 654 | NSString *selectorName = NSStringFromSelector(selector); 655 | Class currentClass = [self class]; 656 | AspectTracker *subclassTracker = nil; 657 | do { 658 | AspectTracker *tracker = swizzledClassesDict[currentClass]; 659 | if (subclassTracker) { 660 | [tracker removeSubclassTracker:subclassTracker hookingSelectorName:selectorName]; 661 | } else { 662 | [tracker.selectorNames removeObject:selectorName]; 663 | } 664 | if (tracker.selectorNames.count == 0 && tracker.selectorNamesToSubclassTrackers) { 665 | [swizzledClassesDict removeObjectForKey:currentClass]; 666 | } 667 | subclassTracker = tracker; 668 | }while ((currentClass = class_getSuperclass(currentClass))); 669 | } 670 | 671 | @end 672 | 673 | @implementation AspectTracker 674 | 675 | - (id)initWithTrackedClass:(Class)trackedClass { 676 | if (self = [super init]) { 677 | _trackedClass = trackedClass; 678 | _selectorNames = [NSMutableSet new]; 679 | _selectorNamesToSubclassTrackers = [NSMutableDictionary new]; 680 | } 681 | return self; 682 | } 683 | 684 | - (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName { 685 | return self.selectorNamesToSubclassTrackers[selectorName] != nil; 686 | } 687 | 688 | - (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName { 689 | NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName]; 690 | if (!trackerSet) { 691 | trackerSet = [NSMutableSet new]; 692 | self.selectorNamesToSubclassTrackers[selectorName] = trackerSet; 693 | } 694 | [trackerSet addObject:subclassTracker]; 695 | } 696 | - (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName { 697 | NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName]; 698 | [trackerSet removeObject:subclassTracker]; 699 | if (trackerSet.count == 0) { 700 | [self.selectorNamesToSubclassTrackers removeObjectForKey:selectorName]; 701 | } 702 | } 703 | - (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName { 704 | NSMutableSet *hookingSubclassTrackers = [NSMutableSet new]; 705 | for (AspectTracker *tracker in self.selectorNamesToSubclassTrackers[selectorName]) { 706 | if ([tracker.selectorNames containsObject:selectorName]) { 707 | [hookingSubclassTrackers addObject:tracker]; 708 | } 709 | [hookingSubclassTrackers unionSet:[tracker subclassTrackersHookingSelectorName:selectorName]]; 710 | } 711 | return hookingSubclassTrackers; 712 | } 713 | - (NSString *)trackedClassName { 714 | return NSStringFromClass(self.trackedClass); 715 | } 716 | 717 | - (NSString *)description { 718 | return [NSString stringWithFormat:@"<%@: %@, trackedClass: %@, selectorNames:%@, subclass selector names: %@>", self.class, self, NSStringFromClass(self.trackedClass), self.selectorNames, self.selectorNamesToSubclassTrackers.allKeys]; 719 | } 720 | 721 | @end 722 | 723 | /////////////////////////////////////////////////////////////////////////////////////////// 724 | #pragma mark - NSInvocation (Aspects) 725 | 726 | @implementation NSInvocation (Aspects) 727 | 728 | // Thanks to the ReactiveCocoa team for providing a generic solution for this. 729 | - (id)aspect_argumentAtIndex:(NSUInteger)index { 730 | const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; 731 | // Skip const type qualifier. 732 | if (argType[0] == _C_CONST) argType++; 733 | 734 | #define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0) 735 | if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { 736 | __autoreleasing id returnObj; 737 | [self getArgument:&returnObj atIndex:(NSInteger)index]; 738 | return returnObj; 739 | } else if (strcmp(argType, @encode(SEL)) == 0) { 740 | SEL selector = 0; 741 | [self getArgument:&selector atIndex:(NSInteger)index]; 742 | return NSStringFromSelector(selector); 743 | } else if (strcmp(argType, @encode(Class)) == 0) { 744 | __autoreleasing Class theClass = Nil; 745 | [self getArgument:&theClass atIndex:(NSInteger)index]; 746 | return theClass; 747 | // Using this list will box the number with the appropriate constructor, instead of the generic NSValue. 748 | } else if (strcmp(argType, @encode(char)) == 0) { 749 | WRAP_AND_RETURN(char); 750 | } else if (strcmp(argType, @encode(int)) == 0) { 751 | WRAP_AND_RETURN(int); 752 | } else if (strcmp(argType, @encode(short)) == 0) { 753 | WRAP_AND_RETURN(short); 754 | } else if (strcmp(argType, @encode(long)) == 0) { 755 | WRAP_AND_RETURN(long); 756 | } else if (strcmp(argType, @encode(long long)) == 0) { 757 | WRAP_AND_RETURN(long long); 758 | } else if (strcmp(argType, @encode(unsigned char)) == 0) { 759 | WRAP_AND_RETURN(unsigned char); 760 | } else if (strcmp(argType, @encode(unsigned int)) == 0) { 761 | WRAP_AND_RETURN(unsigned int); 762 | } else if (strcmp(argType, @encode(unsigned short)) == 0) { 763 | WRAP_AND_RETURN(unsigned short); 764 | } else if (strcmp(argType, @encode(unsigned long)) == 0) { 765 | WRAP_AND_RETURN(unsigned long); 766 | } else if (strcmp(argType, @encode(unsigned long long)) == 0) { 767 | WRAP_AND_RETURN(unsigned long long); 768 | } else if (strcmp(argType, @encode(float)) == 0) { 769 | WRAP_AND_RETURN(float); 770 | } else if (strcmp(argType, @encode(double)) == 0) { 771 | WRAP_AND_RETURN(double); 772 | } else if (strcmp(argType, @encode(BOOL)) == 0) { 773 | WRAP_AND_RETURN(BOOL); 774 | } else if (strcmp(argType, @encode(bool)) == 0) { 775 | WRAP_AND_RETURN(BOOL); 776 | } else if (strcmp(argType, @encode(char *)) == 0) { 777 | WRAP_AND_RETURN(const char *); 778 | } else if (strcmp(argType, @encode(void (^)(void))) == 0) { 779 | __unsafe_unretained id block = nil; 780 | [self getArgument:&block atIndex:(NSInteger)index]; 781 | return [block copy]; 782 | } else { 783 | NSUInteger valueSize = 0; 784 | NSGetSizeAndAlignment(argType, &valueSize, NULL); 785 | 786 | unsigned char valueBytes[valueSize]; 787 | [self getArgument:valueBytes atIndex:(NSInteger)index]; 788 | 789 | return [NSValue valueWithBytes:valueBytes objCType:argType]; 790 | } 791 | return nil; 792 | #undef WRAP_AND_RETURN 793 | } 794 | 795 | - (NSArray *)aspects_arguments { 796 | NSMutableArray *argumentsArray = [NSMutableArray array]; 797 | for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) { 798 | [argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null]; 799 | } 800 | return [argumentsArray copy]; 801 | } 802 | 803 | @end 804 | 805 | /////////////////////////////////////////////////////////////////////////////////////////// 806 | #pragma mark - AspectIdentifier 807 | 808 | @implementation AspectIdentifier 809 | 810 | + (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error { 811 | NSCParameterAssert(block); 812 | NSCParameterAssert(selector); 813 | NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc. 814 | if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) { 815 | return nil; 816 | } 817 | 818 | AspectIdentifier *identifier = nil; 819 | if (blockSignature) { 820 | identifier = [AspectIdentifier new]; 821 | identifier.selector = selector; 822 | identifier.block = block; 823 | identifier.blockSignature = blockSignature; 824 | identifier.options = options; 825 | identifier.object = object; // weak 826 | } 827 | return identifier; 828 | } 829 | 830 | - (BOOL)invokeWithInfo:(id)info { 831 | NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature]; 832 | NSInvocation *originalInvocation = info.originalInvocation; 833 | NSUInteger numberOfArguments = self.blockSignature.numberOfArguments; 834 | 835 | // Be extra paranoid. We already check that on hook registration. 836 | if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) { 837 | AspectLogError(@"Block has too many arguments. Not calling %@", info); 838 | return NO; 839 | } 840 | 841 | // The `self` of the block will be the AspectInfo. Optional. 842 | if (numberOfArguments > 1) { 843 | [blockInvocation setArgument:&info atIndex:1]; 844 | } 845 | 846 | void *argBuf = NULL; 847 | for (NSUInteger idx = 2; idx < numberOfArguments; idx++) { 848 | const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx]; 849 | NSUInteger argSize; 850 | NSGetSizeAndAlignment(type, &argSize, NULL); 851 | 852 | if (!(argBuf = reallocf(argBuf, argSize))) { 853 | AspectLogError(@"Failed to allocate memory for block invocation."); 854 | return NO; 855 | } 856 | 857 | [originalInvocation getArgument:argBuf atIndex:idx]; 858 | [blockInvocation setArgument:argBuf atIndex:idx]; 859 | } 860 | 861 | [blockInvocation invokeWithTarget:self.block]; 862 | 863 | if (argBuf != NULL) { 864 | free(argBuf); 865 | } 866 | return YES; 867 | } 868 | 869 | - (NSString *)description { 870 | return [NSString stringWithFormat:@"<%@: %p, SEL:%@ object:%@ options:%tu block:%@ (#%tu args)>", self.class, self, NSStringFromSelector(self.selector), self.object, self.options, self.block, self.blockSignature.numberOfArguments]; 871 | } 872 | 873 | - (BOOL)remove { 874 | return aspect_remove(self, NULL); 875 | } 876 | 877 | @end 878 | 879 | /////////////////////////////////////////////////////////////////////////////////////////// 880 | #pragma mark - AspectsContainer 881 | 882 | @implementation AspectsContainer 883 | 884 | - (BOOL)hasAspects { 885 | return self.beforeAspects.count > 0 || self.insteadAspects.count > 0 || self.afterAspects.count > 0; 886 | } 887 | 888 | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options { 889 | NSParameterAssert(aspect); 890 | NSUInteger position = options&AspectPositionFilter; 891 | switch (position) { 892 | case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break; 893 | case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break; 894 | case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break; 895 | } 896 | } 897 | 898 | - (BOOL)removeAspect:(id)aspect { 899 | for (NSString *aspectArrayName in @[NSStringFromSelector(@selector(beforeAspects)), 900 | NSStringFromSelector(@selector(insteadAspects)), 901 | NSStringFromSelector(@selector(afterAspects))]) { 902 | NSArray *array = [self valueForKey:aspectArrayName]; 903 | NSUInteger index = [array indexOfObjectIdenticalTo:aspect]; 904 | if (array && index != NSNotFound) { 905 | NSMutableArray *newArray = [NSMutableArray arrayWithArray:array]; 906 | [newArray removeObjectAtIndex:index]; 907 | [self setValue:newArray forKey:aspectArrayName]; 908 | return YES; 909 | } 910 | } 911 | return NO; 912 | } 913 | 914 | - (NSString *)description { 915 | return [NSString stringWithFormat:@"<%@: %p, before:%@, instead:%@, after:%@>", self.class, self, self.beforeAspects, self.insteadAspects, self.afterAspects]; 916 | } 917 | 918 | @end 919 | 920 | /////////////////////////////////////////////////////////////////////////////////////////// 921 | #pragma mark - AspectInfo 922 | 923 | @implementation AspectInfo 924 | 925 | @synthesize arguments = _arguments; 926 | 927 | - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation { 928 | NSCParameterAssert(instance); 929 | NSCParameterAssert(invocation); 930 | if (self = [super init]) { 931 | _instance = instance; 932 | _originalInvocation = invocation; 933 | } 934 | return self; 935 | } 936 | 937 | - (NSArray *)arguments { 938 | // Lazily evaluate arguments, boxing is expensive. 939 | if (!_arguments) { 940 | _arguments = self.originalInvocation.aspects_arguments; 941 | } 942 | return _arguments; 943 | } 944 | 945 | @end 946 | -------------------------------------------------------------------------------- /HighlightSelectedString/HighlightSelectedString.h: -------------------------------------------------------------------------------- 1 | // 2 | // HighlightSelectedString.h 3 | // HighlightSelectedString 4 | // 5 | // Created by lixy on 15/1/23. 6 | // Copyright (c) 2015年 lixy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface HighlightSelectedString : NSObject 12 | 13 | + (instancetype)sharedPlugin; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /HighlightSelectedString/HighlightSelectedString.m: -------------------------------------------------------------------------------- 1 | // 2 | // HighlightSelectedString.m 3 | // HighlightSelectedString 4 | // 5 | // Created by lixy on 15/1/23. 6 | // Copyright (c) 2015年 lixy. All rights reserved. 7 | // 8 | 9 | #import "HighlightSelectedString.h" 10 | #import "NSLayoutManager+SelectedSymbols.h" 11 | #import 12 | #import "Aspects.h" 13 | 14 | #define HighlightColorKey @"HighlightColorKey" 15 | #define HighlightEnableStateKey @"HighlightEnableStateKey" 16 | #define HighlightOnlySymbolsKey @"HighlightOnlySymbolsKey" 17 | #define HighlightDoubleClickKey @"HighlightDoubleClickKey" 18 | #define HighlightCaseInsensitiveKey @"HighlightCaseInsensitive" 19 | 20 | static const char hilightStateKey; 21 | 22 | static HighlightSelectedString *sharedPlugin; 23 | 24 | @interface HighlightSelectedString() 25 | 26 | @property (nonatomic,copy) NSString *selectedText; 27 | 28 | @property (nonatomic, readonly) NSTextView *sourceTextView; 29 | @property (readonly) NSTextStorage *textStorage; 30 | @property (readonly) NSString *string; 31 | 32 | @property (nonatomic, strong) NSMenuItem *enableMenuItem; 33 | @property (nonatomic, strong) NSMenuItem *caseInsensitiveMenuItem; 34 | @property (nonatomic, strong) NSMenuItem *symbolsOnlyMenuItem; 35 | @property (nonatomic, strong) NSMenuItem *doubleClickMenuItem; 36 | 37 | @property (nonatomic, strong) NSColor *highlightColor; 38 | 39 | @end 40 | 41 | @implementation HighlightSelectedString 42 | 43 | + (void)pluginDidLoad:(NSBundle *)plugin 44 | { 45 | static dispatch_once_t onceToken; 46 | NSString *currentApplicationName = [[NSBundle mainBundle] infoDictionary][@"CFBundleName"]; 47 | if ([currentApplicationName isEqual:@"Xcode"]) { 48 | dispatch_once(&onceToken, ^{ 49 | sharedPlugin = [[self alloc] initWithBundle:plugin]; 50 | }); 51 | } 52 | } 53 | 54 | + (instancetype)sharedPlugin 55 | { 56 | return sharedPlugin; 57 | } 58 | 59 | - (id)initWithBundle:(NSBundle *)plugin 60 | { 61 | if (self = [super init]) { 62 | [[NSNotificationCenter defaultCenter] addObserver:self 63 | selector:@selector(applicationDidFinishLaunching:) 64 | name:NSApplicationDidFinishLaunchingNotification 65 | object:nil]; 66 | } 67 | return self; 68 | } 69 | 70 | - (void)applicationDidFinishLaunching: (NSNotification*)noti 71 | { 72 | [self loadConfig]; 73 | [self loadMenuItems]; 74 | [self addObserverAndNotifitions]; 75 | } 76 | 77 | - (void)loadConfig 78 | { 79 | NSUserDefaults *userD = [NSUserDefaults standardUserDefaults]; 80 | NSArray *array = [userD objectForKey:HighlightColorKey]; 81 | 82 | if (array) { 83 | CGFloat red = [array[0] floatValue]; 84 | CGFloat green = [array[1] floatValue]; 85 | CGFloat blue = [array[2] floatValue]; 86 | CGFloat alpha = [array[3] floatValue]; 87 | 88 | _highlightColor = [NSColor colorWithCalibratedRed:red green:green blue:blue alpha:alpha]; 89 | 90 | } else { 91 | _highlightColor = [NSColor colorWithCalibratedRed:1.000 green:0.992 blue:0.518 alpha:1.000]; 92 | } 93 | } 94 | 95 | - (void)loadMenuItems 96 | { 97 | NSUserDefaults *userD = [NSUserDefaults standardUserDefaults]; 98 | NSMenuItem *editMenuItem = [[NSApp mainMenu] itemWithTitle:@"Edit"]; 99 | 100 | BOOL enableState = [userD objectForKey:HighlightEnableStateKey]?[[userD objectForKey:HighlightEnableStateKey] integerValue]:1; 101 | 102 | BOOL symbolsOnlyState = [userD objectForKey:HighlightOnlySymbolsKey]?[[userD objectForKey:HighlightOnlySymbolsKey] integerValue]:1; 103 | 104 | BOOL doubleClickState = [userD objectForKey:HighlightDoubleClickKey]?[[userD objectForKey:HighlightDoubleClickKey] integerValue]:0; 105 | 106 | BOOL caseInsensitiveState = [userD objectForKey:HighlightCaseInsensitiveKey]?[[userD objectForKey:HighlightCaseInsensitiveKey] integerValue]:1; 107 | 108 | if (editMenuItem) { 109 | [[editMenuItem submenu] addItem:[NSMenuItem separatorItem]]; 110 | 111 | NSMenu *highlightMenu = [[NSMenu alloc] initWithTitle:@"Highlight Menu"]; 112 | 113 | self.enableMenuItem = [[NSMenuItem alloc] initWithTitle:@"Enable Highlight" action:@selector(enableClick) keyEquivalent:@""]; 114 | [self.enableMenuItem setTarget:self]; 115 | [self.enableMenuItem setState:enableState]; 116 | [highlightMenu addItem:self.enableMenuItem]; 117 | 118 | self.caseInsensitiveMenuItem = [[NSMenuItem alloc] initWithTitle:@"Case Insensitive" action:@selector(caseInsensitiveClick) keyEquivalent:@""]; 119 | [self.caseInsensitiveMenuItem setTarget:self]; 120 | [self.caseInsensitiveMenuItem setState:caseInsensitiveState]; 121 | [highlightMenu addItem:self.caseInsensitiveMenuItem]; 122 | 123 | NSMenu *symbolsOnlyMenu = [[NSMenu alloc] initWithTitle:@"Symbols Only Menu"]; 124 | 125 | self.symbolsOnlyMenuItem = [[NSMenuItem alloc] initWithTitle:@"Enable" action:NULL keyEquivalent:@""]; 126 | [self.symbolsOnlyMenuItem setTarget:self]; 127 | [self.symbolsOnlyMenuItem setState:symbolsOnlyState]; 128 | [self.symbolsOnlyMenuItem setAction:enableState?@selector(symbolsOnlyClick):NULL]; 129 | [symbolsOnlyMenu addItem:self.symbolsOnlyMenuItem]; 130 | 131 | self.doubleClickMenuItem = [[NSMenuItem alloc] initWithTitle:@"Double Click" action:NULL keyEquivalent:@""]; 132 | [self.doubleClickMenuItem setTarget:self]; 133 | [self.doubleClickMenuItem setState:doubleClickState]; 134 | [self.doubleClickMenuItem setAction:(enableState && self.symbolsOnlyMenuItem.state)?@selector(doubleClick):NULL]; 135 | [symbolsOnlyMenu addItem:self.doubleClickMenuItem]; 136 | 137 | NSMenuItem *symbolsOnlyMenuItem = [[NSMenuItem alloc] initWithTitle:@"Symbols Only" action:nil keyEquivalent:@""]; 138 | [symbolsOnlyMenuItem setSubmenu:symbolsOnlyMenu]; 139 | [highlightMenu addItem:symbolsOnlyMenuItem]; 140 | 141 | NSMenuItem *setting = [[NSMenuItem alloc] initWithTitle:@"Set Highlight Color" action:@selector(setHighlightColor) keyEquivalent:@""]; 142 | [setting setTarget:self]; 143 | [highlightMenu addItem:setting]; 144 | 145 | NSMenuItem *highlightMenuItem = [[NSMenuItem alloc] initWithTitle:@"Highlight" action:nil keyEquivalent:@""]; 146 | [highlightMenuItem setSubmenu:highlightMenu]; 147 | [[editMenuItem submenu] addItem:highlightMenuItem]; 148 | } 149 | 150 | } 151 | 152 | - (void)addObserverAndNotifitions 153 | { 154 | [NSLayoutManager aspect_hookSelector:@selector(init) withOptions:AspectPositionAfter usingBlock:^( id info) { 155 | NSLayoutManager *layoutM = [info instance]; 156 | layoutM.highlightKey = @"1"; 157 | [layoutM addObserver:self forKeyPath:@"autoHighlightTokenRanges" options:NSKeyValueObservingOptionNew context:nil]; 158 | 159 | } error:nil]; 160 | 161 | [NSLayoutManager aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^( id info) { 162 | 163 | NSLayoutManager *layoutM = [info instance]; 164 | if (layoutM.highlightKey.intValue==1) { 165 | [layoutM removeObserver:self forKeyPath:@"autoHighlightTokenRanges"]; 166 | } 167 | 168 | } error:nil]; 169 | 170 | [[NSNotificationCenter defaultCenter] addObserver:self 171 | selector:@selector(selectionDidChange:) 172 | name:NSTextViewDidChangeSelectionNotification 173 | object:nil]; 174 | } 175 | 176 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 177 | { 178 | if ([keyPath isEqualToString:@"autoHighlightTokenRanges"]) { 179 | NSLayoutManager *manager = object; 180 | if (manager.firstTextView == self.sourceTextView) { 181 | if (self.enableMenuItem.state) { 182 | if (self.symbolsOnlyMenuItem.state) { 183 | if (!self.doubleClickMenuItem.state) { 184 | [self highlightSelectedSymbols]; 185 | } else { 186 | if (self.selectedText.length>0) { 187 | [self highlightSelectedSymbols]; 188 | } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | #pragma mark Highlight Selected String state 197 | - (void)enableClick 198 | { 199 | self.enableMenuItem.state = !self.enableMenuItem.state; 200 | 201 | if (!self.enableMenuItem.state) { 202 | 203 | [self removeAllHighlighting]; 204 | self.symbolsOnlyMenuItem.action = NULL; 205 | self.doubleClickMenuItem.action = NULL; 206 | self.caseInsensitiveMenuItem.action = NULL; 207 | 208 | } else{ 209 | self.symbolsOnlyMenuItem.action = @selector(symbolsOnlyClick); 210 | self.doubleClickMenuItem.action = @selector(doubleClick); 211 | self.caseInsensitiveMenuItem.action = @selector(caseInsensitiveClick); 212 | } 213 | 214 | NSUserDefaults *userD = [NSUserDefaults standardUserDefaults]; 215 | [userD setObject:@(self.enableMenuItem.state) forKey:HighlightEnableStateKey]; 216 | [userD synchronize]; 217 | } 218 | 219 | - (void)caseInsensitiveClick 220 | { 221 | self.caseInsensitiveMenuItem.state = !self.caseInsensitiveMenuItem.state; 222 | 223 | NSUserDefaults *userD = [NSUserDefaults standardUserDefaults]; 224 | [userD setObject:@(self.caseInsensitiveMenuItem.state) forKey:HighlightCaseInsensitiveKey]; 225 | [userD synchronize]; 226 | } 227 | 228 | - (void)symbolsOnlyClick 229 | { 230 | self.symbolsOnlyMenuItem.state = !self.symbolsOnlyMenuItem.state; 231 | 232 | if (!self.symbolsOnlyMenuItem.state) { 233 | self.doubleClickMenuItem.action = NULL; 234 | } else{ 235 | self.doubleClickMenuItem.action = @selector(doubleClick); 236 | } 237 | 238 | [self resetHighlight]; 239 | 240 | NSUserDefaults *userD = [NSUserDefaults standardUserDefaults]; 241 | [userD setObject:@(self.symbolsOnlyMenuItem.state) forKey:HighlightOnlySymbolsKey]; 242 | [userD synchronize]; 243 | } 244 | 245 | - (void)doubleClick 246 | { 247 | self.doubleClickMenuItem.state = !self.doubleClickMenuItem.state; 248 | 249 | NSUserDefaults *userD = [NSUserDefaults standardUserDefaults]; 250 | [userD setObject:@(self.doubleClickMenuItem.state) forKey:HighlightDoubleClickKey]; 251 | [userD synchronize]; 252 | } 253 | 254 | #pragma mark Highlight color function 255 | -(void)selectionDidChange:(NSNotification *)noti { 256 | 257 | NSTextView *textView = [noti object]; 258 | NSString *className = NSStringFromClass([textView class]); 259 | 260 | if ([className isEqualToString:@"DVTSourceTextView"]/* 代码编辑器 */ || [className isEqualToString:@"IDEConsoleTextView"] /* 控制台 */) { 261 | 262 | //延迟0.1秒执行高亮 263 | [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(todoSomething) object:nil]; 264 | [self performSelector:@selector(todoSomething) withObject:nil afterDelay:0.1f]; 265 | 266 | } 267 | } 268 | 269 | 270 | - (void)todoSomething 271 | { 272 | if (self.enableMenuItem.state) { 273 | NSString *className = NSStringFromClass([self.sourceTextView class]); 274 | 275 | if ([className isEqualToString:@"DVTSourceTextView"]/* 代码编辑器 */) { 276 | if (self.symbolsOnlyMenuItem.state) { 277 | if (self.doubleClickMenuItem.state) { 278 | if (self.selectedText.length>0) { 279 | [self highlightSelectedSymbols]; 280 | } 281 | else { 282 | [self removeAllHighlighting]; 283 | } 284 | } 285 | } else { 286 | [self highlightSelectedStrings]; 287 | } 288 | 289 | } else if ([className isEqualToString:@"IDEConsoleTextView"] /* 控制台 直接高亮*/) { 290 | [self highlightSelectedStrings]; 291 | } 292 | } 293 | } 294 | 295 | - (NSString *)selectedText 296 | { 297 | NSTextView *textView = self.sourceTextView; 298 | NSRange selectedRange = [textView selectedRange]; 299 | NSString *text = textView.textStorage.string; 300 | NSString *nSelectedStr = [[text substringWithRange:selectedRange] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" \n"]]; 301 | 302 | //如果新选中的长度为0 就返回空 303 | if (!nSelectedStr.length) { 304 | return @""; 305 | } 306 | 307 | _selectedText = nSelectedStr; 308 | 309 | return _selectedText; 310 | } 311 | 312 | - (void)highlightSelectedStrings 313 | { 314 | //每次高亮 都撤销之前高亮 315 | [self removeAllHighlighting]; 316 | 317 | NSArray *array = [self rangesOfString:self.selectedText]; 318 | 319 | if (array.count<2) { 320 | return; 321 | } 322 | 323 | [self addBgColorWithRangeArray:array]; 324 | } 325 | 326 | - (NSMutableArray*)rangesOfString:(NSString *)string 327 | { 328 | if (string.length == 0) { 329 | return nil; 330 | } 331 | 332 | NSUInteger length = [self.string length]; 333 | 334 | NSString *regexString = string; 335 | 336 | NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:regexString options:self.caseInsensitiveMenuItem.state?NSRegularExpressionCaseInsensitive:0 | NSRegularExpressionIgnoreMetacharacters error:nil]; 337 | NSArray *matches = [regex matchesInString:self.string options:0 range:NSMakeRange(0, length)]; 338 | 339 | NSMutableArray *rangeArray = [NSMutableArray array]; 340 | 341 | for (NSTextCheckingResult *result in matches) { 342 | [rangeArray addObject:[NSValue valueWithRange:result.range]]; 343 | } 344 | 345 | return rangeArray; 346 | } 347 | 348 | 349 | - (void)highlightSelectedSymbols 350 | { 351 | NSString *className = NSStringFromClass([self.sourceTextView class]); 352 | 353 | if ([className isEqualToString:@"DVTSourceTextView"]/* 代码编辑器 */) { 354 | 355 | [self removeAllHighlighting]; 356 | 357 | NSArray *array = self.sourceTextView.layoutManager.autoHighlightTokenRanges; 358 | if (array.count>1) { 359 | [self addBgColorWithRangeArray:array]; 360 | } 361 | 362 | } 363 | } 364 | 365 | #pragma mark---Color rendering---- 366 | - (void)addBgColorWithRangeArray:(NSArray*)rangeArray 367 | { 368 | NSTextView *textView = self.sourceTextView; 369 | 370 | [rangeArray enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL *stop) { 371 | NSRange range = [value rangeValue]; 372 | [textView.layoutManager addTemporaryAttribute:NSBackgroundColorAttributeName value:_highlightColor forCharacterRange:range]; 373 | }]; 374 | 375 | [textView setNeedsDisplay:YES]; 376 | if (textView) { 377 | objc_setAssociatedObject(textView, &hilightStateKey, @"1", OBJC_ASSOCIATION_COPY); 378 | } 379 | } 380 | 381 | - (void)resetHighlight 382 | { 383 | [self removeAllHighlighting]; 384 | 385 | NSArray *rangesArray = nil; 386 | if (self.symbolsOnlyMenuItem.state) { 387 | rangesArray = self.sourceTextView.layoutManager.autoHighlightTokenRanges; 388 | } else { 389 | rangesArray = [self rangesOfString:self.selectedText]; 390 | } 391 | 392 | if (rangesArray.count>1) { 393 | [self addBgColorWithRangeArray:rangesArray]; 394 | } 395 | } 396 | 397 | - (void)removeAllHighlighting 398 | { 399 | NSUInteger length = [[self.textStorage string] length]; 400 | NSTextView *textView = self.sourceTextView; 401 | 402 | NSString *hilightState = objc_getAssociatedObject(textView, &hilightStateKey); 403 | if (![hilightState boolValue]) { 404 | return; 405 | } 406 | 407 | NSRange range = NSMakeRange(0, 0); 408 | for (int i=0; i 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 2.2 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | DVTPlugInCompatibilityUUIDs 24 | 25 | ACA8656B-FEA8-4B6D-8E4A-93F4C95C362C 26 | 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSPrincipalClass 30 | HighlightSelectedString 31 | XC4Compatible 32 | 33 | XCPluginHasUI 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /HighlightSelectedString/NSLayoutManager+SelectedSymbols.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutManager+SelectedSymbols.h 3 | // HighlightSelectedString 4 | // 5 | // Created by lixy on 16/2/19. 6 | // Copyright © 2016年 lixy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSLayoutManager (SelectedSymbols) 12 | 13 | @property (nonatomic, copy) NSString *highlightKey; 14 | @property(readonly, copy) NSArray *autoHighlightTokenRanges; // @synthesize autoHighlightTokenRanges=_autoHighlightTokenRanges; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /HighlightSelectedString/NSLayoutManager+SelectedSymbols.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutManager+SelectedSymbols.m 3 | // HighlightSelectedString 4 | // 5 | // Created by lixy on 16/2/19. 6 | // Copyright © 2016年 lixy. All rights reserved. 7 | // 8 | 9 | #import "NSLayoutManager+SelectedSymbols.h" 10 | #import 11 | 12 | static const char kHighlightLayoutManagerKey; 13 | 14 | @implementation NSLayoutManager (SelectedSymbols) 15 | 16 | - (NSArray *)autoHighlightTokenRanges 17 | { 18 | return nil; 19 | } 20 | 21 | - (void)setAutoHighlightTokenRanges:(NSArray *)autoHighlightTokenRanges 22 | { 23 | 24 | } 25 | 26 | - (void)setHighlightKey:(NSString *)highlightKey 27 | { 28 | objc_setAssociatedObject(self, &kHighlightLayoutManagerKey, highlightKey, OBJC_ASSOCIATION_COPY); 29 | } 30 | 31 | - (NSString *)highlightKey 32 | { 33 | NSString *hilightKey = objc_getAssociatedObject(self, &kHighlightLayoutManagerKey); 34 | return hilightKey; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Highlight the String which is same to selected string. 3 |
4 | 高亮显示所有与选中 字符串 相同的 字符串。 5 |
6 |
7 | demo.png 8 | # Support 9 | Xcode 7 or earlier: Install with [Alcatraz][1] or Clone and build the project 10 |
11 | Xcode 8 or later: see [Install Xcode8][2] 12 | # Update 13 | ## 2.2 14 | More new features. More intelligent. 15 |
16 |
17 | menu.png 18 | 19 | # Thanks 20 | Xcode 4 插件制作入门
21 | XcodeBoost
22 | Xcode Plugin Template
23 | Xcode插件兼容多个版本
24 | 25 | [1]: http://alcatraz.io "Alcatraz" 26 | [2]: https://github.com/XVimProject/XVim/blob/master/INSTALL_Xcode8.md "Install Xcode8" -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepyounger/HighlightSelectedString/f9044434e9e5fe11405abc52e944925fd3346504/demo.png -------------------------------------------------------------------------------- /menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepyounger/HighlightSelectedString/f9044434e9e5fe11405abc52e944925fd3346504/menu.png --------------------------------------------------------------------------------