├── .gitignore ├── Default.sublime-commands ├── FindKeyConflicts.sublime-settings ├── LICENSE.txt ├── Main.sublime-menu ├── README.md ├── find_key_conflicts.py ├── lib ├── __init__.py ├── minify_json.py ├── package_resources.py └── strip_commas.py ├── messages.json └── messages ├── 1.txt ├── 2.txt ├── 3.txt └── install.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "FindKeyConflicts: (Direct) Conflicts to Quick Panel", 4 | "command": "find_key_conflicts" 5 | }, 6 | { 7 | "caption": "FindKeyConflicts: (Direct) Conflicts to Buffer", 8 | "command": "find_key_conflicts", 9 | "args": {"output": "buffer"} 10 | }, 11 | { 12 | "caption": "FindKeyConflicts: All Key Maps to Quick Panel", 13 | "command": "find_key_mappings" 14 | }, 15 | { 16 | "caption": "FindKeyConflicts: All Key Maps to Buffer", 17 | "command": "find_key_mappings", 18 | "args": {"output": "buffer"} 19 | }, 20 | { 21 | "caption": "FindKeyConflicts: All Conflicts", 22 | "command": "find_all_key_conflicts" 23 | }, 24 | { 25 | "caption": "FindKeyConflicts: Overlap Conflicts", 26 | "command": "find_overlap_conflicts" 27 | }, 28 | { 29 | "caption": "FindKeyConflicts: Single Package Conflicts", 30 | "command": "find_key_conflicts_with_package" 31 | }, 32 | { 33 | "caption": "FindKeyConflicts: Multiple Package Conflicts", 34 | "command": "find_key_conflicts_with_package", 35 | "args": {"multiple": true} 36 | }, 37 | { 38 | "caption": "FindKeyConflicts: Command Search", 39 | "command": "find_key_conflicts_command_search" 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /FindKeyConflicts.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // List of packages to ignore. Note that ignored packages from 3 | // Preferences are included by default. 4 | "ignored_packages": [], 5 | 6 | // Boolean specifying if single key commands should be ignored. 7 | "ignore_single_key": false, 8 | 9 | // List of ignored patterns. These should match the style used 10 | // to specify key bindings. 11 | "ignore_patterns": [], 12 | 13 | // Used to determine if internal package conflicts should be displayed. 14 | "display_internal_conflicts": true, 15 | 16 | // Enables debug mode 17 | "debug": false 18 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 FindKeyConflicts authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | and associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 6 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 7 | is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "FindKeyConflicts", 16 | "children": 17 | [ 18 | { 19 | "command": "open_file", 20 | "args": {"file": "${packages}/FindKeyConflicts/README.md"}, 21 | "caption": "README" 22 | }, 23 | { "caption": "-" }, 24 | { 25 | "command": "open_file", 26 | "args": {"file": "${packages}/FindKeyConflicts/FindKeyConflicts.sublime-settings"}, 27 | "caption": "Settings – Default" 28 | }, 29 | { 30 | "command": "open_file", 31 | "args": {"file": "${packages}/User/FindKeyConflicts.sublime-settings"}, 32 | "caption": "Settings – User" 33 | }, 34 | { "caption": "-" }, 35 | { 36 | "command": "open_file", 37 | "args": { 38 | "file": "${packages}/User/Default (OSX).sublime-keymap", 39 | "platform": "OSX" 40 | }, 41 | "caption": "Key Bindings – User" 42 | }, 43 | { 44 | "command": "open_file", 45 | "args": { 46 | "file": "${packages}/User/Default (Linux).sublime-keymap", 47 | "platform": "Linux" 48 | }, 49 | "caption": "Key Bindings – User" 50 | }, 51 | { 52 | "command": "open_file", 53 | "args": { 54 | "file": "${packages}/User/Default (Windows).sublime-keymap", 55 | "platform": "Windows" 56 | }, 57 | "caption": "Key Bindings – User" 58 | }, 59 | { "caption": "-" } 60 | ] 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | 67 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FindKeyConflicts 2 | Assist in finding key conflicts between various plugins. This plugin will report back shortcut keys that are mapped to more than one package. This does not guarantee that the listed plugins are necessarily in conflict, as details, such as context, are ignored. This is simply a tool to help assist what plugins may be conflicting. 3 | 4 | ## Installation 5 | ### Manual 6 | Clone or copy this repository into the packages directory. By default, they are located at: 7 | 8 | * OS X: ~/Library/Application Support/Sublime Text 2/Packages/ 9 | * Windows: %APPDATA%/Roaming/Sublime Text 2/Packages/ 10 | * Linux: ~/.config/sublime-text-2/Packages/ 11 | 12 | ### Package Control 13 | Installation through [package control](http://wbond.net/sublime_packages/package_control) is recommended. It will handle updating your packages as they become available. To install, do the following. 14 | 15 | * In the Command Palette, enter `Package Control: Install Package` 16 | * Search for `FindKeyConflicts` 17 | 18 | ## Usage 19 | This plugin can be run through specifying commands on the command palette. The commands are listed in alphabetical order, beginning with modifiers (alt, cntl, shift, super), followed by keys. The commands are as follows: 20 | 21 | `FindKeyConflicts: All Key Maps to Quick Panel`: 22 | 23 | Displays all key mappings in a quick panel. Selecting an entry will open a buffer with additional details about the key binding. 24 | 25 | `FindKeyConflicts: All Key Maps to Buffer`: 26 | 27 | Displays all key mappings in a buffer. 28 | 29 | `FindKeyConflicts: (Direct) Conflicts to Quick Panel`: 30 | 31 | This command finds all direct key conflicts, and displays them on the quick panel. The last package listed under the command is the source for the command being run, if it is not limited by context. Selecting a particular entry will open a buffer with details about that key binding. 32 | 33 | `FindKeyConflicts: (Direct) Conflicts to Buffer`: 34 | 35 | Display key direct conflicts in a view. Using this will give a better idea of how commands conflict, as the context for the commands will be included in the output. The last package listed for a particular binding is the command that is used, if it is not limited by context. 36 | 37 | `FindKeyConflicts: Overlap Conflicts`: 38 | 39 | Displays key bindings that overlap with mutli part key bindings in a buffer. For example, if `["ctrl+t"]` exists as one binding and `["ctrl+t", "t"]`, exists as another binding, this will be displayed. 40 | 41 | `FindKeyConflicts: All Conflicts`: 42 | 43 | Displays all conflicts in a buffer. This option will include both direct and overlapping conflicts. 44 | 45 | `FindKeyConflicts: Single Package Conflicts`: 46 | 47 | Displays conflicts that involve the selected package. 48 | 49 | `FindKeyConflicts: Multiple Package Conflicts`: 50 | 51 | Displays conflicts that involve the selected packages. Select `(Done)` when you are done selecting packages. You may use `(View Selected)` and `(View Packages)` to view the selected packages and the package list respsectively. Also, you may remove packages from the selected list by pressing `enter` when viewing the selected packages list. 52 | 53 | `FindKeyConflicts: Command Search`: 54 | 55 | Display a list of the packages containing keymap files. After selecting a package, a list of commands will be displayed in the quick panel. Selecting a command from the subsequent list will run the command. 56 | 57 | ## Settings 58 | `ignored_packages`: 59 | 60 | An array containing packages to ignore. Note that the `ignored_packages` in the Preferences are automatically added to this list. 61 | 62 | `ignore_single_key`: 63 | 64 | Boolean value specifying if single key bindings should be ignored. False by default. 65 | 66 | `ignore_patterns`: 67 | 68 | Array containing key patterns to ignore. These should follow the same guidelines as specifying key bindings. 69 | 70 | `display_internal_conflicts`: 71 | 72 | Boolean value used to determine if internal command conflicts to a package should be displayed. 73 | 74 | ## Notes 75 | Thanks to [bizoo](https://github.com/bizoo) for sharing their work with me. 76 | Thanks to [getify](https://github.com/getify) for the json minifier. 77 | Thanks to [facelessuser](https://github.com/facelessuser) for the strip dangling commas work. -------------------------------------------------------------------------------- /find_key_conflicts.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | import os 4 | import json 5 | import threading 6 | import copy 7 | import logging 8 | import traceback 9 | import sys 10 | 11 | VERSION = int(sublime.version()) 12 | 13 | if VERSION >= 3006: 14 | from FindKeyConflicts.lib.package_resources import * 15 | from FindKeyConflicts.lib.strip_commas import strip_dangling_commas 16 | from FindKeyConflicts.lib.minify_json import json_minify 17 | else: 18 | from lib.package_resources import * 19 | from lib.strip_commas import strip_dangling_commas 20 | from lib.minify_json import json_minify 21 | 22 | 23 | PACKAGES_PATH = sublime.packages_path() 24 | PLATFORM = sublime.platform().title() 25 | if PLATFORM == "Osx": 26 | PLATFORM = "OSX" 27 | MODIFIERS = ('shift', 'ctrl', 'alt', 'super') 28 | 29 | DONE_TEXT = "(Done)" 30 | VIEW_SELECTED_LIST_TEXT = "(View Selected)" 31 | VIEW_PACKAGES_LIST_TEXT = "(View Packages)" 32 | SETTINGS_FILE = "FindKeyConflicts.sublime-settings" 33 | 34 | # Set up logger 35 | logger = logging.getLogger(__name__) 36 | logger.setLevel(logging.WARNING) 37 | 38 | if not len(logger.handlers): # Behave better on reloads 39 | _handler = logging.StreamHandler(sys.stdout) 40 | _formatter = logging.Formatter('[%(name)s] %(levelname)s - %(message)s') 41 | 42 | _handler.setFormatter(_formatter) 43 | logger.addHandler(_handler) 44 | 45 | 46 | class GenerateKeymaps(object): 47 | def run(self, package=None): 48 | plugin_settings = sublime.load_settings(SETTINGS_FILE) 49 | 50 | self.window = self.window 51 | self.view = self.window.active_view() 52 | self.display_internal_conflicts = plugin_settings.get( 53 | "display_internal_conflicts", True) 54 | self.show_args = plugin_settings.get("show_args", False) 55 | 56 | packages = get_packages_list() 57 | if package is None: 58 | thread = FindKeyConflictsCall(plugin_settings, packages) 59 | else: 60 | thread = FindPackageCommandsCall(plugin_settings, package) 61 | 62 | thread.start() 63 | self.handle_thread(thread) 64 | 65 | def generate_package_list(self): 66 | plugin_settings = sublime.load_settings("SETTINGS_FILE") 67 | view = self.window.active_view() 68 | packages = get_packages_list() 69 | packages.sort() 70 | 71 | ignored_packages = view.settings().get("ignored_packages", []) 72 | ignored_packages += plugin_settings.get("ignored_packages", []) 73 | 74 | packages = self.remove_ignored_packages(packages, ignored_packages) 75 | return packages 76 | 77 | def handle_thread(self, thread, i=0, move=1): 78 | if thread.is_alive(): 79 | # This animates a little activity indicator in the status area 80 | before = i % 8 81 | after = (7) - before 82 | if not after: 83 | move = -1 84 | if not before: 85 | move = 1 86 | i += move 87 | self.view.set_status('find_key_conflicts', 88 | 'FindKeyConflicts [%s=%s]' % 89 | (' ' * before, ' ' * after)) 90 | 91 | # Timer to check again. 92 | sublime.set_timeout( 93 | lambda: self.handle_thread(thread, i, move), 100) 94 | else: 95 | self.view.erase_status('find_key_conflicts') 96 | sublime.status_message('FindKeyConflicts finished.') 97 | if thread.debug: 98 | content = "" 99 | for package in thread.debug_minified: 100 | content += "%s\n" % package 101 | content += "%s\n" % thread.debug_minified[package] 102 | 103 | panel = sublime.active_window().new_file() 104 | panel.set_scratch(True) 105 | panel.settings().set('word_wrap', False) 106 | panel.set_name("Debug") 107 | panel.run_command("insert_content", {"content": content}) 108 | self.handle_results(thread.all_key_map) 109 | 110 | def handle_results(self, all_key_map): 111 | raise NotImplementedError("Should have implemented this") 112 | 113 | def remove_ignored_packages(self, packages, ignored_packages): 114 | for ignored_package in ignored_packages: 115 | try: 116 | packages.remove(ignored_package) 117 | except: 118 | logger.warning("FindKeyConflicts: Package '" + 119 | ignored_package + "' does not exist.") 120 | 121 | return packages 122 | 123 | def remove_non_conflicts(self, all_key_map): 124 | keylist = list(all_key_map.keys()) 125 | 126 | keylist.sort() 127 | new_key_map = {} 128 | for key in keylist: 129 | value = all_key_map[key] 130 | if len(value["packages"]) > 1: 131 | new_key_map[key] = value 132 | elif len(value[value["packages"][0]]) > 1 and self.display_internal_conflicts: 133 | new_key_map[key] = value 134 | return new_key_map 135 | 136 | def find_overlap_conflicts(self, all_key_map): 137 | keylist = list(all_key_map.keys()) 138 | keylist.sort() 139 | conflicts = {} 140 | for key in keylist: 141 | for key_nested in keylist: 142 | if key_nested.startswith(key + ","): 143 | if key in conflicts: 144 | conflicts[key].append(key_nested) 145 | else: 146 | conflicts[key] = [key_nested] 147 | return conflicts 148 | 149 | 150 | class GenerateOutput(object): 151 | def __init__(self, all_key_map, show_args, window=None): 152 | self.window = window 153 | self.all_key_map = all_key_map 154 | self.show_args = show_args 155 | 156 | def generate_header(self, header): 157 | return '%s\n%s\n%s\n' % ('-' * len(header), header, '-' * len(header)) 158 | 159 | def generate_overlapping_key_text(self, conflict_map): 160 | content = "" 161 | keys = list(conflict_map.keys()) 162 | keys.sort() 163 | potential_conflicts_keys = list(conflict_map.keys()) 164 | potential_conflicts_keys.sort() 165 | offset = 2 166 | for key_string in potential_conflicts_keys: 167 | content += self.generate_text(key_string, self.all_key_map, 0) 168 | for conflict in conflict_map[key_string]: 169 | content += self.generate_text(conflict, self.all_key_map, 170 | offset, "(", ")") 171 | return content 172 | 173 | def generate_key_map_text(self, key_map): 174 | content = '' 175 | keys = list(key_map.keys()) 176 | 177 | keys.sort() 178 | for key_string in keys: 179 | content += self.generate_text(key_string, key_map) 180 | 181 | return content 182 | 183 | def generate_file(self, content, name="Keys"): 184 | panel = sublime.active_window().new_file() 185 | panel.set_scratch(True) 186 | panel.settings().set('word_wrap', False) 187 | panel.set_name(name) 188 | # content output 189 | panel.run_command("insert_content", {"content": content}) 190 | 191 | def longest_command_length(self, key_map): 192 | pass 193 | 194 | def longest_package_length(self, key_map): 195 | pass 196 | 197 | def generate_text(self, key_string, key_map, offset=0, key_wrap_in='[', 198 | key_wrap_out=']'): 199 | content = '' 200 | item = key_map.get(key_string) 201 | content += " " * offset 202 | content += ' %s%s%s\n' % (key_wrap_in, key_string, key_wrap_out) 203 | packages = item.get("packages") 204 | misconfigured_command_message = '' 205 | for package in packages: 206 | package_map = item.get(package) 207 | for entry in package_map: 208 | if 'command' not in entry: 209 | misconfigured_command_message += '%s in %s does not ' \ 210 | 'have a command\n' % (key_string, package) 211 | continue 212 | content += " " * offset 213 | content += ' %*s %*s %s\n' % \ 214 | (-40 + offset, entry['command'], -20, package, 215 | json.dumps(entry['context']) if "context" in entry else '') 216 | 217 | if misconfigured_command_message: 218 | sublime.error_message(misconfigured_command_message) 219 | 220 | return content 221 | 222 | def generate_output_quick_panel(self, key_map): 223 | self.key_map = key_map 224 | quick_panel_items = [] 225 | keylist = list(key_map.keys()) 226 | keylist.sort() 227 | self.list = [] 228 | for key in keylist: 229 | self.list.append(key) 230 | value = key_map[key] 231 | quick_panel_item = [key, ", ".join(value["packages"])] 232 | quick_panel_items.append(quick_panel_item) 233 | 234 | self.window.show_quick_panel(quick_panel_items, 235 | self.quick_panel_callback) 236 | 237 | def quick_panel_callback(self, index): 238 | if index == -1: 239 | return 240 | entry = self.list[index] 241 | content = self.generate_header("Entry Details") 242 | content += self.generate_text(entry, self.key_map) 243 | self.generate_file(content, "[%s] Details" % entry) 244 | 245 | 246 | class FindKeyConflictsCommand(GenerateKeymaps, sublime_plugin.WindowCommand): 247 | def run(self, output="quick_panel"): 248 | self.output = output 249 | GenerateKeymaps.run(self) 250 | 251 | def handle_results(self, all_key_map): 252 | output = GenerateOutput(all_key_map, self.show_args, self.window) 253 | 254 | new_key_map = self.remove_non_conflicts(all_key_map) 255 | if self.output == "quick_panel": 256 | output.generate_output_quick_panel(new_key_map) 257 | elif self.output == "buffer": 258 | content = output.generate_header("Key Conflicts (Only direct conflicts)") 259 | content += output.generate_key_map_text(new_key_map) 260 | output.generate_file(content, "Key Conflicts") 261 | else: 262 | logger.warning("FindKeyConflicts[Warning]: Invalid output type specified") 263 | 264 | 265 | class FindAllKeyConflictsCommand(GenerateKeymaps, sublime_plugin.WindowCommand): 266 | def run(self): 267 | GenerateKeymaps.run(self) 268 | 269 | def handle_results(self, all_key_map): 270 | output = GenerateOutput(all_key_map, self.show_args) 271 | new_key_map = self.remove_non_conflicts(all_key_map) 272 | overlapping_confilicts_map = self.find_overlap_conflicts(all_key_map) 273 | 274 | content = output.generate_header("Multi Part Key Conflicts") 275 | content += output.generate_overlapping_key_text(overlapping_confilicts_map) 276 | content += output.generate_header("Key Conflicts (Only direct conflicts)") 277 | content += output.generate_key_map_text(new_key_map) 278 | output.generate_file(content, "All Key Conflicts") 279 | 280 | 281 | class FindOverlapConflictsCommand(GenerateKeymaps, sublime_plugin.WindowCommand): 282 | def run(self): 283 | GenerateKeymaps.run(self) 284 | 285 | def handle_results(self, all_key_map): 286 | output = GenerateOutput(all_key_map, self.show_args) 287 | overlapping_confilicts_map = self.find_overlap_conflicts(all_key_map) 288 | 289 | content = output.generate_header("Multi Part Key Conflicts") 290 | content += output.generate_overlapping_key_text(overlapping_confilicts_map) 291 | output.generate_file(content, "Overlap Key Conflicts") 292 | 293 | 294 | class FindKeyMappingsCommand(GenerateKeymaps, sublime_plugin.WindowCommand): 295 | def run(self, output="quick_panel"): 296 | self.output = output 297 | GenerateKeymaps.run(self) 298 | 299 | def handle_results(self, all_key_map): 300 | output = GenerateOutput(all_key_map, self.show_args, self.window) 301 | if self.output == "quick_panel": 302 | output.generate_output_quick_panel(all_key_map) 303 | elif self.output == "buffer": 304 | content = output.generate_header("All Key Mappings") 305 | content += output.generate_key_map_text(all_key_map) 306 | output.generate_file(content, "All Key Mappings") 307 | else: 308 | logger.warning("FindKeyConflicts[Warning]: Invalid output type specified") 309 | 310 | 311 | class FindKeyConflictsWithPackageCommand(GenerateKeymaps, sublime_plugin.WindowCommand): 312 | def run(self, multiple=False): 313 | self.package_list = [entry for entry in GenerateKeymaps.generate_package_list(self)] 314 | self.multiple = multiple 315 | self.selected_list = [] 316 | 317 | self.generate_quick_panel(self.package_list, self.package_list_callback, False) 318 | 319 | def generate_quick_panel(self, packages, callback, selected_list): 320 | self.quick_panel_list = copy.copy(packages) 321 | if self.multiple: 322 | if selected_list: 323 | self.quick_panel_list.insert(0, VIEW_PACKAGES_LIST_TEXT) 324 | elif self.selected_list: 325 | self.quick_panel_list.insert(0, VIEW_SELECTED_LIST_TEXT) 326 | self.quick_panel_list.insert(0, DONE_TEXT) 327 | sublime.set_timeout(lambda: self.window.show_quick_panel(self.quick_panel_list, callback), 10) 328 | 329 | def selected_list_callback(self, index): 330 | if index == -1: 331 | return 332 | 333 | entry_text = self.quick_panel_list[index] 334 | if entry_text != VIEW_PACKAGES_LIST_TEXT and entry_text != DONE_TEXT: 335 | self.package_list.append(entry_text) 336 | self.selected_list.remove(entry_text) 337 | self.package_list.sort() 338 | 339 | if entry_text == DONE_TEXT: 340 | if len(self.selected_list) > 0: 341 | GenerateKeymaps.run(self) 342 | elif entry_text == VIEW_PACKAGES_LIST_TEXT: 343 | self.generate_quick_panel(self.package_list, self.package_list_callback, False) 344 | else: 345 | self.generate_quick_panel(self.selected_list, self.selected_list_callback, True) 346 | 347 | def package_list_callback(self, index): 348 | if index == -1: 349 | return 350 | 351 | if self.quick_panel_list[index] != DONE_TEXT and self.quick_panel_list[index] != VIEW_SELECTED_LIST_TEXT: 352 | self.selected_list.append(self.quick_panel_list[index]) 353 | self.package_list.remove(self.quick_panel_list[index]) 354 | self.selected_list.sort() 355 | 356 | if not self.multiple or self.quick_panel_list[index] == DONE_TEXT: 357 | if len(self.selected_list) > 0: 358 | GenerateKeymaps.run(self) 359 | elif self.quick_panel_list[index] == VIEW_SELECTED_LIST_TEXT: 360 | self.generate_quick_panel(self.selected_list, self.selected_list_callback, True) 361 | else: 362 | self.generate_quick_panel(self.package_list, self.package_list_callback, False) 363 | 364 | def handle_results(self, all_key_map): 365 | output = GenerateOutput(all_key_map, self.show_args) 366 | 367 | output_keymap = {} 368 | overlapping_conflicts_map = {} 369 | conflict_key_map = self.remove_non_conflicts(all_key_map) 370 | all_overlapping_confilicts_map = self.find_overlap_conflicts(all_key_map) 371 | for key in conflict_key_map: 372 | package_list = conflict_key_map[key]["packages"] 373 | for package in self.selected_list: 374 | if package in package_list: 375 | output_keymap[key] = conflict_key_map[key] 376 | break 377 | 378 | for overlap_base_key in all_overlapping_confilicts_map: 379 | for package in self.selected_list: 380 | if package in all_key_map[overlap_base_key]["packages"]: 381 | overlapping_conflicts_map[overlap_base_key] = all_overlapping_confilicts_map[overlap_base_key] 382 | break 383 | 384 | for overlap_key in all_overlapping_confilicts_map[overlap_base_key]: 385 | if package in all_key_map[overlap_key]["packages"]: 386 | overlapping_conflicts_map[overlap_base_key] = all_overlapping_confilicts_map[overlap_base_key] 387 | break 388 | 389 | content = "Key conflicts involving the following packages:\n" 390 | content += ", ".join(self.selected_list) + "\n\n" 391 | 392 | content += output.generate_header("Multi Part Key Conflicts") 393 | content += output.generate_overlapping_key_text(overlapping_conflicts_map) 394 | content += output.generate_header("Key Conflicts") 395 | content += output.generate_key_map_text(output_keymap) 396 | output.generate_file(content, "Key Conflicts") 397 | 398 | 399 | class FindKeyConflictsCommandSearchCommand(GenerateKeymaps, sublime_plugin.WindowCommand): 400 | def run(self): 401 | packages = [entry for entry in GenerateKeymaps.generate_package_list(self)] 402 | self.package_list = [] 403 | 404 | for package in packages: 405 | if len(find_resource("Default( \(%s\))?.sublime-keymap$" % PLATFORM, package)) > 0: 406 | self.package_list.append(package) 407 | 408 | self.generate_quick_panel(self.package_list, self.package_list_callback) 409 | 410 | def generate_quick_panel(self, packages, callback): 411 | self.window.show_quick_panel(packages, callback) 412 | 413 | def package_list_callback(self, index): 414 | if index == -1: 415 | return 416 | GenerateKeymaps.run(self, self.package_list[index]) 417 | 418 | def handle_results(self, key_binding_commands): 419 | self.key_bindings = key_binding_commands 420 | entries = [] 421 | for key_entry in key_binding_commands: 422 | entry = [] 423 | entry.append(str(key_entry["command"])) 424 | entry.append(str(key_entry["keys"])) 425 | if "args" in key_entry: 426 | entry.append(str(key_entry["args"])) 427 | entries.append(entry) 428 | self.window.show_quick_panel(entries, self.entry_callback) 429 | 430 | def entry_callback(self, index): 431 | if index == -1: 432 | return 433 | command = self.key_bindings[index]["command"] 434 | args = None 435 | if "args" in self.key_bindings[index]: 436 | args = self.key_bindings[index]["args"] 437 | view = self.window.active_view() 438 | if view is not None: 439 | view.run_command(command, args) 440 | self.window.run_command(command, args) 441 | sublime.run_command(command, args) 442 | 443 | 444 | class ThreadBase(threading.Thread): 445 | def manage_package(self, package): 446 | self.done = False 447 | file_list = list_package_files(package) 448 | platform_keymap = "default (%s).sublime-keymap" % (PLATFORM.lower()) 449 | for filename in file_list: 450 | if filename.lower().endswith("default.sublime-keymap") or filename.lower().endswith(platform_keymap): 451 | content = get_resource(package, filename) 452 | if content is None: 453 | continue 454 | 455 | try: 456 | if VERSION < 3013: 457 | minified_content = json_minify(content) 458 | minified_content = strip_dangling_commas(minified_content) 459 | minified_content = minified_content.replace("\n", "\\\n") 460 | if self.debug: 461 | self.debug_minified[package] = minified_content 462 | key_map = json.loads(minified_content) 463 | else: 464 | key_map = sublime.decode_value(content) 465 | except: 466 | if not self.prev_error: 467 | traceback.print_exc() 468 | self.prev_error = True 469 | sublime.error_message("Could not parse a keymap file. See console for details") 470 | #error_path = os.path.join(os.path.basename(orig_path), filename) 471 | logger.warning("FindKeyConflicts[Warning]: An error " + "occured while parsing '" + package + "'") 472 | continue 473 | if key_map is not None: 474 | self.handle_key_map(package, key_map) 475 | self.done = True 476 | 477 | def check_ignore(self, key_array): 478 | if ",".join(key_array) in self.ignore_patterns: 479 | return True 480 | if len(key_array) > 1 or not self.ignore_single_key: 481 | return False 482 | 483 | for key_string in key_array: 484 | split_keys = key_string.split("+") 485 | try: 486 | i = split_keys.index("") 487 | split_keys[i] = "+" 488 | split_keys.remove("") 489 | except: 490 | pass 491 | 492 | if len(split_keys) == 1 and self.ignore_single_key: 493 | return True 494 | 495 | return False 496 | 497 | def order_key_string(self, key_string): 498 | split_keys = key_string.split("+") 499 | try: 500 | i = split_keys.index("") 501 | split_keys[i] = "+" 502 | split_keys.remove("") 503 | except: 504 | pass 505 | 506 | modifiers = [] 507 | keys = [] 508 | for key in split_keys: 509 | if key in MODIFIERS: 510 | modifiers.append(key) 511 | else: 512 | keys.append(key) 513 | modifiers.sort() 514 | keys.sort() 515 | ordered_key_string = "+".join(modifiers + keys) 516 | return ordered_key_string 517 | 518 | def handle_key_map(self, package, key_map): 519 | raise NotImplementedError("Should have implemented this") 520 | 521 | 522 | class FindKeyConflictsCall(ThreadBase): 523 | def __init__(self, settings, packages): 524 | self.ignore_single_key = settings.get("ignore_single_key", False) 525 | self.ignore_patterns = settings.get("ignore_patterns", []) 526 | self.packages = packages 527 | self.all_key_map = {} 528 | self.debug_minified = {} 529 | self.debug = settings.get("debug", False) 530 | self.prev_error = False 531 | threading.Thread.__init__(self) 532 | 533 | def run(self): 534 | run_user = False 535 | temp = [] 536 | for ignore_pattern in self.ignore_patterns: 537 | temp.append(self.order_key_string(ignore_pattern)) 538 | self.ignore_patterns = temp 539 | if "Default" in self.packages: 540 | self.manage_package("Default") 541 | self.packages.remove("Default") 542 | if "User" in self.packages: 543 | run_user = True 544 | self.packages.remove("User") 545 | 546 | for package in self.packages: 547 | self.manage_package(package) 548 | if run_user: 549 | self.manage_package("User") 550 | 551 | def handle_key_map(self, package, key_map): 552 | for entry in key_map: 553 | keys = entry["keys"] 554 | # if "context" in entry: 555 | # print(entry["context"]) 556 | # entry["context"].sort() 557 | key_array = [] 558 | key_string = "" 559 | for key in keys: 560 | key_array.append(self.order_key_string(key)) 561 | 562 | if self.check_ignore(key_array): 563 | continue 564 | key_string = ",".join(key_array) 565 | 566 | if key_string in self.all_key_map: 567 | tmp = self.all_key_map.get(key_string) 568 | if package not in tmp["packages"]: 569 | tmp["packages"].append(package) 570 | tmp[package] = [entry] 571 | else: 572 | tmp[package].append(entry) 573 | 574 | self.all_key_map[key_string] = tmp 575 | else: 576 | new_entry = {} 577 | new_entry["packages"] = [package] 578 | new_entry[package] = [entry] 579 | self.all_key_map[key_string] = new_entry 580 | 581 | 582 | class FindPackageCommandsCall(ThreadBase): 583 | def __init__(self, settings, package): 584 | self.package = package 585 | self.all_key_map = [] 586 | self.debug_minified = {} 587 | self.debug = settings.get("debug", False) 588 | self.prev_error = False 589 | threading.Thread.__init__(self) 590 | 591 | def run(self): 592 | self.manage_package(self.package) 593 | 594 | def handle_key_map(self, package, key_map): 595 | for entry in key_map: 596 | keys = entry["keys"] 597 | key_array = [] 598 | key_string = "" 599 | for key in keys: 600 | key_array.append(self.order_key_string(key)) 601 | 602 | key_string = ",".join(key_array) 603 | 604 | entry["keys"] = key_string 605 | self.all_key_map.append(entry) 606 | 607 | 608 | class InsertContentCommand(sublime_plugin.TextCommand): 609 | def run(self, edit, content): 610 | self.view.insert(edit, 0, content) 611 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skuroda/FindKeyConflicts/7875ee074f0e10ff30f969c32e1510a9906f50e6/lib/__init__.py -------------------------------------------------------------------------------- /lib/minify_json.py: -------------------------------------------------------------------------------- 1 | """A port of the `JSON-minify` utility to the Python language. 2 | Based on JSON.minify.js: https://github.com/getify/JSON.minify 3 | Contributers: 4 | - Gerald Storer 5 | - Contributed original version 6 | - Felipe Machado 7 | - Performance optimization 8 | - Pradyun S. Gedam 9 | - Conditions and variable names changed 10 | - Reformatted tests and moved to separate file 11 | - Made into a PyPI Package 12 | """ 13 | 14 | import re 15 | 16 | 17 | def json_minify(string, strip_space=True): 18 | tokenizer = re.compile('"|(/\*)|(\*/)|(//)|\n|\r') 19 | end_slashes_re = re.compile(r'(\\)*$') 20 | 21 | in_string = False 22 | in_multi = False 23 | in_single = False 24 | 25 | new_str = [] 26 | index = 0 27 | 28 | for match in re.finditer(tokenizer, string): 29 | 30 | if not (in_multi or in_single): 31 | tmp = string[index:match.start()] 32 | if not in_string and strip_space: 33 | # replace white space as defined in standard 34 | tmp = re.sub('[ \t\n\r]+', '', tmp) 35 | new_str.append(tmp) 36 | 37 | index = match.end() 38 | val = match.group() 39 | 40 | if val == '"' and not (in_multi or in_single): 41 | escaped = end_slashes_re.search(string, 0, match.start()) 42 | 43 | # start of string or unescaped quote character to end string 44 | if not in_string or (escaped is None or len(escaped.group()) % 2 == 0): # noqa 45 | in_string = not in_string 46 | index -= 1 # include " character in next catch 47 | elif not (in_string or in_multi or in_single): 48 | if val == '/*': 49 | in_multi = True 50 | elif val == '//': 51 | in_single = True 52 | elif val == '*/' and in_multi and not (in_string or in_single): 53 | in_multi = False 54 | elif val in '\r\n' and not (in_multi or in_string) and in_single: 55 | in_single = False 56 | elif not ((in_multi or in_single) or (val in ' \r\n\t' and strip_space)): # noqa 57 | new_str.append(val) 58 | 59 | new_str.append(string[index:]) 60 | return ''.join(new_str) 61 | -------------------------------------------------------------------------------- /lib/package_resources.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | Copyright (c) 2013 Scott Kuroda 4 | 5 | SHA: d10b8514a1a7c06ef18677ef07256db65aefff4f 6 | """ 7 | import sublime 8 | import os 9 | import zipfile 10 | import tempfile 11 | import re 12 | import codecs 13 | 14 | __all__ = [ 15 | "get_resource", 16 | "get_binary_resource", 17 | "find_resource", 18 | "list_package_files", 19 | "get_package_and_resource_name", 20 | "get_packages_list" 21 | ] 22 | 23 | 24 | VERSION = int(sublime.version()) 25 | 26 | def get_resource(package_name, resource, encoding="utf-8"): 27 | return _get_resource(package_name, resource, encoding=encoding) 28 | 29 | def get_binary_resource(package_name, resource): 30 | return _get_resource(package_name, resource, return_binary=True) 31 | 32 | def _get_resource(package_name, resource, return_binary=False, encoding="utf-8"): 33 | packages_path = sublime.packages_path() 34 | content = None 35 | if VERSION > 3013: 36 | try: 37 | if return_binary: 38 | content = sublime.load_binary_resource("Packages/" + package_name + "/" + resource) 39 | else: 40 | content = sublime.load_resource("Packages/" + package_name + "/" + resource) 41 | except IOError: 42 | pass 43 | else: 44 | path = None 45 | if os.path.exists(os.path.join(packages_path, package_name, resource)): 46 | path = os.path.join(packages_path, package_name, resource) 47 | content = _get_directory_item_content(path, return_binary, encoding) 48 | 49 | if VERSION >= 3006: 50 | sublime_package = package_name + ".sublime-package" 51 | 52 | packages_path = sublime.installed_packages_path() 53 | if content is None: 54 | if os.path.exists(os.path.join(packages_path, sublime_package)): 55 | content = _get_zip_item_content(os.path.join(packages_path, sublime_package), resource, return_binary, encoding) 56 | 57 | packages_path = os.path.dirname(sublime.executable_path()) + os.sep + "Packages" 58 | 59 | if content is None: 60 | if os.path.exists(os.path.join(packages_path, sublime_package)): 61 | content = _get_zip_item_content(os.path.join(packages_path, sublime_package), resource, return_binary, encoding) 62 | 63 | return content 64 | 65 | 66 | def find_resource(resource_pattern, package=None): 67 | file_set = set() 68 | if package == None: 69 | for package in get_packages_list(): 70 | file_set.update(find_resource(resource_pattern, package)) 71 | 72 | ret_list = list(file_set) 73 | else: 74 | file_set.update(_find_directory_resource(os.path.join(sublime.packages_path(), package), resource_pattern)) 75 | 76 | if VERSION >= 3006: 77 | zip_location = os.path.join(sublime.installed_packages_path(), package + ".sublime-package") 78 | file_set.update(_find_zip_resource(zip_location, resource_pattern)) 79 | zip_location = os.path.join(os.path.dirname(sublime.executable_path()), "Packages", package + ".sublime-package") 80 | file_set.update(_find_zip_resource(zip_location, resource_pattern)) 81 | ret_list = map(lambda e: package + "/" + e, file_set) 82 | 83 | return sorted(ret_list) 84 | 85 | 86 | def list_package_files(package, ignore_patterns=[]): 87 | """ 88 | List files in the specified package. 89 | """ 90 | package_path = os.path.join(sublime.packages_path(), package, "") 91 | path = None 92 | file_set = set() 93 | file_list = [] 94 | if os.path.exists(package_path): 95 | for root, directories, filenames in os.walk(package_path): 96 | temp = root.replace(package_path, "") 97 | for filename in filenames: 98 | file_list.append(os.path.join(temp, filename)) 99 | 100 | file_set.update(file_list) 101 | 102 | if VERSION >= 3006: 103 | sublime_package = package + ".sublime-package" 104 | packages_path = sublime.installed_packages_path() 105 | 106 | if os.path.exists(os.path.join(packages_path, sublime_package)): 107 | file_set.update(_list_files_in_zip(packages_path, sublime_package)) 108 | 109 | packages_path = os.path.dirname(sublime.executable_path()) + os.sep + "Packages" 110 | 111 | if os.path.exists(os.path.join(packages_path, sublime_package)): 112 | file_set.update(_list_files_in_zip(packages_path, sublime_package)) 113 | 114 | file_list = [] 115 | 116 | for filename in file_set: 117 | if not _ignore_file(filename, ignore_patterns): 118 | file_list.append(_normalize_to_sublime_path(filename)) 119 | 120 | return sorted(file_list) 121 | 122 | def _ignore_file(filename, ignore_patterns=[]): 123 | ignore = False 124 | directory, base = os.path.split(filename) 125 | for pattern in ignore_patterns: 126 | if re.match(pattern, base): 127 | return True 128 | 129 | if len(directory) > 0: 130 | ignore = _ignore_file(directory, ignore_patterns) 131 | 132 | return ignore 133 | 134 | 135 | def _normalize_to_sublime_path(path): 136 | path = os.path.normpath(path) 137 | path = re.sub(r"^([a-zA-Z]):", "/\\1", path) 138 | path = re.sub(r"\\", "/", path) 139 | return path 140 | 141 | def get_package_and_resource_name(path): 142 | """ 143 | This method will return the package name and resource name from a path. 144 | 145 | Arguments: 146 | path Path to parse for package and resource name. 147 | """ 148 | package = None 149 | resource = None 150 | path = _normalize_to_sublime_path(path) 151 | if os.path.isabs(path): 152 | packages_path = _normalize_to_sublime_path(sublime.packages_path()) 153 | if path.startswith(packages_path): 154 | package, resource = _search_for_package_and_resource(path, packages_path) 155 | 156 | if int(sublime.version()) >= 3006: 157 | packages_path = _normalize_to_sublime_path(sublime.installed_packages_path()) 158 | if path.startswith(packages_path): 159 | package, resource = _search_for_package_and_resource(path, packages_path) 160 | 161 | packages_path = _normalize_to_sublime_path(os.path.dirname(sublime.executable_path()) + os.sep + "Packages") 162 | if path.startswith(packages_path): 163 | package, resource = _search_for_package_and_resource(path, packages_path) 164 | else: 165 | path = re.sub(r"^Packages/", "", path) 166 | split = re.split(r"/", path, 1) 167 | package = split[0] 168 | package = package.replace(".sublime-package", "") 169 | resource = split[1] 170 | 171 | return (package, resource) 172 | 173 | def get_packages_list(ignore_packages=True): 174 | """ 175 | Return a list of packages. 176 | """ 177 | package_set = set() 178 | package_set.update(_get_packages_from_directory(sublime.packages_path())) 179 | 180 | if int(sublime.version()) >= 3006: 181 | package_set.update(_get_packages_from_directory(sublime.installed_packages_path(), ".sublime-package")) 182 | 183 | executable_package_path = os.path.dirname(sublime.executable_path()) + os.sep + "Packages" 184 | package_set.update(_get_packages_from_directory(executable_package_path, ".sublime-package")) 185 | 186 | if ignore_packages: 187 | ignored_package_list = sublime.load_settings( 188 | "Preferences.sublime-settings").get("ignored_packages", []) 189 | for ignored in ignored_package_list: 190 | package_set.discard(ignored) 191 | 192 | return sorted(list(package_set)) 193 | 194 | def _get_packages_from_directory(directory, file_ext=""): 195 | package_list = [] 196 | for package in os.listdir(directory): 197 | if not package.endswith(file_ext): 198 | continue 199 | else: 200 | package = package.replace(file_ext, "") 201 | 202 | package_list.append(package) 203 | return package_list 204 | 205 | def _search_for_package_and_resource(path, packages_path): 206 | """ 207 | Derive the package and resource from a path. 208 | """ 209 | relative_package_path = path.replace(packages_path + "/", "") 210 | 211 | package, resource = re.split(r"/", relative_package_path, 1) 212 | package = package.replace(".sublime-package", "") 213 | return (package, resource) 214 | 215 | 216 | def _list_files_in_zip(package_path, package): 217 | if not os.path.exists(os.path.join(package_path, package)): 218 | return [] 219 | 220 | ret_value = [] 221 | with zipfile.ZipFile(os.path.join(package_path, package)) as zip_file: 222 | ret_value = zip_file.namelist() 223 | return ret_value 224 | 225 | def _get_zip_item_content(path_to_zip, resource, return_binary, encoding): 226 | if not os.path.exists(path_to_zip): 227 | return None 228 | 229 | ret_value = None 230 | 231 | with zipfile.ZipFile(path_to_zip) as zip_file: 232 | namelist = zip_file.namelist() 233 | if resource in namelist: 234 | ret_value = zip_file.read(resource) 235 | if not return_binary: 236 | ret_value = ret_value.decode(encoding) 237 | 238 | return ret_value 239 | 240 | def _get_directory_item_content(filename, return_binary, encoding): 241 | content = None 242 | if os.path.exists(filename): 243 | if return_binary: 244 | mode = "rb" 245 | encoding = None 246 | else: 247 | mode = "r" 248 | with codecs.open(filename, mode, encoding=encoding) as file_obj: 249 | content = file_obj.read() 250 | return content 251 | 252 | def _find_zip_resource(path_to_zip, pattern): 253 | ret_list = [] 254 | if os.path.exists(path_to_zip): 255 | with zipfile.ZipFile(path_to_zip) as zip_file: 256 | namelist = zip_file.namelist() 257 | for name in namelist: 258 | if re.search(pattern, name): 259 | ret_list.append(name) 260 | 261 | return ret_list 262 | 263 | def _find_directory_resource(path, pattern): 264 | ret_list = [] 265 | if os.path.exists(path): 266 | path = os.path.join(path, "") 267 | for root, directories, filenames in os.walk(path): 268 | temp = root.replace(path, "") 269 | for filename in filenames: 270 | if re.search(pattern, os.path.join(temp, filename)): 271 | ret_list.append(os.path.join(temp, filename)) 272 | return ret_list 273 | 274 | def extract_zip_resource(path_to_zip, resource, extract_dir=None): 275 | if extract_dir is None: 276 | extract_dir = tempfile.mkdtemp() 277 | 278 | file_location = None 279 | if os.path.exists(path_to_zip): 280 | with zipfile.ZipFile(path_to_zip) as zip_file: 281 | file_location = zip_file.extract(resource, extract_dir) 282 | 283 | return file_location 284 | 285 | ####################### Force resource viewer to reload ######################## 286 | import sys 287 | if VERSION > 3000: 288 | from imp import reload 289 | if "FindKeyConflicts.find_key_conflicts" in sys.modules: 290 | reload(sys.modules["FindKeyConflicts.find_key_conflicts"]) 291 | else: 292 | if "find_key_conflicts" in sys.modules: 293 | reload(sys.modules["find_key_conflicts"]) 294 | -------------------------------------------------------------------------------- /lib/strip_commas.py: -------------------------------------------------------------------------------- 1 | ''' 2 | File Strip 3 | Licensed under MIT 4 | Copyright (c) 2012 Isaac Muse 5 | ''' 6 | 7 | import re 8 | import sublime 9 | 10 | def strip_dangling_commas(text, preserve_lines=False): 11 | regex = re.compile( 12 | # ([1st group] dangling commas) | ([8th group] everything else) 13 | r"""((,([\s\r\n]*)(\]))|(,([\s\r\n]*)(\})))|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|.[^,"']*)""", 14 | re.MULTILINE | re.DOTALL 15 | ) 16 | 17 | def remove_comma(m, preserve_lines=False): 18 | if preserve_lines: 19 | # ,] -> ] else ,} -> } 20 | return m.group(3) + m.group(4) if m.group(2) else m.group(6) + m.group(7) 21 | else: 22 | # ,] -> ] else ,} -> } 23 | return m.group(4) if m.group(2) else m.group(7) 24 | 25 | return ( 26 | ''.join( 27 | map( 28 | lambda m: m.group(8) if m.group(8) else remove_comma(m, preserve_lines), 29 | regex.finditer(text) 30 | ) 31 | ) 32 | ) 33 | 34 | ####################### Force resource viewer to reload ######################## 35 | import sys 36 | if int(sublime.version()) > 3000: 37 | from imp import reload 38 | if "FindKeyConflicts.find_key_conflicts" in sys.modules: 39 | reload(sys.modules["FindKeyConflicts.find_key_conflicts"]) 40 | else: 41 | if "find_key_conflicts" in sys.modules: 42 | reload(sys.modules["find_key_conflicts"]) 43 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "2012.11.10.15.45.00": "messages/1.txt", 4 | "2012.11.29.15.00.00": "messages/2.txt", 5 | "2013.03.04.08.00.00": "messages/3.txt" 6 | } -------------------------------------------------------------------------------- /messages/1.txt: -------------------------------------------------------------------------------- 1 | New Features: 2 | - Option to display internal package key conflicts as well (Default is true) -------------------------------------------------------------------------------- /messages/2.txt: -------------------------------------------------------------------------------- 1 | New Features: 2 | - Add command to display overlapping conflicts. 3 | - Add command to display conflicts from selected packages. 4 | 5 | Please see the README for more information about using these new features. 6 | 7 | Bug Fixes: 8 | - Prevent non directory files from being added to the packages list. 9 | - Fix for minifier where comments occur on the last line. 10 | -------------------------------------------------------------------------------- /messages/3.txt: -------------------------------------------------------------------------------- 1 | New Features: 2 | - Add command to view commands for a particular package. Upon selecting the command, it will be run. 3 | - Add Sublime Text 3 compatibility. 4 | 5 | Please see the README for more information about using these new features. -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | Thank you for installing the FindKeyConflicts plugin. 2 | 3 | For more information please review the README or visit https://github.com/skuroda/FindKeyConflicts. 4 | 5 | If you have any questions, comments, or run into issues, please let me know! Hope you enjoy the plugin. 6 | 7 | Thank you! --------------------------------------------------------------------------------