├── .github └── workflows │ └── test.yaml ├── .gitignore ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── LICENSE ├── Main.sublime-menu ├── MultiEditUtils.py ├── MultiEditUtils.sublime-settings ├── README.md ├── changelogs └── 1.6.0.txt ├── messages.json ├── selection_fields.py └── tests ├── testMultiEditUtils.py ├── testPreserveCase.py └── testSelectionFields.py /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-tests: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | st-version: [3, 4] 11 | os: ["ubuntu-latest", "macOS-latest", "windows-latest"] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: SublimeText/UnitTesting/actions/setup@v1 16 | with: 17 | sublime-text-version: ${{ matrix.st-version }} 18 | package-name: MultiEditUtils 19 | - uses: SublimeText/UnitTesting/actions/run-tests@v1 20 | with: 21 | package-name: MultiEditUtils 22 | coverage: false 23 | codecov-upload: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.sublime-project 3 | *.sublime-workspace -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["shift+escape"], "command": "jump_to_last_region" }, 3 | { "keys": ["ctrl+alt+u"], "command": "add_last_selection" }, 4 | { "keys": ["ctrl+alt+c"], "command": "cycle_through_regions" }, 5 | { "keys": ["ctrl+alt+n"], "command": "normalize_region_ends" }, 6 | { "keys": ["ctrl+alt+,"], "command": "split_selection" }, 7 | { "keys": ["ctrl+alt+s"], "command": "strip_selection" }, 8 | { "keys": ["ctrl+alt+r"], "command": "remove_empty_regions", "context": 9 | [ 10 | {"key": "setting.is_widget", "operator": "equal", "operand": false} 11 | ] 12 | }, 13 | 14 | // Multi Find All example keybindings, uncomment to activate 15 | 16 | // // main keybinding, set the search type you're most comfortable with, default is Case+Word 17 | // { "keys": ["ctrl+alt+f", "ctrl+alt+f"], "command": "multi_find_all", "args": {"case": true, "word": true}}, 18 | 19 | // { "keys": ["ctrl+alt+f", "ctrl+c"], "command": "multi_find_all", "args": {"case": false}}, 20 | // { "keys": ["ctrl+alt+f", "c"], "command": "multi_find_all", "args": {"case": true}}, 21 | // { "keys": ["ctrl+alt+f", "ctrl+w"], "command": "multi_find_all", "args": {"case": false, "word": true}}, 22 | // { "keys": ["ctrl+alt+f", "w"], "command": "multi_find_all", "args": {"case": true, "word": true}}, 23 | // { "keys": ["ctrl+alt+f", "q"], "command": "multi_find_all", "args": {"case": true, "word": true, "ignore_comments": true}}, 24 | 25 | // // find all with regex search, additive(on top of current selection) or subtractive 26 | // { "keys": ["ctrl+alt+f", "r"], "command": "multi_find_all_regex"}, 27 | // { "keys": ["ctrl+alt+f", "ctrl+alt+r"], "command": "multi_find_all_regex", "args": {"subtract": true}}, 28 | // { "keys": ["ctrl+alt+f", "ctrl+r"], "command": "multi_find_all_regex", "args": {"case": false}}, 29 | // { "keys": ["ctrl+alt+f", "ctrl+alt+shift+r"], "command": "multi_find_all_regex", "args": {"subtract": true, "case": false}}, 30 | 31 | 32 | { "keys": ["ctrl+alt+d"], "command": "selection_fields", "args": {"mode": "smart"} }, 33 | { "keys": ["escape"], "command": "selection_fields", 34 | "args": {"mode": "pop"}, 35 | "context": 36 | [ 37 | { "key": "is_selection_field" }, 38 | { "key": "selection_fields_escape_enabled" }, 39 | // set the default precedence to be less than snippet fields 40 | { "key": "has_next_field", "operator": "equal", "operand": false }, 41 | { "key": "has_prev_field", "operator": "equal", "operand": false }, 42 | { "key": "panel_visible", "operator": "equal", "operand": false }, 43 | { "key": "overlay_visible", "operator": "equal", "operand": false }, 44 | // usually we would use popup_visible, but this is ST3 only 45 | { "key": "meu_popup_visible_proxy", "operator": "equal", "operand": false }, 46 | { "key": "auto_complete_visible", "operator": "equal", "operand": false } 47 | ] 48 | }, 49 | { "keys": ["shift+escape"], "command": "selection_fields", 50 | "args": {"mode": "remove"}, 51 | "context": 52 | [ 53 | { "key": "is_selection_field" }, 54 | { "key": "selection_fields_escape_enabled" } 55 | ] 56 | }, 57 | { "keys": ["tab"], "command": "selection_fields", 58 | "args": {"mode": "smart"}, 59 | "context": 60 | [ 61 | { "key": "is_selection_field" }, 62 | { "key": "selection_fields_tab_enabled" }, 63 | // set the default precedence to be less than snippet fields 64 | { "key": "has_next_field", "operator": "equal", "operand": false }, 65 | { "key": "auto_complete_visible", "operator": "equal", "operand": false } 66 | ] 67 | }, 68 | { "keys": ["shift+tab"], "command": "selection_fields", 69 | "args": {"mode": "cycle", "jump_forward": false }, 70 | "context": 71 | [ 72 | { "key": "is_selection_field" }, 73 | { "key": "selection_fields_tab_enabled" }, 74 | // set the default precedence to be less than snippet fields 75 | { "key": "has_prev_field", "operator": "equal", "operand": false } 76 | ] 77 | } 78 | ] -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["shift+escape"], "command": "jump_to_last_region" }, 3 | { "keys": ["super+alt+u"], "command": "add_last_selection" }, 4 | { "keys": ["super+alt+c"], "command": "cycle_through_regions" }, 5 | { "keys": ["super+alt+n"], "command": "normalize_region_ends" }, 6 | { "keys": ["super+alt+,"], "command": "split_selection" }, 7 | { "keys": ["super+alt+s"], "command": "strip_selection" }, 8 | { "keys": ["super+alt+r"], "command": "remove_empty_regions", "context" : 9 | [ 10 | {"key": "setting.is_widget", "operator": "equal", "operand": false} 11 | ] 12 | }, 13 | 14 | // Multi Find All example keybindings, uncomment to activate 15 | 16 | // // main keybinding, set the search type you're most comfortable with, default is Case+Word 17 | // { "keys": ["ctrl+super+g", "ctrl+super+g"], "command": "multi_find_all", "args": {"case": true, "word": true}}, 18 | 19 | // { "keys": ["ctrl+super+g", "ctrl+c"], "command": "multi_find_all", "args": {"case": false}}, 20 | // { "keys": ["ctrl+super+g", "c"], "command": "multi_find_all", "args": {"case": true}}, 21 | // { "keys": ["ctrl+super+g", "ctrl+w"], "command": "multi_find_all", "args": {"case": false, "word": true}}, 22 | // { "keys": ["ctrl+super+g", "w"], "command": "multi_find_all", "args": {"case": true, "word": true}}, 23 | // { "keys": ["ctrl+super+g", "q"], "command": "multi_find_all", "args": {"case": true, "word": true, "ignore_comments": true}}, 24 | 25 | // // find all with regex search, additive(on top of current selection) or subtractive 26 | // { "keys": ["ctrl+super+g", "r"], "command": "multi_find_all_regex"}, 27 | // { "keys": ["ctrl+super+g", "ctrl+alt+r"], "command": "multi_find_all_regex", "args": {"subtract": true}}, 28 | // { "keys": ["ctrl+super+g", "ctrl+r"], "command": "multi_find_all_regex", "args": {"case": false}}, 29 | // { "keys": ["ctrl+super+g", "ctrl+alt+shift+r"], "command": "multi_find_all_regex", "args": {"subtract": true, "case": false}}, 30 | 31 | 32 | { "keys": ["super+alt+d"], "command": "selection_fields", "args": {"mode": "smart"} }, 33 | { "keys": ["escape"], "command": "selection_fields", 34 | "args": {"mode": "pop"}, 35 | "context": 36 | [ 37 | { "key": "is_selection_field" }, 38 | { "key": "selection_fields_escape_enabled" }, 39 | // set the default precedence to be less than snippet fields 40 | { "key": "has_next_field", "operator": "equal", "operand": false }, 41 | { "key": "has_prev_field", "operator": "equal", "operand": false }, 42 | { "key": "panel_visible", "operator": "equal", "operand": false }, 43 | { "key": "overlay_visible", "operator": "equal", "operand": false }, 44 | // usually we would use popup_visible, but this is ST3 only 45 | { "key": "meu_popup_visible_proxy", "operator": "equal", "operand": false }, 46 | { "key": "auto_complete_visible", "operator": "equal", "operand": false } 47 | ] 48 | }, 49 | { "keys": ["shift+escape"], "command": "selection_fields", 50 | "args": {"mode": "remove"}, 51 | "context": 52 | [ 53 | { "key": "is_selection_field" }, 54 | { "key": "selection_fields_escape_enabled" } 55 | ] 56 | }, 57 | { "keys": ["tab"], "command": "selection_fields", 58 | "args": {"mode": "smart"}, 59 | "context": 60 | [ 61 | { "key": "is_selection_field" }, 62 | { "key": "selection_fields_tab_enabled" }, 63 | // set the default precedence to be less than snippet fields 64 | { "key": "has_next_field", "operator": "equal", "operand": false }, 65 | { "key": "auto_complete_visible", "operator": "equal", "operand": false } 66 | ] 67 | }, 68 | { "keys": ["shift+tab"], "command": "selection_fields", 69 | "args": {"mode": "cycle", "jump_forward": false }, 70 | "context": 71 | [ 72 | { "key": "is_selection_field" }, 73 | { "key": "selection_fields_tab_enabled" }, 74 | // set the default precedence to be less than snippet fields 75 | { "key": "has_prev_field", "operator": "equal", "operand": false } 76 | ] 77 | } 78 | ] 79 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["shift+escape"], "command": "jump_to_last_region" }, 3 | { "keys": ["ctrl+alt+u"], "command": "add_last_selection" }, 4 | { "keys": ["ctrl+alt+c"], "command": "cycle_through_regions" }, 5 | { "keys": ["ctrl+alt+n"], "command": "normalize_region_ends" }, 6 | { "keys": ["ctrl+alt+,"], "command": "split_selection" }, 7 | { "keys": ["ctrl+alt+s"], "command": "strip_selection" }, 8 | { "keys": ["ctrl+alt+r"], "command": "remove_empty_regions", "context": 9 | [ 10 | {"key": "setting.is_widget", "operator": "equal", "operand": false} 11 | ] 12 | }, 13 | 14 | // Multi Find All example keybindings, uncomment to activate 15 | 16 | // // main keybinding, set the search type you're most comfortable with, default is Case+Word 17 | // { "keys": ["ctrl+alt+f", "ctrl+alt+f"], "command": "multi_find_all", "args": {"case": true, "word": true}}, 18 | 19 | // { "keys": ["ctrl+alt+f", "ctrl+c"], "command": "multi_find_all", "args": {"case": false}}, 20 | // { "keys": ["ctrl+alt+f", "c"], "command": "multi_find_all", "args": {"case": true}}, 21 | // { "keys": ["ctrl+alt+f", "ctrl+w"], "command": "multi_find_all", "args": {"case": false, "word": true}}, 22 | // { "keys": ["ctrl+alt+f", "w"], "command": "multi_find_all", "args": {"case": true, "word": true}}, 23 | // { "keys": ["ctrl+alt+f", "q"], "command": "multi_find_all", "args": {"case": true, "word": true, "ignore_comments": true}}, 24 | 25 | // // find all with regex search, additive(on top of current selection) or subtractive 26 | // { "keys": ["ctrl+alt+f", "r"], "command": "multi_find_all_regex"}, 27 | // { "keys": ["ctrl+alt+f", "ctrl+alt+r"], "command": "multi_find_all_regex", "args": {"subtract": true}}, 28 | // { "keys": ["ctrl+alt+f", "ctrl+r"], "command": "multi_find_all_regex", "args": {"case": false}}, 29 | // { "keys": ["ctrl+alt+f", "ctrl+alt+shift+r"], "command": "multi_find_all_regex", "args": {"subtract": true, "case": false}}, 30 | 31 | 32 | { "keys": ["ctrl+alt+d"], "command": "selection_fields", "args": {"mode": "smart"} }, 33 | { "keys": ["escape"], "command": "selection_fields", 34 | "args": {"mode": "pop"}, 35 | "context": 36 | [ 37 | { "key": "is_selection_field" }, 38 | { "key": "selection_fields_escape_enabled" }, 39 | // set the default precedence to be less than snippet fields 40 | { "key": "has_next_field", "operator": "equal", "operand": false }, 41 | { "key": "has_prev_field", "operator": "equal", "operand": false }, 42 | { "key": "panel_visible", "operator": "equal", "operand": false }, 43 | { "key": "overlay_visible", "operator": "equal", "operand": false }, 44 | // usually we would use popup_visible, but this is ST3 only 45 | { "key": "meu_popup_visible_proxy", "operator": "equal", "operand": false }, 46 | { "key": "auto_complete_visible", "operator": "equal", "operand": false } 47 | ] 48 | }, 49 | { "keys": ["shift+escape"], "command": "selection_fields", 50 | "args": {"mode": "remove"}, 51 | "context": 52 | [ 53 | { "key": "is_selection_field" }, 54 | { "key": "selection_fields_escape_enabled" } 55 | ] 56 | }, 57 | { "keys": ["tab"], "command": "selection_fields", 58 | "args": {"mode": "smart"}, 59 | "context": 60 | [ 61 | { "key": "is_selection_field" }, 62 | { "key": "selection_fields_tab_enabled" }, 63 | // set the default precedence to be less than snippet fields 64 | { "key": "has_next_field", "operator": "equal", "operand": false }, 65 | { "key": "auto_complete_visible", "operator": "equal", "operand": false } 66 | ] 67 | }, 68 | { "keys": ["shift+tab"], "command": "selection_fields", 69 | "args": {"mode": "cycle", "jump_forward": false }, 70 | "context": 71 | [ 72 | { "key": "is_selection_field" }, 73 | { "key": "selection_fields_tab_enabled" }, 74 | // set the default precedence to be less than snippet fields 75 | { "key": "has_prev_field", "operator": "equal", "operand": false } 76 | ] 77 | } 78 | ] -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { "command": "jump_to_last_region", "caption" : "MultiEditUtils: Jump to last region" }, 3 | { "command": "add_last_selection", "caption" : "MultiEditUtils: Add last selection" }, 4 | { "command": "selection_fields", "caption": "MultiEditUtils: Selection as Fields", "args": {"mode": "toggle"} }, 5 | { "command": "selection_fields", "caption": "MultiEditUtils: Selection as Fields - Add Selections to Fields", "args": {"mode": "add"} }, 6 | { "command": "cycle_through_regions", "caption" : "MultiEditUtils: Cycle through regions" }, 7 | { "command": "normalize_region_ends", "caption" : "MultiEditUtils: Normalize region ends" }, 8 | { "command": "split_selection", "caption" : "MultiEditUtils: Split selection" }, 9 | { "command": "strip_selection", "caption" : "MultiEditUtils: Strip Selection" }, 10 | { "command": "remove_empty_regions", "caption" : "MultiEditUtils: Remove Empty Regions" }, 11 | { "command": "multi_find_menu", "caption" : "MultiEditUtils: Multi FindAll" }, 12 | { "command": "preserve_case", "caption" : "MultiEditUtils: Preserve Case" } 13 | ] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Philipp Otto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "selection", 4 | "children": 5 | [ 6 | { 7 | "caption": "MultiEditUtils", 8 | "children":[ 9 | { "command": "jump_to_last_region" }, 10 | { "command": "add_last_selection" }, 11 | { "command": "cycle_through_regions" }, 12 | { "command": "normalize_region_ends" }, 13 | { "command": "split_selection" }, 14 | { "command": "strip_selection"}, 15 | { "command": "remove_empty_regions"}, 16 | { "command": "multi_find_all"}, 17 | { "command": "selection_fields", "caption": "Selection as Fields", "args": {"mode": "toggle"} }, 18 | { "command": "preserve_case"} 19 | ] 20 | } 21 | ] 22 | }, 23 | { 24 | "caption": "Preferences", 25 | "mnemonic": "n", 26 | "id": "preferences", 27 | "children": 28 | [ 29 | { 30 | "caption": "Package Settings", 31 | "mnemonic": "P", 32 | "id": "package-settings", 33 | "children": 34 | [ 35 | { 36 | "caption": "MultiEditUtils", 37 | "children": 38 | [ 39 | { 40 | "command": "open_file", 41 | "id": "bindings", 42 | "args": { 43 | "file": "${packages}/MultiEditUtils/Default (Windows).sublime-keymap", 44 | "platform": "Windows" 45 | }, 46 | "caption": "Key Bindings – Default" 47 | }, 48 | { 49 | "command": "open_file", 50 | "args": { 51 | "file": "${packages}/MultiEditUtils/Default (OSX).sublime-keymap", 52 | "platform": "OSX" 53 | }, 54 | "caption": "Key Bindings – Default" 55 | }, 56 | { 57 | "command": "open_file", 58 | "args": { 59 | "file": "${packages}/MultiEditUtils/Default (Linux).sublime-keymap", 60 | "platform": "Linux" 61 | }, 62 | "caption": "Key Bindings – Default" 63 | }, 64 | { 65 | "command": "open_file", 66 | "args": { 67 | "file": "${packages}/User/Default (Windows).sublime-keymap", 68 | "platform": "Windows" 69 | }, 70 | "caption": "Key Bindings – User" 71 | }, 72 | { 73 | "command": "open_file", 74 | "args": { 75 | "file": "${packages}/User/Default (OSX).sublime-keymap", 76 | "platform": "OSX" 77 | }, 78 | "caption": "Key Bindings – User" 79 | }, 80 | { 81 | "command": "open_file", 82 | "args": { 83 | "file": "${packages}/User/Default (Linux).sublime-keymap", 84 | "platform": "Linux" 85 | }, 86 | "caption": "Key Bindings – User" 87 | }, 88 | { 89 | "command": "open_file", 90 | "args": { 91 | "file": "${packages}/MultiEditUtils/MultiEditUtils.sublime-settings" 92 | }, 93 | "caption": "Settings - Default" 94 | }, 95 | { 96 | "command": "open_file", 97 | "args": { 98 | "file": "${packages}/User/MultiEditUtils.sublime-settings" 99 | }, 100 | "caption": "Settings - User" 101 | } 102 | ] 103 | } 104 | ] 105 | } 106 | ] 107 | } 108 | ] 109 | -------------------------------------------------------------------------------- /MultiEditUtils.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import re 3 | from collections import namedtuple 4 | 5 | class MultiFindAllCommand(sublime_plugin.TextCommand): 6 | 7 | def run(self, edit, case=True, word=False, ignore_comments=False, expand=True): 8 | 9 | view = self.view 10 | newRegions = [] 11 | 12 | # filter selections in order to exclude duplicates since it can hang 13 | # Sublime if search is performed on dozens of selections, this doesn't 14 | # happen with built-in command because it works on a single selection 15 | initial = [sel for sel in view.sel()] 16 | regions, substrings = [], [] 17 | for region in view.sel(): 18 | if expand and region.empty(): 19 | # if expanding substring will be the word 20 | region = view.word(region.a) 21 | # add the region since nothing is selected yet 22 | view.sel().add(region) 23 | # filter by substring (word or not) 24 | substr = view.substr(region) 25 | if substr and substr not in substrings: 26 | regions.append(region) 27 | substrings.append(substr) 28 | view.sel().clear() 29 | if regions: 30 | for region in regions: 31 | view.sel().add(region) 32 | else: 33 | view.window().status_message("Multi Find All: nothing selected") 34 | for sel in initial: 35 | view.sel().add(sel) 36 | return 37 | 38 | selected_words = [view.substr(view.word(sel)).lower() for sel in view.sel()] 39 | 40 | for region in view.sel(): 41 | substr = view.substr(region) 42 | 43 | if case: 44 | for region in view.find_all(substr, sublime.LITERAL): 45 | newRegions.append(region) 46 | else: 47 | for region in view.find_all(substr, sublime.LITERAL | sublime.IGNORECASE): 48 | newRegions.append(region) 49 | 50 | if word: 51 | deleted = [region for region in newRegions 52 | if view.substr(view.word(region)).lower() not in selected_words] 53 | newRegions = [region for region in newRegions if region not in deleted] 54 | 55 | if ignore_comments: 56 | deleted = [region for region in newRegions 57 | if re.search(r'\bcomment\b', view.scope_name(region.a))] 58 | newRegions = [region for region in newRegions if region not in deleted] 59 | 60 | for region in newRegions: 61 | view.sel().add(region) 62 | 63 | class MultiFindAllRegexCommand(sublime_plugin.TextCommand): 64 | 65 | def on_done(self, regex): 66 | 67 | case = sublime.IGNORECASE if not self.case else 0 68 | regions = self.view.find_all(regex, case) 69 | 70 | # we don't clear the selection so it's additive, it's nice to just add a 71 | # regex search on top of a previous search 72 | if not self.subtract: 73 | for region in regions: 74 | self.view.sel().add(region) 75 | 76 | # the resulting regions will be subtracted instead 77 | else: 78 | for region in regions: 79 | self.view.sel().subtract(region) 80 | 81 | # remove empty selections in both cases, so there aren't loose cursors 82 | regions = [r for r in self.view.sel() if not r.empty()] 83 | self.view.sel().clear() 84 | for region in regions: 85 | self.view.sel().add(region) 86 | for region in self.view.sel(): 87 | print(region) 88 | 89 | def run(self, edit, case=True, subtract=False): 90 | 91 | self.edit = edit 92 | self.case = case 93 | self.subtract = subtract 94 | c = "Additive regex search:" if not subtract else "Subtractive regex search:" 95 | sublime.active_window().show_input_panel(c, "", self.on_done, None, None) 96 | 97 | class MultiFindMenuCommand(sublime_plugin.TextCommand): 98 | 99 | def run(self, edit): 100 | 101 | choice = [ 102 | "Find All Case + Word +", 103 | "Find All Case + Word -", 104 | "Find All Case - Word +", 105 | "Find All Case - Word -", 106 | "Find All Case + Word + (Ignore Comments)", 107 | "Find Regex (Additive)", 108 | "Find Regex (Subtractive)" 109 | ] 110 | 111 | def on_done(index): 112 | 113 | if index == -1: 114 | return 115 | if index == 0: 116 | self.view.run_command('multi_find_all', {"case": True, "word": True}) 117 | elif index == 1: 118 | self.view.run_command('multi_find_all', {"case": True}) 119 | elif index == 2: 120 | self.view.run_command('multi_find_all', {"case": False, "word": True}) 121 | elif index == 3: 122 | self.view.run_command('multi_find_all', {"case": False}) 123 | elif index == 4: 124 | self.view.run_command('multi_find_all', {"case": True, "word": True, "ignore_comments": True}) 125 | elif index == 5: 126 | self.view.run_command('multi_find_all_regex') 127 | elif index == 6: 128 | self.view.run_command('multi_find_all_regex', {"subtract": True}) 129 | 130 | self.view.window().show_quick_panel(choice, on_done, 1, 0, None) 131 | 132 | class JumpToLastRegionCommand(sublime_plugin.TextCommand): 133 | 134 | def run(self, edit): 135 | 136 | selection = self.view.sel() 137 | lastRegion = selection[-1] 138 | cursorPosition = lastRegion.b 139 | selection.clear() 140 | selection.add(sublime.Region(cursorPosition)) 141 | self.view.show(cursorPosition, False) 142 | 143 | 144 | class AddLastSelectionCommand(sublime_plugin.TextCommand): 145 | 146 | def run(self, edit): 147 | 148 | helper = Helper.getOrConstructHelperForView(self.view) 149 | lastSelections = helper.lastSelections 150 | 151 | if len(lastSelections) < 1: 152 | return 153 | 154 | currentSelection = self.view.sel() 155 | oldSelectionHash = Helper.hashSelection(currentSelection) 156 | 157 | for region in lastSelections[-1]: 158 | helper.ignoreSelectionCommand = True 159 | currentSelection.add(region) 160 | 161 | newSelectionHash = Helper.hashSelection(currentSelection) 162 | 163 | lastSelections.pop(-1) 164 | 165 | nothingChanged = oldSelectionHash == newSelectionHash 166 | if nothingChanged: 167 | # Rerun if the previous selection was only a subset of the current selection. 168 | self.run(edit) 169 | 170 | 171 | class CycleThroughRegionsCommand(sublime_plugin.TextCommand): 172 | 173 | def run(self, edit): 174 | 175 | view = self.view 176 | visibleRegion = view.visible_region() 177 | selectedRegions = view.sel() 178 | 179 | if not len(selectedRegions): 180 | return 181 | 182 | nextRegion = None 183 | 184 | # Find the first region which comes after the visible region. 185 | for region in selectedRegions: 186 | if region.end() > visibleRegion.b: 187 | nextRegion = region 188 | break 189 | 190 | # If the last region in the buffer was reached, take the first region. 191 | # Empty regions will be evaluated falsy, which is why short-circuit evaluation doesn't work here. 192 | if nextRegion is None: 193 | nextRegion = selectedRegions[0] 194 | 195 | view.show(nextRegion, False) 196 | 197 | 198 | 199 | class NormalizeRegionEndsCommand(sublime_plugin.TextCommand): 200 | 201 | def run(self, edit): 202 | 203 | view = self.view 204 | selection = view.sel() 205 | 206 | if not len(selection): 207 | return 208 | 209 | if self.areRegionsNormalized(selection): 210 | regions = self.invertRegions(selection) 211 | else: 212 | regions = self.normalizeRegions(selection) 213 | 214 | selection.clear() 215 | for region in regions: 216 | selection.add(region) 217 | 218 | firstVisibleRegion = self.findFirstVisibleRegion() 219 | if firstVisibleRegion is not None: 220 | # if firstVisibleRegion won't work with empty regions 221 | view.show(firstVisibleRegion.b, False) 222 | 223 | 224 | def findFirstVisibleRegion(self): 225 | 226 | visibleRegion = self.view.visible_region() 227 | 228 | for region in self.view.sel(): 229 | if region.intersects(visibleRegion): 230 | return region 231 | 232 | return None 233 | 234 | 235 | def normalizeRegions(self, regions): 236 | 237 | return self.invertRegions(regions, lambda region: region.a > region.b) 238 | 239 | 240 | def invertRegions(self, regions, condition = lambda region: True): 241 | 242 | invertedRegions = [] 243 | 244 | for region in regions: 245 | invertedRegion = region 246 | if condition(region): 247 | invertedRegion = sublime.Region(region.b, region.a) 248 | 249 | invertedRegions.append(invertedRegion) 250 | 251 | return invertedRegions 252 | 253 | 254 | def areRegionsNormalized(self, regions): 255 | 256 | return all(region.a < region.b for region in regions) 257 | 258 | 259 | 260 | class SplitSelectionCommand(sublime_plugin.TextCommand): 261 | 262 | def run(self, edit, separator = None): 263 | 264 | self.savedSelection = [r for r in self.view.sel()] 265 | 266 | selectionSize = sum(map(lambda region: region.size(), self.savedSelection)) 267 | if selectionSize == 0: 268 | # nothing to do 269 | sublime.status_message("Cannot split an empty selection.") 270 | return 271 | 272 | if separator != None: 273 | self.splitSelection(separator) 274 | else: 275 | onConfirm, onChange = self.getHandlers() 276 | 277 | inputView = sublime.active_window().show_input_panel( 278 | "Separating character(s) for splitting the selection", 279 | " ", 280 | onConfirm, 281 | onChange, 282 | self.restoreSelection 283 | ) 284 | 285 | inputView.run_command("select_all") 286 | 287 | 288 | def getHandlers(self): 289 | 290 | settings = sublime.load_settings("MultiEditUtils.sublime-settings") 291 | live_split_selection = settings.get("live_split_selection") 292 | 293 | if live_split_selection: 294 | onConfirm = None 295 | onChange = self.splitSelection 296 | else: 297 | onConfirm = self.splitSelection 298 | onChange = None 299 | 300 | return (onConfirm, onChange) 301 | 302 | 303 | def restoreSelection(self): 304 | 305 | selection = self.view.sel() 306 | selection.clear() 307 | for region in self.savedSelection: 308 | selection.add(region) 309 | 310 | self.workaroundForRefreshBug(self.view, selection) 311 | 312 | 313 | def splitSelection(self, separator): 314 | 315 | view = self.view 316 | newRegions = [] 317 | 318 | for region in self.savedSelection: 319 | currentPosition = region.begin() 320 | regionString = view.substr(region) 321 | 322 | if separator: 323 | subRegions = regionString.split(separator) 324 | else: 325 | # take each character separately 326 | subRegions = list(regionString) 327 | 328 | for subRegion in subRegions: 329 | newRegion = sublime.Region( 330 | currentPosition, 331 | currentPosition + len(subRegion) 332 | ) 333 | newRegions.append(newRegion) 334 | currentPosition += len(subRegion) + len(separator) 335 | 336 | selection = view.sel() 337 | selection.clear() 338 | for region in newRegions: 339 | selection.add(region) 340 | 341 | self.workaroundForRefreshBug(view, selection) 342 | 343 | 344 | def workaroundForRefreshBug(self, view, selection): 345 | # work around sublime bug with caret position not refreshing 346 | # see: https://github.com/code-orchestra/colt-sublime-plugin/commit/9e6ffbf573fc60b356665ff2ba9ced614c71120f 347 | 348 | bug = [s for s in selection] 349 | view.add_regions("bug", bug, "bug", "dot", sublime.HIDDEN | sublime.PERSISTENT) 350 | view.erase_regions("bug") 351 | 352 | 353 | 354 | Case = namedtuple("Case", "lower upper capitalized mixed")(1, 2, 3, 4) 355 | StringMetaData = namedtuple("StringMetaData", "separator cases stringGroups") 356 | 357 | 358 | class PreserveCaseCommand(sublime_plugin.TextCommand): 359 | 360 | def run(self, edit, newString = None, selections = None): 361 | 362 | self.edit = edit 363 | if selections is not None: 364 | self.savedSelection = [sublime.Region(r[0], r[1]) for r in selections] 365 | else: 366 | self.savedSelection = [r for r in self.view.sel()] 367 | 368 | selectionSize = sum(map(lambda region: region.size(), self.savedSelection)) 369 | if selectionSize == 0: 370 | sublime.status_message("Cannot run preserve case on an empty selection.") 371 | return 372 | 373 | if newString != None: 374 | self.preserveCase(newString) 375 | else: 376 | firstRegionString = self.view.substr(self.savedSelection[0]) 377 | inputView = sublime.active_window().show_input_panel( 378 | "New string for preserving case", 379 | firstRegionString, 380 | self.runPreserveCase, 381 | None, 382 | None 383 | ) 384 | inputView.run_command("select_all") 385 | 386 | 387 | def runPreserveCase(self, newString): 388 | selections = [[s.a, s.b] for s in self.savedSelection] 389 | self.view.run_command("preserve_case", {"newString": newString, "selections": selections}) 390 | 391 | 392 | def preserveCase(self, newString): 393 | 394 | view = self.view 395 | regionOffset = 0 396 | newStringGroups = self.analyzeString(newString).stringGroups 397 | 398 | for region in self.savedSelection: 399 | region = sublime.Region(region.begin() + regionOffset, region.end() + regionOffset) 400 | regionString = view.substr(region) 401 | 402 | newRegionString = self.replaceStringWithCase(regionString, newStringGroups) 403 | view.replace(self.edit, region, newRegionString) 404 | regionOffset += len(newRegionString) - len(regionString) 405 | 406 | 407 | def analyzeString(self, aString): 408 | 409 | separators = "-_/. " 410 | counts = list(map(lambda sep: aString.count(sep), separators)) 411 | maxCounts = max(counts) 412 | 413 | if max(counts) > 0: 414 | separator = separators[counts.index(maxCounts)] 415 | stringGroups = aString.split(separator) 416 | else: 417 | # no real separator 418 | separator = "" 419 | stringGroups = self.splitByCase(aString) 420 | 421 | cases = list(map(self.analyzeCase, stringGroups)) 422 | 423 | return StringMetaData(separator, cases, stringGroups) 424 | 425 | 426 | def splitByCase(self, aString): 427 | 428 | # split at the change from lower to upper case (or vice versa) 429 | # groups = re.split('(? 1 or firstRegionLength > 0 567 | 568 | 569 | def isSubsetOf(self, selectionA, selectionB): 570 | # Check if selectionA is a subset of selectionB. 571 | 572 | return all(selectionA.contains(region) for region in selectionB) 573 | 574 | 575 | 576 | class TriggerSelectionModifiedCommand(sublime_plugin.TextCommand): 577 | 578 | def run(self, edit): 579 | 580 | SelectionListener().on_selection_modified(self.view) 581 | 582 | 583 | 584 | class Helper: 585 | 586 | viewToHelperMap = {} 587 | 588 | def __init__(self): 589 | 590 | # The SelectionCommand should be ignored if it was triggered by AddLastSelectionCommand. 591 | self.ignoreSelectionCommand = False 592 | self.lastSelections = [] 593 | 594 | 595 | @staticmethod 596 | def getOrConstructHelperForView(view): 597 | 598 | mapping = Helper.viewToHelperMap 599 | viewID = view.id() 600 | 601 | if not viewID in mapping.keys(): 602 | mapping[viewID] = Helper() 603 | 604 | helper = mapping[viewID] 605 | return helper 606 | 607 | 608 | @staticmethod 609 | def hashSelection(selection): 610 | 611 | return str(list(selection)) 612 | -------------------------------------------------------------------------------- /MultiEditUtils.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "live_split_selection" : true, 3 | // the highlighting scope of fields 4 | "selection_fields.scope.fields": "comment", 5 | // the highlighting scope of fields added via the `add` mode 6 | "selection_fields.scope.added_fields": "none", 7 | // whether the add command of selection fields should add separated field, 8 | // such that the special keybindings are not enabled via the add command 9 | "selection_fields.add_separated": true, 10 | // whether the tab key should jump to the next field during the selection field mode 11 | "selection_fields_tab_enabled": true, 12 | // whether the escape key should cancel the selection field mode 13 | "selection_fields_escape_enabled": true 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sublime MultiEditUtils [![test](https://github.com/philippotto/Sublime-MultiEditUtils/actions/workflows/test.yaml/badge.svg)](https://github.com/philippotto/Sublime-MultiEditUtils/actions/workflows/test.yaml) 2 | ============== 3 | 4 | A Sublime Text 2/3 Plugin which enhances editing of multiple selections. In case you aren't familar with Sublime's awesome multiple selection features, visit [this page](https://www.sublimetext.com/docs/2/multiple_selection_with_the_keyboard.html). 5 | 6 | ## Features 7 | 8 | ### Preserve case while editing selection contents 9 | 10 | When multi-selecting all occurences of an identifier it is cumbersome to change it to another one if the case differs (camelCase, PascalCase, UPPER CASE and even cases with separators like snake_case, dash-case, dot.case etc.). The "Preserve case" feature facilitates this. Just invoke "Preserve case" via the command palette (or define an own keybinding) and type in the new identifier. 11 | 12 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/preserve-case.gif) 13 | 14 | 15 | ### Split the selection 16 | 17 | Sublime has a default command to split selections into lines, but sometimes you want to define your own splitting character(s). MultiEditUtils' ```split_selection``` command (default keybinding is **ctrl/cmd+alt+,**) will ask you for a separator and split the selection using your input. An empty separator will split the selection into its characters. 18 | 19 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/05%20split%20selection.gif) 20 | 21 | 22 | ### Extend the current selection with the last selection 23 | 24 | Sometimes Sublime's standard features for creating multiple selections won't cut it. MultiEditUtils allows to select the desired parts individually and merge the selections with the ```add_last_selection``` command (default keybinding is **ctrl/cmd+alt+u**). 25 | 26 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/01%20expand%20with%20last%20region.gif) 27 | 28 | 29 | ### Normalize and toggle region ends 30 | 31 | When creating selections in Sublime, it can occur that the end of the selection comes before the beginning. This happens when you make the selection "backwards". To resolve this, you can normalize the regions with MultiEditUtils' ```normalize_region_ends``` command (default keybinding is **ctrl/cmd+alt+n**). When executing this command a second time, all regions will be reversed. 32 | 33 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/02a%20normalize%20region%20ends.gif) 34 | 35 | This feature can also be very handy when you want to toggle the selection end of a single region. 36 | 37 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/02b%20toggle%20selection%20end.gif) 38 | 39 | 40 | ### Jump to last region 41 | 42 | When exiting multi selection mode, Sublime will set the cursor to the **first** region of your previous selection. This can be annoying if the regions were scattered throughout the current buffer and you want to continue your work at the **last** region. To avoid this, just execute MultiEditUtils' ```jump_to_last_region``` command (default keybinding is **shift+esc**) and the cursor will jump to the last region. 43 | 44 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/03%20jump%20to%20last%20region.gif) 45 | 46 | 47 | ### Cycle through the regions 48 | 49 | In case you want to double check your current selections, MultiEditUtils' ```cycle_through_regions``` command (default keybinding is **ctrl/cmd+alt+c**) will let you cycle through the active regions. This can come handy if the regions don't fit on one screen and you want to avoid scrolling through the whole file. 50 | 51 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/04%20cycle%20through%20regions.gif) 52 | 53 | 54 | ### Strip selection 55 | 56 | Sometimes selections contain surrounding whitespace which can get in the way of your editing. The ```strip_selection``` command strips the regions so that this whitespace gets removed. The default keybinding is **ctrl/cmd+alt+s**. 57 | 58 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/06%20strip%20selection.gif) 59 | 60 | 61 | ### Remove empty regions 62 | 63 | When splitting your selection or performing other actions on your selection, it can happen that some regions are empty while others are not. Often only the non-empty regions are of interest. The ```remove_empty_regions``` commands will take care of this and remove all empty regions from your current selection. The default keybinding is **ctrl/cmd+alt+r**. 64 | 65 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/07%20remove%20empty%20selections.gif) 66 | 67 | 68 | ### Quick Find All for multiple selections 69 | 70 | Similar to the built-in "Quick Find All" functionality, MultiEditUtils provides a functionality which selects all occurrences of all active selections. By default, it will select the word the cursor is on, if the selection is empty, just like `find_all_under` command. If you don't like this behaviour, add the argument `"expand": false` 71 | 72 | These are just suggested keybindings, but you'll have to activate them in your keymap file first. Here shown for Windows/Linux: 73 | 74 | ``` 75 | ctrl+alt+f, ctrl+alt+f case: true word: true 76 | 77 | ctrl+alt+f, c case: true 78 | ctrl+alt+f, ctrl+c case: false 79 | ctrl+alt+f, w case: true word: true 80 | ctrl+alt+f, ctrl+w case: false word: true 81 | ctrl+alt+f, q case: true word: true ignore_comments: true 82 | ``` 83 | 84 | Additionally, you can perform a regex search that finds all occurrences of the entered regex. It can be **additive** (applied on top of your current selection) or **subtractive** (removes the results of the search instead). Example keybindings: 85 | 86 | ``` 87 | ctrl+alt+f, r 88 | ctrl+alt+f, ctrl+alt+r subtract: true 89 | ctrl+alt+f, ctrl+r case : false 90 | ctrl+alt+f, ctrl+alt+shift+r subtract: true case: false 91 | ``` 92 | 93 | ![](http://philippotto.github.io/Sublime-MultiEditUtils/screens/08%20multi%20find%20all.gif) 94 | 95 | 96 | ### Use selections as fields 97 | 98 | Converts the selections to fields similar to the fields used in snippets. When the `selection_fields` command is executed, all current selections are saved as fields, which can be activated one by one. The first field is activated automatically. You can jump to the next field with **tab** (or the default keybinding) and to the previous field with **shift+tab**. If you jump behind the last field or press **escape** all fields will be converted to proper selections again. If you press **shift+escape** all fields will be removed and the current selection remains unchanged. 99 | 100 | ![demo_selection_fields](https://cloud.githubusercontent.com/assets/12573621/14402686/17391716-fe3d-11e5-8fba-4e52a4f93459.gif) 101 | 102 | You can bind this command to a keybinding by adding the following to your keymap (Change the key to the keybinding you prefer): 103 | 104 | ``` js 105 | { "keys": ["alt+d"], "command": "selection_fields" }, 106 | ``` 107 | 108 | Although using one keybinding with the default options should be sufficient for most cases, additional modes and arguments are possible. Feel free to ignore or use them as you wish. 109 | 110 | Arguments: 111 | 112 | - `mode` (`"smart"`) is the executing mode, which defines the executed action. Possible modes are: 113 | + `"push"` to push the current selection as fields. This will overwrite already pushed fields. 114 | + `"pop"` to pop the pushed fields as selections 115 | + `"remove"` to remove the pushed fields without adding them to the selection. This has the same behavior as pop if `only_other` is `true`. 116 | + `"add"` to add the current selection to the pushed fields 117 | + `"subtract"` to subtract the current selection from the pushed fields 118 | + `"smart"` to try to detect whether to push, pop or jump to the next field 119 | + `"toggle"` to pop if fields are pushed, else push the selections as fields. 120 | + `"cycle"` to push or go next. This will cycle, i.e. go to the first if the last field is reached, never pops 121 | - `jump_forward` (`true`) can be `true` to jump forward and `false` to jump backward 122 | - `only_other` (`false`) ignores the current selection for pop and go next actions. 123 | 124 | Suggestion for more keybindings based on the arguments: 125 | 126 | ``` js 127 | // default use of selection_fields 128 | { "keys": ["alt+d"], "command": "selection_fields" }, 129 | // add the current selections as a fields 130 | { "keys": ["alt+a"], "command": "selection_fields", "args": {"mode": "add"} }, 131 | // jump and remove current selection in selection_fields 132 | { "keys": ["ctrl+alt+d"], "command": "selection_fields", 133 | "args": {"mode": "smart", "only_other": true} }, 134 | // cancel selection_fields and remove current selection 135 | { "keys": ["ctrl+alt+shift+d"], "command": "selection_fields", 136 | "args": {"mode": "toggle", "only_other": true} }, 137 | ``` 138 | 139 | ## Installation 140 | 141 | Either use [Package Control](https://sublime.wbond.net/installation) and search for `MultiEditUtils` or clone this repository into Sublime Text "Packages" directory. 142 | 143 | ## Shortcut Cheat Sheet 144 | 145 | [![multieditutilscheatsheetmain](https://user-images.githubusercontent.com/2641327/27285539-57bd445c-54fd-11e7-867f-2e3a264c7d35.png)](https://github.com/philippotto/Sublime-MultiEditUtils/files/1084890/multiEditUtilsCheatsheetMain.pdf) 146 | 147 | Thank you [@AllanLRH](https://github.com/AllanLRH) for creating this cheat sheet! 148 | 149 | ## License 150 | 151 | MIT © Philipp Otto 152 | -------------------------------------------------------------------------------- /changelogs/1.6.0.txt: -------------------------------------------------------------------------------- 1 | MultiEditUtils Changelog 2 | 3 | - added "Preserve Case" feature (available via command palette) to simulatenously change something like "someIdentifier" in the following code: 4 | someIdentifier 5 | SOME_IDENTIFIER 6 | usingSomeIdentifier 7 | - more information here: https://github.com/philippotto/Sublime-MultiEditUtils -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.6": "changelogs/1.0.6.txt" 3 | } -------------------------------------------------------------------------------- /selection_fields.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | _ST3 = sublime.version() >= "3000" 5 | 6 | # highlight pushed region options 7 | if _ST3: 8 | _FLAGS = sublime.DRAW_EMPTY | sublime.DRAW_NO_FILL 9 | else: 10 | _FLAGS = sublime.DRAW_EMPTY | sublime.DRAW_OUTLINED 11 | 12 | 13 | def get_settings(key, default=None): 14 | """Get the setting specified by the key.""" 15 | settings = sublime.load_settings("MultiEditUtils.sublime-settings") 16 | return settings.get(key, default) 17 | 18 | 19 | def _get_settings(key, default=None): 20 | """ 21 | Get the setting specified by the key, 22 | with the prefix `selection_fields.`. 23 | """ 24 | return get_settings("selection_fields.{0}".format(key), default) 25 | 26 | 27 | def _set_fields(view, regions, added_fields=False): 28 | """Set the fields as regions in the view.""" 29 | # push the fields to the view, kwargs for ST3 and pos args for ST2 30 | if not added_fields: 31 | reg_name = "meu_sf_stored_selections" 32 | scope_setting = "scope.fields" 33 | else: 34 | reg_name = "meu_sf_added_selections" 35 | scope_setting = "scope.added_fields" 36 | scope = _get_settings(scope_setting, "comment") 37 | if _ST3: 38 | view.add_regions(reg_name, regions, scope=scope, flags=_FLAGS) 39 | else: 40 | view.add_regions(reg_name, regions, scope, _FLAGS) 41 | 42 | 43 | def _get_fields(view, added_fields=True): 44 | fields = view.get_regions("meu_sf_stored_selections") 45 | if added_fields: 46 | fields.extend(view.get_regions("meu_sf_added_selections")) 47 | return fields 48 | 49 | 50 | def _erase_added_fields(view): 51 | view.erase_regions("meu_sf_added_selections") 52 | 53 | 54 | def _erase_fields(view): 55 | view.erase_regions("meu_sf_stored_selections") 56 | view.erase_regions("meu_sf_added_selections") 57 | view.erase_status("meu_field_message") 58 | 59 | 60 | def _change_selection(view, regions, pos): 61 | """Extract the next selection, push all other fields.""" 62 | # save and remove the position in the regions 63 | sel = regions[pos] 64 | del regions[pos] 65 | # add the regions as fields to the view 66 | _set_fields(view, regions) 67 | # add a feedback to the statusbar 68 | if len(regions) >= 1: 69 | view.set_status("meu_field_message", 70 | "Selection-Field {0} of {1}" 71 | .format(pos + 1, len(regions) + 1)) 72 | else: 73 | view.erase_status("meu_field_message") 74 | # return the selection, which was at the position 75 | sel_regions = [sel] 76 | return sel_regions 77 | 78 | 79 | def _restore_selection(view, only_other): 80 | """Restore the selection from the pushed fields.""" 81 | sel_regions = _get_fields(view) 82 | if not only_other: 83 | sel_regions.extend(view.sel()) 84 | _erase_fields(view) 85 | return sel_regions 86 | 87 | 88 | def _execute_jump(view, jump_forward, only_other): 89 | """ 90 | Add the selection to the fields and move the selection to the 91 | next field. 92 | """ 93 | regions = _get_fields(view) 94 | 95 | try: 96 | # search for the first field, which is behind the last selection 97 | end = max(sel.end() for sel in view.sel()) 98 | pos = next(i for i, sel in enumerate(regions) if sel.begin() > end) 99 | except: 100 | # if there is no remaining field move the position behind the regions 101 | pos = len(regions) 102 | # insert the selection into the region 103 | if only_other: 104 | sel_count = 0 105 | else: 106 | sel_count = len(view.sel()) 107 | if sel_count == 1: 108 | # handle special case of one selection 109 | regions.insert(pos, view.sel()[0]) 110 | else: 111 | # put the selections into the regions array 112 | regions = regions[:pos] + list(view.sel()) + regions[pos:] 113 | # the forward jump must jump over all added selections 114 | delta = sel_count if jump_forward else -1 115 | # move the position to the next field 116 | pos = pos + delta 117 | return regions, pos 118 | 119 | 120 | def _subtract_selection(pushed_regions, sel_regions): 121 | """Subtract the selections from the pushed fields.""" 122 | for reg in pushed_regions: 123 | for sel in sel_regions: 124 | if sel.begin() <= reg.end() and reg.begin() <= sel.end(): 125 | # yield the region from the start of the field to the selection 126 | if reg.begin() < sel.begin(): 127 | yield sublime.Region(reg.begin(), sel.begin()) 128 | # update the region to be from the end of the selection to 129 | # the end of the field 130 | reg = sublime.Region(sel.end(), reg.end()) 131 | # if the region is not forward, break and don't add it as field 132 | if not reg.a < reg.b: 133 | break 134 | else: 135 | # yield the region as field 136 | yield reg 137 | 138 | _valid_modes = [ 139 | "push", # push the current selection as fields, overwrite existing fields 140 | "pop", # pop the pushed field and add them to the selection 141 | "remove", # pop the pushed field without adding them to the selection 142 | # same behavior as pop if only_other is true 143 | "add", # add the current selection to the pushed fields 144 | "subtract", # subtract the current selection from the pushed fields 145 | "smart", # try to detect whether to push, pop or go next 146 | "toggle", # pop if fields are pushed, else push 147 | "cycle" # push or go next, go to first if at the end, never pop 148 | ] 149 | 150 | 151 | class SelectionFieldsCommand(sublime_plugin.TextCommand): 152 | def run(self, edit, mode="smart", jump_forward=True, only_other=False): 153 | if mode not in _valid_modes: 154 | sublime.error_message( 155 | "'{0}' is an invalid mode for 'selection_fields'.\n" 156 | "Valid modes are: [{1}]" 157 | .format(mode, ", ".join(_valid_modes)) 158 | ) 159 | return 160 | view = self.view 161 | has_fields = bool(_get_fields(view)) 162 | has_only_added_fields = (not _get_fields(view, added_fields=False) and 163 | _get_settings("add_separated", True)) 164 | do_push = { 165 | "pop": False, 166 | "remove": False, 167 | "push": True, 168 | "subtract": False, 169 | "add": False # add is specially handled 170 | }.get(mode, not has_fields) 171 | # the regions, which should be selected after executing this command 172 | sel_regions = None 173 | 174 | if do_push: # push or initial trigger with anything except pop 175 | sels = list(view.sel()) 176 | border_pos = 0 if jump_forward else len(sels) - 1 177 | sel_regions = _change_selection(view, sels, border_pos) 178 | elif mode == "subtract": # subtract selections from the pushed fields 179 | sel_regions = list(view.sel()) 180 | pushed_regions = _get_fields(view) 181 | regions = list(_subtract_selection(pushed_regions, sel_regions)) 182 | _erase_added_fields(view) 183 | _set_fields(view, regions, added_fields=has_only_added_fields) 184 | elif mode == "add": # add selections to the pushed fields 185 | pushed_regions = _get_fields(view) 186 | sel_regions = list(view.sel()) 187 | _set_fields(view, sel_regions + pushed_regions, 188 | added_fields=has_only_added_fields) 189 | elif mode == "remove": # remove pushed fields 190 | pop_regions = _restore_selection(view, only_other) 191 | sel_regions = list(view.sel()) if not only_other else pop_regions 192 | elif mode not in ["smart", "cycle"]: # pop or toggle with region 193 | sel_regions = _restore_selection(view, only_other) 194 | # pop added fields instead of jumping 195 | elif mode == "smart" and has_only_added_fields: 196 | sel_regions = _restore_selection(view, only_other) 197 | else: # smart or cycle 198 | # execute the jump 199 | regions, pos = _execute_jump(view, jump_forward, only_other) 200 | # if we are in the cycle mode force the position to be valid 201 | if mode == "cycle": 202 | pos = pos % len(regions) 203 | # check whether it is a valid position 204 | pos_valid = pos == pos % len(regions) 205 | if pos_valid: 206 | # move the selection to the new field 207 | sel_regions = _change_selection(view, regions, pos) 208 | else: 209 | # if we reached the end restore the selection and 210 | # remove the highlight regions 211 | sel_regions = _restore_selection(view, only_other) 212 | 213 | # change to the result selections, if they exists 214 | if sel_regions: 215 | view.sel().clear() 216 | if _ST3: 217 | view.sel().add_all(sel_regions) 218 | else: 219 | for sel in sel_regions: 220 | view.sel().add(sel) 221 | view.show(sel_regions[0]) 222 | 223 | 224 | class SelectionFieldsContext(sublime_plugin.EventListener): 225 | def on_query_context(self, view, key, operator, operand, match_all): 226 | if key not in ["is_selection_field", "is_selection_field.added_fields", 227 | "selection_fields_tab_enabled", 228 | "selection_fields_escape_enabled"]: 229 | return False 230 | 231 | if key == "is_selection_field": 232 | # selection field is active if the regions are pushed to the view 233 | result = bool(_get_fields(view, added_fields=False)) 234 | elif key == "is_selection_field.added_fields": 235 | # selection field is active if the regions are pushed to the view 236 | # also if added fields are pushed 237 | result = bool(_get_fields(view)) 238 | else: 239 | # the *_enabled key has the same name in the settings 240 | result = get_settings(key, False) 241 | 242 | if operator == sublime.OP_EQUAL: 243 | result = result == operand 244 | elif operator == sublime.OP_NOT_EQUAL: 245 | result = result != operand 246 | else: 247 | raise Exception("Invalid Operator '{0}'.".format(operator)) 248 | return result 249 | 250 | 251 | # this context listener is necessary for ST2/3 compatibility, because 252 | # the popup has only been added in ST3 build 3080 and we want this 253 | # context to be disabled for the escape key 254 | class MeuPopupVisibleProxyContext(sublime_plugin.EventListener): 255 | def on_query_context(self, view, key, operator, operand, match_all): 256 | if key != "meu_popup_visible_proxy": 257 | return False 258 | 259 | # the popup has been added in ST build 3080 260 | if hasattr(view, "is_popup_visible"): 261 | result = view.is_popup_visible() 262 | else: 263 | result = False 264 | 265 | if operator == sublime.OP_EQUAL: 266 | result = result == operand 267 | elif operator == sublime.OP_NOT_EQUAL: 268 | result = result != operand 269 | else: 270 | raise Exception("Invalid Operator '{0}'.".format(operator)) 271 | return result 272 | -------------------------------------------------------------------------------- /tests/testMultiEditUtils.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | 3 | import sublime 4 | from unittest import TestCase 5 | import re 6 | 7 | version = sublime.version() 8 | 9 | class TestMultiEditUtils(TestCase): 10 | 11 | def setUp(self): 12 | 13 | self.view = sublime.active_window().new_file() 14 | 15 | 16 | def tearDown(self): 17 | 18 | if self.view: 19 | self.view.set_scratch(True) 20 | self.view.window().run_command("close_file") 21 | 22 | 23 | def splitBy(self, separator, expectedAmount): 24 | 25 | testString = "this, is, a, test" 26 | self.view.run_command("insert", {"characters": testString}) 27 | self.view.run_command("select_all") 28 | self.view.run_command("split_selection", dict(separator = separator)) 29 | 30 | selection = self.view.sel() 31 | 32 | self.assertEqual(len(selection), expectedAmount) 33 | 34 | 35 | def testSplitBySpace(self): 36 | 37 | self.splitBy(" ", 4) 38 | 39 | 40 | def testSplitByCommaSpace(self): 41 | 42 | self.splitBy(", ", 4) 43 | 44 | 45 | def testSplitByCharacter(self): 46 | 47 | self.splitBy("", 17) 48 | 49 | 50 | def testToggleRegionEnds(self): 51 | 52 | testString = "this is a test" 53 | self.view.run_command("insert", {"characters": testString}) 54 | 55 | regionTuple = [0, 14] 56 | self.selectRegions([regionTuple]) 57 | 58 | selection = self.view.sel() 59 | self.assertRegionEqual(selection[0], regionTuple) 60 | 61 | self.view.run_command("normalize_region_ends") 62 | 63 | self.assertRegionEqual(selection[0], regionTuple[::-1]) 64 | 65 | 66 | def testToggleRegionEnds(self): 67 | 68 | testString = "test test" 69 | self.view.run_command("insert", {"characters": testString}) 70 | 71 | regionTuples = [[0, 4], [9, 5]] 72 | self.selectRegions(regionTuples) 73 | 74 | selection = self.view.sel() 75 | self.assertRegionEqual(selection[0], regionTuples[0]) 76 | self.assertRegionEqual(selection[1], regionTuples[1]) 77 | 78 | self.view.run_command("normalize_region_ends") 79 | 80 | self.assertRegionEqual(selection[0], regionTuples[0]) 81 | self.assertRegionEqual(selection[1], regionTuples[1][::-1]) 82 | 83 | 84 | def testJumpToLastRegion(self): 85 | 86 | testString = "test test test test" 87 | self.view.run_command("insert", {"characters": testString}) 88 | 89 | self.selectRegions([[0, 4], [5, 9]]) 90 | 91 | selection = self.view.sel() 92 | self.assertEqual(len(selection), 2) 93 | 94 | self.view.run_command("jump_to_last_region") 95 | 96 | self.assertEqual(len(selection), 1) 97 | self.assertRegionEqual(selection[0], [9, 9]) 98 | 99 | 100 | def testAddLastSelection(self): 101 | 102 | testString = "this is a test" 103 | self.view.run_command("insert", {"characters": testString}) 104 | 105 | regions = [[0, 4], [5, 9]] 106 | self.selectRegions([regions[0]]) 107 | self.view.run_command("trigger_selection_modified") 108 | self.selectRegions([regions[1]]) 109 | self.view.run_command("trigger_selection_modified") 110 | 111 | self.view.run_command("add_last_selection") 112 | 113 | selection = self.view.sel() 114 | 115 | 116 | self.assertEqual(len(selection), 2) 117 | self.assertRegionEqual(selection[0], regions[0]) 118 | self.assertRegionEqual(selection[1], regions[1]) 119 | 120 | 121 | def testRemoveEmptyRegions(self): 122 | 123 | testString = "a\nb\n\nc" 124 | regions = [[0, 1], [2, 3], [5, 6]] 125 | 126 | self.view.run_command("insert", {"characters": testString}) 127 | self.view.run_command("select_all") 128 | self.view.run_command("split_selection_into_lines") 129 | self.view.run_command("remove_empty_regions") 130 | 131 | selection = self.view.sel() 132 | 133 | self.assertEqual(len(selection), 3) 134 | 135 | for actual, expected in zip(selection, regions): 136 | self.assertRegionEqual(actual, expected) 137 | 138 | 139 | def testStripSelection(self): 140 | 141 | testString = " too much whitespace here " 142 | 143 | self.view.run_command("insert", {"characters": testString}) 144 | self.view.run_command("select_all") 145 | self.view.run_command("strip_selection") 146 | 147 | selection = self.view.sel() 148 | 149 | self.assertEqual(len(selection), 1) 150 | self.assertRegionEqual(selection[0], [2, 26]) 151 | 152 | 153 | def testStripSelectionWithPureWhitespace(self): 154 | 155 | testString = " " 156 | 157 | self.view.run_command("insert", {"characters": testString}) 158 | selection = self.view.sel() 159 | 160 | # cursor should stay at the end of the line 161 | self.view.run_command("select_all") 162 | self.view.run_command("strip_selection") 163 | 164 | self.assertEqual(len(selection), 1) 165 | self.assertRegionEqual(selection[0], [4, 4]) 166 | 167 | # cursor should be at the beginning of the line 168 | self.view.run_command("select_all") 169 | self.view.run_command("normalize_region_ends") 170 | self.view.run_command("strip_selection") 171 | 172 | self.assertEqual(len(selection), 1) 173 | self.assertRegionEqual(selection[0], [0, 0]) 174 | 175 | 176 | def testMultiFindAll(self): 177 | 178 | testString = "abc def - abc - def - def" 179 | 180 | self.view.run_command("insert", {"characters": testString}) 181 | selection = self.view.sel() 182 | 183 | # select the first occurrences of abc and def 184 | selection.clear() 185 | selection.add(sublime.Region(0, 3)) 186 | selection.add(sublime.Region(4, 7)) 187 | 188 | self.view.run_command("multi_find_all") 189 | 190 | self.assertEqual(len(selection), 5) 191 | expectedRegions = [[0, 3], [4, 7], [10, 13], [16, 19], [22, 25]] 192 | 193 | self.assertRegionsEqual(selection, expectedRegions) 194 | 195 | 196 | def testDecode(self): 197 | 198 | 199 | sel = self.decode_sel(">test< some->Test< >TEST<") 200 | 201 | print(sel) 202 | 203 | 204 | 205 | def decode_sel(self, content): 206 | 207 | splitted = re.split(r'([│><])', content) 208 | content = '' 209 | pos = 0 210 | regionStart = 0 211 | regions = [] 212 | for s in splitted: 213 | if s == '│': 214 | regions.append(pos) 215 | elif s == '<': 216 | regions.append(sublime.Region(regionStart, pos)) 217 | elif s == '>': 218 | regionStart = pos 219 | else: 220 | pos += len(s) 221 | content += s 222 | 223 | return content, regions 224 | 225 | 226 | def testBasicPreserveCase(self): 227 | 228 | testString = ">test< some->Test< some_test Some-Test >TEST<" 229 | testString, regions = self.decode_sel(testString) 230 | self.view.run_command("insert", {"characters": testString}) 231 | selection = self.view.sel() 232 | 233 | for region in regions: 234 | selection.add(region) 235 | 236 | self.view.run_command("preserve_case", {"newString": "case"}) 237 | 238 | self.assertEqual(self.view.substr(regions[0]), "case") 239 | self.assertEqual(self.view.substr(regions[1]), "Case") 240 | self.assertEqual(self.view.substr(regions[2]), "CASE") 241 | 242 | 243 | def testAdvancedPreserveCase(self): 244 | 245 | expectedStrings = ["some case", "some-Case", "some_case", "Some-Case", "SomeCase", "someCase", "SomeCASE"] 246 | testString = ">some test< >some-Test< >some_test< >Some-Test< >SomeTest< >someTest< >SomeTEST<" 247 | testString, regions = self.decode_sel(testString) 248 | self.view.run_command("insert", {"characters": testString}) 249 | selection = self.view.sel() 250 | 251 | for region in regions: 252 | selection.add(region) 253 | 254 | self.view.run_command("preserve_case", {"newString": "some case"}) 255 | 256 | for region, expectedString in zip(regions, expectedStrings): 257 | self.assertEqual(self.view.substr(region), expectedString) 258 | 259 | 260 | def assertRegionEqual(self, a, b): 261 | 262 | self.assertEqual(a.a, b[0]) 263 | self.assertEqual(a.b, b[1]) 264 | 265 | 266 | def assertRegionsEqual(self, selection, expectedRegions): 267 | 268 | for index, region in enumerate(expectedRegions): 269 | self.assertRegionEqual(selection[index], region) 270 | 271 | 272 | def selectRegions(self, regions): 273 | 274 | self.view.sel().clear() 275 | for regionTuple in regions: 276 | self.view.sel().add(sublime.Region(regionTuple[0], regionTuple[1])) 277 | -------------------------------------------------------------------------------- /tests/testPreserveCase.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from unittest import TestCase 3 | 4 | import os.path, sys 5 | 6 | from importlib import import_module 7 | 8 | MultiEditUtils = import_module(".MultiEditUtils", "MultiEditUtils") 9 | 10 | class TestPreserveCase(TestCase): 11 | 12 | def setUp(self): 13 | 14 | self.view = sublime.active_window().new_file() 15 | self.cmd = MultiEditUtils.PreserveCaseCommand(None) 16 | 17 | def tearDown(self): 18 | 19 | if self.view: 20 | self.view.set_scratch(True) 21 | self.view.window().run_command("close_file") 22 | 23 | def assertListEqual(self, listA, listB): 24 | 25 | self.assertEqual(len(listA), len(listB)) 26 | 27 | for idx, el in enumerate(listA): 28 | self.assertEqual(el, listB[idx]) 29 | 30 | 31 | def testAnalyzeString(self): 32 | 33 | meta = self.cmd.analyzeString("a-BU-Cap-MiX") 34 | 35 | Case = MultiEditUtils.Case 36 | 37 | self.assertEqual(meta.separator, "-") 38 | self.assertListEqual(meta.cases, [Case.lower, Case.upper, Case.capitalized, Case.mixed]) 39 | self.assertListEqual(meta.stringGroups, ["a", "BU", "Cap", "MiX"]) 40 | 41 | 42 | def testSplitByCase(self): 43 | 44 | self.assertListEqual(self.cmd.splitByCase("abcDefGhi"), ["abc", "Def", "Ghi"]) 45 | self.assertListEqual(self.cmd.splitByCase("AbcDefGhi"), ["Abc", "Def", "Ghi"]) 46 | self.assertListEqual(self.cmd.splitByCase("AbcDEF"), ["Abc", "DEF"]) 47 | self.assertListEqual(self.cmd.splitByCase("ABCDef"), ["ABCD", "ef"]) 48 | self.assertListEqual(self.cmd.splitByCase("AbcDEFGhi"), ["Abc", "DEFG", "hi"]) 49 | 50 | 51 | 52 | def testReplaceStringWithCase_Equal(self): 53 | 54 | oldString = "test-TEST-Test" 55 | newStringGroups = ["case", "case", "case"] 56 | replacedString = self.cmd.replaceStringWithCase(oldString, newStringGroups) 57 | 58 | self.assertEqual(replacedString, "case-CASE-Case") 59 | 60 | def testReplaceStringWithCase_Less(self): 61 | 62 | oldString = "test-TEST-Test" 63 | newStringGroups = ["case", "case"] 64 | replacedString = self.cmd.replaceStringWithCase(oldString, newStringGroups) 65 | 66 | self.assertEqual(replacedString, "case-CASE") 67 | 68 | 69 | def testReplaceStringWithCase_More(self): 70 | 71 | oldString = "test-TEST-Test" 72 | newStringGroups = ["case", "case", "case", "case"] 73 | replacedString = self.cmd.replaceStringWithCase(oldString, newStringGroups) 74 | 75 | self.assertEqual(replacedString, "case-CASE-Case-Case") 76 | -------------------------------------------------------------------------------- /tests/testSelectionFields.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | 3 | import sublime 4 | from unittest import TestCase 5 | 6 | _ST3 = sublime.version() >= "3000" 7 | version = sublime.version() 8 | 9 | 10 | content_string = """Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 11 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 12 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 13 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 14 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 15 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.""" 16 | 17 | 18 | def to_region(v): 19 | if isinstance(v, int): 20 | region = sublime.Region(v, v) 21 | elif isinstance(v, sublime.Region): 22 | region = v 23 | else: 24 | region = sublime.Region(v[0], v[1]) 25 | return region 26 | 27 | 28 | class TestSelectionFields(TestCase): 29 | def setUp(self): 30 | self.view = sublime.active_window().new_file() 31 | regions = [(12, 14), (32, 30), 50, 60] 32 | self.start_regions = list(map(to_region, regions)) 33 | 34 | self.view.run_command("insert", {"characters": content_string}) 35 | self.select_regions(self.start_regions) 36 | 37 | def tearDown(self): 38 | if self.view: 39 | self.view.set_scratch(True) 40 | self.view.window().run_command("close_file") 41 | 42 | def assertSelectionEqual(self, sel1, sel2): 43 | self.assertEqual(len(sel1), len(sel2)) 44 | for i in range(len(sel1)): 45 | self.assertEqual(to_region(sel1[i]), to_region(sel2[i])) 46 | 47 | def select_regions(self, regions): 48 | self.view.sel().clear() 49 | if _ST3: 50 | self.view.sel().add_all(map(to_region, regions)) 51 | else: 52 | for region in regions: 53 | self.view.sel().add(to_region(region)) 54 | 55 | def test_toggle(self): 56 | """Test whether the toggle works.""" 57 | view = self.view 58 | regions = list(self.start_regions) 59 | 60 | view.run_command("selection_fields", {"mode": "toggle"}) 61 | 62 | self.assertEqual(len(view.sel()), 1) 63 | self.assertEqual(view.sel()[0], regions[0]) 64 | stored_regions = view.get_regions("meu_sf_stored_selections") 65 | self.assertSelectionEqual(regions[1:], stored_regions) 66 | 67 | view.run_command("selection_fields", {"mode": "toggle"}) 68 | self.assertEqual(len(view.sel()), len(regions)) 69 | self.assertSelectionEqual(view.sel(), regions) 70 | 71 | def test_smart_run(self): 72 | """Test whether a full run with the smart mode works.""" 73 | view = self.view 74 | regions = list(self.start_regions) 75 | 76 | view.run_command("selection_fields", {"mode": "smart"}) 77 | 78 | for i in range(len(regions)): 79 | self.assertEqual(len(view.sel()), 1) 80 | self.assertEqual(view.sel()[0], regions[i]) 81 | stored_regions = view.get_regions("meu_sf_stored_selections") 82 | self.assertSelectionEqual(regions[:i] + regions[i+1:], 83 | stored_regions) 84 | view.run_command("selection_fields", {"mode": "smart"}) 85 | self.assertSelectionEqual(view.sel(), regions) 86 | 87 | def test_smart_move(self): 88 | """ 89 | Test whether moving during a run, results in the corresponding 90 | caret positions after the run. 91 | """ 92 | view = self.view 93 | regions = list(self.start_regions) 94 | 95 | view.run_command("selection_fields", {"mode": "smart"}) 96 | 97 | for i in range(len(regions)): 98 | sel = view.sel()[0] 99 | if sel.empty(): 100 | regions[i] = sel.end() + i + 1 101 | else: 102 | regions[i] = sel.end() + i 103 | for _ in range(i + 1): 104 | view.run_command("move", {"by": "characters", "forward": True}) 105 | view.run_command("selection_fields", {"mode": "smart"}) 106 | self.assertSelectionEqual(view.sel(), regions) 107 | 108 | def test_smart_add_selections(self): 109 | """ 110 | Test whether adding carets during a run, results in the 111 | corresponding caret positions after the run. 112 | """ 113 | view = self.view 114 | regions = list(self.start_regions) 115 | 116 | view.run_command("selection_fields", {"mode": "smart"}) 117 | 118 | for i, v in enumerate(self.start_regions): 119 | sel = view.sel()[0] 120 | new_sel = to_region(sel.begin() - 1) 121 | view.sel().add(new_sel) 122 | regions.insert(i * 2, to_region(new_sel)) 123 | 124 | view.run_command("selection_fields", {"mode": "smart"}) 125 | self.assertSelectionEqual(view.sel(), regions) 126 | 127 | def test_jump_remove(self): 128 | """ 129 | Test whether jumps remove other selections. 130 | """ 131 | view = self.view 132 | 133 | view.run_command("selection_fields", {"mode": "smart"}) 134 | 135 | jumps = 3 136 | for _ in range(jumps): 137 | view.run_command("selection_fields", {"mode": "smart"}) 138 | self.assertSelectionEqual(view.sel(), [self.start_regions[jumps]]) 139 | 140 | def test_add(self): 141 | """ 142 | Test whether it is possible to add fields via the add mode. 143 | """ 144 | view = self.view 145 | regions = list(self.start_regions) 146 | add_regions_list = [(16, 17), 54, 109] 147 | view.run_command("selection_fields", {"mode": "add"}) 148 | 149 | view.sel().clear() 150 | self.select_regions(add_regions_list) 151 | 152 | view.run_command("selection_fields", {"mode": "add"}) 153 | view.run_command("move", {"by": "characters", "forward": True}) 154 | view.run_command("selection_fields", 155 | {"mode": "pop", "only_other": True}) 156 | 157 | # add the added regions and sort it to retrieve the desired selections 158 | regions.extend(map(to_region, add_regions_list)) 159 | regions.sort(key=lambda sel: sel.begin()) 160 | self.assertSelectionEqual(view.sel(), regions) 161 | 162 | def test_subtract(self): 163 | """Test whether subtract fields works properly.""" 164 | view = self.view 165 | regions_list = [(16, 35), 54, 60, (100, 103)] 166 | subtract_regions_list = [(2, 10), (14, 20), 54, (99, 120)] 167 | result_regions_list = [(20, 35), 60] 168 | 169 | self.select_regions(regions_list) 170 | 171 | view.run_command("selection_fields", {"mode": "add"}) 172 | 173 | self.select_regions(subtract_regions_list) 174 | 175 | view.run_command("selection_fields", {"mode": "subtract"}) 176 | 177 | view.run_command("selection_fields", 178 | {"mode": "pop", "only_other": True}) 179 | 180 | # add the added regions and sort it to retrieve the desired selections 181 | regions = list(map(to_region, result_regions_list)) 182 | self.assertSelectionEqual(view.sel(), regions) 183 | --------------------------------------------------------------------------------