├── .gitignore ├── .vscodeignore ├── images └── icon.png ├── .vscode └── launch.json ├── CHANGELOG.md ├── package.json ├── extension.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .gitignore 3 | test/** 4 | node_modules/** 5 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rioj7/select-by/HEAD/images/icon.png -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "extensionHost", 9 | "request": "launch", 10 | "name": "Launch Extension", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.20.2] 2025-02-17 4 | ### Fixed 5 | - `selectby.moveSelections` : correct behavior when using `start`, `end`, `anchor`, `active` 6 | 7 | ## [1.20.1] 2024-08-27 8 | ### Modified 9 | - `selectby.pasteClipboard` : select pasted text if initial selection is empty (editBuilder.replace() has changed behavior) 10 | 11 | ## [1.20.0] 2022-12-28 12 | ### Added 13 | - `selectby.regex` : Multi Cursor support 14 | 15 | ## [1.19.0] 2022-12-19 16 | ### Added 17 | - `selectby.addSelectionToNextFindMatchMultiCursor` : Multi Cursor variant of `editor.action.addSelectionToNextFindMatch` (`Ctrl+D`) 18 | 19 | ## [1.18.0] 2022-11-03 20 | ### Added 21 | - `moveby.calculation` : ask for numeric value for variable `relative` 22 | 23 | ## [1.17.0] 2022-09-05 24 | ### Added 25 | - `selectby.moveSelections` : move selections start/end/anchor/active a given offset 26 | 27 | ## [1.16.1] 2022-07-26 28 | ### Fixed 29 | - `selectby.regex` : fix: shows QuickPick list if args in key binding is an Array with regex name 30 | 31 | ## [1.16.0] 2022-06-15 32 | ### Added 33 | - `moveby.regex` : show QuickPick list of predefined searches if called from Command Palette or no args in key binding 34 | 35 | ## [1.15.0] 2022-05-13 36 | ### Added 37 | - `selectby.mark-restore` : restore position of cursors to mark locations 38 | 39 | ## [1.14.1] 2022-04-02 40 | ### Added 41 | - Create and modify Multi Cursors with the keyboard: 42 | - `selectby.addNewSelection` 43 | - `selectby.moveLastSelectionActive` 44 | - `selectby.moveLastSelection` 45 | 46 | ## [1.13.0] 2022-03-11 47 | ### Added 48 | - `selectby.anchorAndActiveByRegex` : Modify the anchor and active position of the selection(s) 49 | 50 | ## [1.12.0] 2022-02-17 51 | ### Added 52 | - `forwardShrink` and `backwardShrink` to reduce selection 53 | - add CHANGELOG.md 54 | 55 | ## [1.11.0] 2022-01-30 56 | ### Added 57 | - `moveby.calculation` by offset 58 | 59 | ## [1.10.0] 2022-01-05 60 | ### Added 61 | - `selectby.anchorAndActiveSeparate` 62 | 63 | ## [1.9.0] 2021-11-05 64 | ### Added 65 | - `selectby.mark` : argument `first` to reset call number 66 | - web extension 67 | 68 | ## [1.8.0] 2021-09-22 69 | ### Added 70 | - `selectby.linenr` : `inselection` only places cursors in the selections 71 | 72 | ## [1.7.0] 2021-08-19 73 | ### Added 74 | - `selectby.mark` : Mark position of cursor(s), create selection(s) on next mark 75 | 76 | ## [1.6.0] 2021-08-02 77 | ### Added 78 | - `forward/backwardAllowCurrentPosition` 79 | 80 | ## [1.5.1] 2021-06-27 81 | ### Added 82 | - `moveby.regex` fix a few cases 83 | 84 | ## [1.5.0] 2021-06-18 85 | ### Added 86 | - `moveby.regex` add `checkCurrent` option 87 | 88 | ## [1.4.0] 2021-05-24 89 | ### Added 90 | - `selectby.swapActive` swap cursor position within selection(s) 91 | 92 | ## [1.3.1] 2021-03-22 93 | ### Added 94 | - `moveby.calculation` now has `selections` variable 95 | 96 | ## [1.3.0] 2021-03-06 97 | ### Added 98 | - `selectby.regex` in keybinding can have an object as `args` property 99 | 100 | ## [1.2.0] 2021-02-23 101 | ### Added 102 | - `moveby.regex` has repeat property with ask possibility 103 | 104 | ## [1.1.0] 2021-02-22 105 | ### Added 106 | - `selectby.removeCursor(Above|Below)` reduce number of Multi Cursors 107 | 108 | ## [1.0.0] 2020-11-25 109 | ### Added 110 | - `moveby.calculation` move the cursor to `lineNr:charPos` with a calculation 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "select-by", 3 | "displayName": "Select By", 4 | "description": "Select text range based on certain criteria (regex,...) and move cursor by regex", 5 | "publisher": "rioj7", 6 | "license": "MIT", 7 | "version": "1.20.2", 8 | "engines": {"vscode": "^1.55.0"}, 9 | "categories": ["Other"], 10 | "keywords": ["select","text","range","move","cursor","multi","find"], 11 | "icon": "images/icon.png", 12 | "galleryBanner": {"color": "#000080", "theme": "dark"}, 13 | "activationEvents": [ 14 | "onCommand:selectby.regex1", 15 | "onCommand:selectby.regex2", 16 | "onCommand:selectby.regex3", 17 | "onCommand:selectby.regex4", 18 | "onCommand:selectby.regex5", 19 | "onCommand:selectby.regex", 20 | "onCommand:selectby.pasteClipboard", 21 | "onCommand:selectby.lineNr", 22 | "onCommand:selectby.removeCursorBelow", 23 | "onCommand:selectby.removeCursorAbove", 24 | "onCommand:selectby.swapActive", 25 | "onCommand:selectby.anchorAndActiveSeparate", 26 | "onCommand:selectby.anchorAndActiveByRegex", 27 | "onCommand:selectby.mark", 28 | "onCommand:selectby.mark-restore", 29 | "onCommand:selectby.addNewSelection", 30 | "onCommand:selectby.moveLastSelection", 31 | "onCommand:selectby.moveLastSelectionActive", 32 | "onCommand:selectby.moveSelections", 33 | "onCommand:selectby.addSelectionToNextFindMatchMultiCursor", 34 | "onCommand:moveby.regex", 35 | "onCommand:moveby.calculation", 36 | "onCommand:selectby.addNewSelectionAtOffset" 37 | ], 38 | "contributes": { 39 | "commands": [ 40 | { 41 | "command": "selectby.regex1", 42 | "title": "Select text range based on regex 1", 43 | "category": "SelectBy" 44 | }, 45 | { 46 | "command": "selectby.regex2", 47 | "title": "Select text range based on regex 2", 48 | "category": "SelectBy" 49 | }, 50 | { 51 | "command": "selectby.regex3", 52 | "title": "Select text range based on regex 3", 53 | "category": "SelectBy" 54 | }, 55 | { 56 | "command": "selectby.regex4", 57 | "title": "Select text range based on regex 4", 58 | "category": "SelectBy" 59 | }, 60 | { 61 | "command": "selectby.regex5", 62 | "title": "Select text range based on regex 5", 63 | "category": "SelectBy" 64 | }, 65 | { 66 | "command": "selectby.regex", 67 | "title": "Select text range based on regex", 68 | "category": "SelectBy" 69 | }, 70 | { 71 | "command": "selectby.pasteClipboard", 72 | "title": "Paste clipboard and select", 73 | "category": "SelectBy" 74 | }, 75 | { 76 | "command": "selectby.lineNr", 77 | "title": "Place cursor based on line number, uses boolean expression", 78 | "category": "SelectBy" 79 | }, 80 | { 81 | "command": "selectby.removeCursorBelow", 82 | "title": "Remove Cursor from Below", 83 | "category": "SelectBy" 84 | }, 85 | { 86 | "command": "selectby.removeCursorAbove", 87 | "title": "Remove Cursor from Above", 88 | "category": "SelectBy" 89 | }, 90 | { 91 | "command": "selectby.swapActive", 92 | "title": "Swap the anchor and active (cursor) position of the selection(s)", 93 | "category": "SelectBy" 94 | }, 95 | { 96 | "command": "selectby.anchorAndActiveSeparate", 97 | "title": "Create separate cursors for anchor and active position of the selection(s)", 98 | "category": "SelectBy" 99 | }, 100 | { 101 | "command": "selectby.anchorAndActiveByRegex", 102 | "title": "Modify the anchor and active position of the selection(s)", 103 | "category": "SelectBy" 104 | }, 105 | { 106 | "command": "selectby.mark", 107 | "title": "Mark position of cursor(s), create selection(s) on next mark", 108 | "category": "SelectBy" 109 | }, 110 | { 111 | "command": "selectby.mark-restore", 112 | "title": "Restore the position of the cursors to the marked location", 113 | "category": "SelectBy" 114 | }, 115 | { 116 | "command": "selectby.addNewSelection", 117 | "title": "Add a new selection at an offset (default: 1)", 118 | "category": "SelectBy" 119 | }, 120 | { 121 | "command": "selectby.moveLastSelectionActive", 122 | "title": "Modify (extend/reduce) the last selection by moving the Active position \"offset\" of characters left/right (default: 1)", 123 | "category": "SelectBy" 124 | }, 125 | { 126 | "command": "selectby.moveLastSelection", 127 | "title": "Move the last selection number of characters left/right (default: 1)", 128 | "category": "SelectBy" 129 | }, 130 | { 131 | "command": "selectby.addSelectionToNextFindMatchMultiCursor", 132 | "title": "Add Selection To Next Find Match - Multi Cursor", 133 | "category": "SelectBy" 134 | }, 135 | { 136 | "command": "moveby.regex", 137 | "title": "Move cursor based on regex", 138 | "category": "MoveBy" 139 | }, 140 | { 141 | "command": "moveby.calculation", 142 | "title": "Move cursor based on calculation by keybinding", 143 | "category": "MoveBy" 144 | } 145 | ], 146 | "configuration": { 147 | "title": "Select By", 148 | "properties": { 149 | "selectby.regexes": { 150 | "type": "object", 151 | "scope": "resource", 152 | "description": "Object with parameters for the different regexes", 153 | "default": {}, 154 | "patternProperties": { 155 | "^.+$": { 156 | "type": "object", 157 | "properties": { 158 | "flags": { 159 | "type": "string", 160 | "description": "(Optional) string with the regex flags \"i\" and/or \"m\" (default: \"\")" 161 | }, 162 | "backward": { 163 | "type": ["string", "boolean"], 164 | "description": "(Optional) regular expression to search from the selection start (or cursor) to the start of the file, or false (to override User setting)" 165 | }, 166 | "forward": { 167 | "type": ["string", "boolean"], 168 | "description": "(Optional) regular expression to search for from the selection end (or cursor) to the end of the file, or false (to override User setting)" 169 | }, 170 | "forwardAllowCurrentPosition": { 171 | "type": "boolean", 172 | "description": "(Optional) is the current selection end an allowed forward position (default: true)" 173 | }, 174 | "forwardShrink": { 175 | "type": "boolean", 176 | "description": "(Optional) do we reduce (shrink) at the current selection end. Find previous \"forward\" regular expression relative to selection end. (default: false)" 177 | }, 178 | "backwardAllowCurrentPosition": { 179 | "type": "boolean", 180 | "description": "(Optional) is the current selection start an allowed backward position (default: true)" 181 | }, 182 | "backwardShrink": { 183 | "type": "boolean", 184 | "description": "(Optional) do we reduce (shrink) at the current selection start. Find next \"backward\" regular expression relative to selection start. (default: false)" 185 | }, 186 | "forwardNext": { 187 | "type": ["string", "boolean"], 188 | "description": "(Optional) regular expression to search for starting at the end of the forward search to the end of the file, or false (to override User setting)" 189 | }, 190 | "backwardInclude": { 191 | "type": "boolean", 192 | "description": "(Optional) should the matched backward search text be part of the selection (default: true)" 193 | }, 194 | "forwardInclude": { 195 | "type": "boolean", 196 | "description": "(Optional) should the matched forward search text be part of the selection (default: true)" 197 | }, 198 | "forwardNextInclude": { 199 | "type": "boolean", 200 | "description": "(Optional) should the matched forwardNext search text be part of the selection (default: true)" 201 | }, 202 | "forwardNextExtendSelection": { 203 | "type": "boolean", 204 | "description": "(Optional) should we extend the selection with the matched forwardNext search text if the begin of the selection matches the forward regex (default: false)" 205 | }, 206 | "surround": { 207 | "type": ["string", "boolean"], 208 | "description": "(Optional) regular expression to search around the current selection, the selection is somewhere in the text to select, or false (to override User setting)" 209 | }, 210 | "copyToClipboard": { 211 | "type": "boolean", 212 | "description": "(Optional) copy the selection to the clipboard (default: false)" 213 | }, 214 | "showSelection": { 215 | "type": "boolean", 216 | "description": "(Optional) modify the selection to include the new searched positions. Useful if `copyToClipboard` is true. (default: true)" 217 | }, 218 | "debugNotify": { 219 | "type": "boolean", 220 | "description": "(Optional) show a notify message of the used search properties (User and Workspace properties are merged) (default: false)" 221 | }, 222 | "moveby": { 223 | "type": "string", 224 | "description": "(Optional) regular expression to search for, Used only by moveby.regex" 225 | }, 226 | "label": { 227 | "type": "string", 228 | "description": "(Optional) Label to use in the QuickPick list for the command selectby.regex" 229 | }, 230 | "description": { 231 | "type": "string", 232 | "description": "(Optional) Description to use in the QuickPick list on the same line for the command selectby.regex" 233 | }, 234 | "detail": { 235 | "type": "string", 236 | "description": "(Optional) Detail to use in the QuickPick list on a separate line for the command selectby.regex" 237 | } 238 | }, 239 | "dependencies": { 240 | "backwardInclude": ["backward"], 241 | "backwardShrink": ["backward"], 242 | "forwardInclude": ["forward"], 243 | "forwardShrink": ["forward"], 244 | "backwardAllowCurrentPosition": ["backward"], 245 | "forwardAllowCurrentPosition": ["forward"], 246 | "forwardNextInclude": ["forwardNext"], 247 | "forwardNextExtendSelection": ["forwardNext"] 248 | } 249 | } 250 | }, 251 | "additionalProperties": false 252 | }, 253 | "moveby.revealType": { 254 | "type": "string", 255 | "scope": "resource", 256 | "description": "How to reveal the cursor if it moves outside visible range.", 257 | "default": "Default", 258 | "enum": ["AtTop", "Default", "InCenter", "InCenterIfOutsideViewport"] 259 | }, 260 | "moveby.regexes": { 261 | "type": "object", 262 | "scope": "resource", 263 | "description": "Object with parameters for the different regexes", 264 | "default": {}, 265 | "patternProperties": { 266 | "^.+$": { 267 | "type": ["object", "array"], 268 | "properties": { 269 | "flags": { 270 | "type": "string", 271 | "description": "(Optional) string with the regex flags \"i\" and/or \"m\" (default: \"\")" 272 | }, 273 | "regex": { 274 | "type": "string", 275 | "description": "(Optional) the regex to use, if not given ask for a regex" 276 | }, 277 | "ask": { 278 | "type": "boolean", 279 | "description": "(Optional) ask for a regex if \"regex\" property not given" 280 | }, 281 | "properties": { 282 | "type": "array", 283 | "description": "(Optional) strings that determine how to use the regex: next/prev, start/end, wrap/nowrap (default: [\"next\", \"end\", \"nowrap\"])", 284 | "items": { 285 | "enum": ["next", "prev", "start", "end", "wrap", "nowrap"] 286 | }, 287 | "uniqueItems": true 288 | }, 289 | "repeat": { 290 | "type": ["integer", "string"], 291 | "description": "(Optional) how many times to perform the search, or \"ask\" (default: 1)" 292 | }, 293 | "checkCurrent": { 294 | "type": "boolean", 295 | "description": "(Optional) check if current cursor position is a possible match (default: false)" 296 | }, 297 | "debugNotify": { 298 | "type": "boolean", 299 | "description": "(Optional) show a notify message of the used search properties (User and Workspace properties are merged) (default: false)" 300 | }, 301 | "label": { 302 | "type": "string", 303 | "description": "(Optional) Label to use in the QuickPick list for the command selectby.regex" 304 | }, 305 | "description": { 306 | "type": "string", 307 | "description": "(Optional) Description to use in the QuickPick list on the same line for the command selectby.regex" 308 | }, 309 | "detail": { 310 | "type": "string", 311 | "description": "(Optional) Detail to use in the QuickPick list on a separate line for the command selectby.regex" 312 | } 313 | } 314 | } 315 | } 316 | } 317 | } 318 | } 319 | }, 320 | "main": "./extension.js", 321 | "browser": "./extension.js", 322 | "homepage": "https://github.com/rioj7/select-by", 323 | "bugs": { 324 | "url": "https://github.com/rioj7/select-by/issues" 325 | }, 326 | "repository": { 327 | "type": "git", 328 | "url": "https://github.com/rioj7/select-by.git" 329 | }, 330 | "devDependencies": { 331 | "@types/assert": "^1.5.4", 332 | "@types/mocha": "^8.2.1", 333 | "glob": "^7.1.6", 334 | "mocha": "^8.3.0", 335 | "simple-mock": "^0.8.0" 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | 3 | const getConfigRegExes = (config) => vscode.workspace.getConfiguration(config, null).get('regexes'); 4 | const getConfigSelectByRegExes = () => getConfigRegExes('selectby'); 5 | const getConfigMoveByRegExes = () => getConfigRegExes('moveby'); 6 | const getProperty = (obj, prop, deflt) => { return obj.hasOwnProperty(prop) ? obj[prop] : deflt; }; 7 | const getPropertyOrGlobal = (obj, prop, globalProps, deflt) => { return getProperty(obj, prop, getProperty(globalProps, prop, deflt)); }; 8 | const isString = obj => typeof obj === 'string'; 9 | const isArray = obj => Array.isArray(obj); 10 | const isObject = obj => (typeof obj === 'object') && !isArray(obj); 11 | const isPosInteger = value => /^\d+$/.test(value); 12 | const isInteger = value => /^-?\d+$/.test(value); 13 | /** @param {Array} a @param {Array} b */ 14 | const zip = (a, b) => a.map((k, i) => [k, b[i]]); 15 | 16 | class ConfigRegex { 17 | constructor(config) { 18 | this.recentlyUsed = []; 19 | this.config = config; 20 | } 21 | isValidRegex(regex) { return false; } 22 | defaultOnEmptyList() { return undefined; } 23 | async getRegexKey(args) { 24 | if (args !== undefined) { return args; } 25 | let regexes = getConfigRegExes(this.config); 26 | let qpItems = []; 27 | for (const key in regexes) { 28 | if (!regexes.hasOwnProperty(key)) { continue; } 29 | const regex = regexes[key]; 30 | if (!this.isValidRegex(regex)) { continue; } 31 | let label = getProperty(regex, 'label', key); 32 | if (getProperty(regex, 'copyToClipboard')) { label += ' $(clippy)'; } 33 | if (getProperty(regex, 'debugNotify')) { label += ' $(debug)'; } 34 | let description = getProperty(regex, 'description'); 35 | let detail = getProperty(regex, 'detail'); 36 | qpItems.push( { idx: qpItems.length, regexKey: key, label, description, detail } ); 37 | } 38 | if (qpItems.length === 0) { 39 | let deflt = this.defaultOnEmptyList(); 40 | if (deflt === undefined) { 41 | vscode.window.showInformationMessage("No usable regex found"); 42 | } 43 | return deflt; 44 | } 45 | const sortIndex = a => { 46 | let idx = this.recentlyUsed.findIndex( e => e === a.regexKey ); 47 | return idx >= 0 ? idx : this.recentlyUsed.length + a.idx; 48 | }; 49 | // we could update recentlyUsed and remove regexKeys that are not found in the setting 50 | // TODO when we persistently save recentlyUsed 51 | qpItems.sort( (a, b) => sortIndex(a) - sortIndex(b) ); 52 | return vscode.window.showQuickPick(qpItems) 53 | .then( item => { 54 | if (!item) { return undefined; } 55 | let regexKey = item.regexKey; 56 | this.recentlyUsed = [regexKey].concat(this.recentlyUsed.filter( e => e !== regexKey )); 57 | return regexKey; 58 | }); 59 | } 60 | } 61 | 62 | class ConfigRegexSelectBy extends ConfigRegex { 63 | constructor() { 64 | super('selectby'); 65 | } 66 | isValidRegex(regex) { 67 | return getProperty(regex, 'backward') || getProperty(regex, 'forward') || getProperty(regex, 'forwardNext') || getProperty(regex, 'surround'); 68 | } 69 | } 70 | 71 | class ConfigRegexMoveBy extends ConfigRegex { 72 | constructor() { 73 | super('moveby'); 74 | } 75 | isValidRegex(regex) { 76 | // if (isObject(regex)) { 77 | // return getProperty(regex, 'regex') || getProperty(regex, 'ask'); 78 | // } 79 | return true; 80 | } 81 | defaultOnEmptyList() { return {}; } 82 | } 83 | 84 | function activate(context) { 85 | var getConfigRegEx = (regexKey, regexes) => { 86 | if (!regexes.hasOwnProperty(regexKey)) { 87 | vscode.window.showErrorMessage(regexKey+" not found."); 88 | return undefined; 89 | } 90 | let config = regexes[regexKey]; 91 | if (getProperty(config, "debugNotify", false)) { 92 | vscode.window.showInformationMessage(JSON.stringify(config)); 93 | } 94 | return config; 95 | }; 96 | var getConfigSelectByRegEx = regexKey => { 97 | return getConfigRegEx(regexKey, getConfigSelectByRegExes()); 98 | }; 99 | var getConfigMoveByRegEx = regexKey => { 100 | return getConfigRegEx(regexKey, getConfigMoveByRegExes()); 101 | }; 102 | function range(size, startAt = 0) { return [...Array(size).keys()].map(i => i + startAt); } // https://stackoverflow.com/a/10050831/9938317 103 | function expressionFunc(expr, args) { 104 | try { 105 | return Function(`"use strict";return (function calcexpr(${args}) { 106 | let val = ${expr}; 107 | if (isNaN(val)) { throw new Error("Error calculating: ${expr}"); } 108 | return val; 109 | })`)(); 110 | } 111 | catch (ex) { 112 | let message = ex.message; 113 | if (message.indexOf("';'") >= 0) { message = "Incomplete expression"; } 114 | throw new Error(`${message} in ${expr}`); 115 | } 116 | } 117 | var configRegexSelectBy = new ConfigRegexSelectBy(); 118 | var configRegexMoveBy = new ConfigRegexMoveBy(); 119 | var processRegExKey = (regexKey, editor) => { 120 | if (!isString(regexKey)) { regexKey = 'regex' + regexKey.toString(10); } 121 | let search = getConfigSelectByRegEx(regexKey); 122 | processRegExSearch(search, editor); 123 | }; 124 | var processRegExSearch = (search, editor) => { 125 | if (!search) { return; } 126 | var docText = editor.document.getText(); 127 | updateEditorSelections(editor, 128 | editor.selections.map( selection => processRegExSearchSelection(search, editor, selection, docText) ), 129 | getProperty(search, "showSelection", true)); // found by johnnytemp in #10 130 | }; 131 | var processRegExSearchSelection = (search, editor, selection, docText) => { 132 | // position of cursor is "start" of selection 133 | var offsetCursor = editor.document.offsetAt(selection.start); 134 | var selectStart = offsetCursor; 135 | var flags = getProperty(search, "flags", "") + "g"; 136 | var regex; 137 | regex = getProperty(search, "backward"); 138 | if (regex && isString(regex)) { 139 | let incMatch = getProperty(search, "backwardInclude", true); 140 | let backwardShrink = getProperty(search, "backwardShrink", false); 141 | let backwardAllowCurrent = getProperty(search, "backwardAllowCurrentPosition", true); 142 | regex = new RegExp(regex, flags); 143 | if (backwardShrink) { 144 | regex.lastIndex = selectStart; 145 | selectStart = docText.length; 146 | let result; 147 | while ((result=regex.exec(docText)) != null) { 148 | selectStart = incMatch ? result.index : regex.lastIndex; 149 | if (incMatch && selectStart === offsetCursor) { continue; } 150 | break; 151 | } 152 | } else { 153 | selectStart = 0; 154 | regex.lastIndex = 0; 155 | while (true) { 156 | let prevLastIndex = regex.lastIndex; 157 | let result = regex.exec(docText); 158 | if (result == null) { break; } 159 | if (result.index >= offsetCursor) { break; } 160 | let newSelectStart = incMatch ? result.index : regex.lastIndex; 161 | if (prevLastIndex === regex.lastIndex) { // empty match 162 | regex.lastIndex = prevLastIndex + 1; 163 | } 164 | if (!backwardAllowCurrent && newSelectStart === offsetCursor) { continue; } 165 | selectStart = newSelectStart; 166 | } 167 | } 168 | } 169 | let currentSelectionEnd = editor.document.offsetAt(selection.end); 170 | let selectEnd = currentSelectionEnd; 171 | regex = getProperty(search, "forward"); 172 | let regexForwardNext = getProperty(search, "forwardNext"); 173 | let forwardNextInclude = getProperty(search, "forwardNextInclude", true); 174 | let forwardNextExtendSelection = getProperty(search, "forwardNextExtendSelection", false); 175 | let forwardAllowCurrent = getProperty(search, "forwardAllowCurrentPosition", true); 176 | let searchForwardNext = (forwardResult, startForwardNext) => { 177 | if (!(regexForwardNext && isString(regexForwardNext))) return [undefined, undefined]; 178 | let regexForwardNextModified = regexForwardNext.replace(/{{(\d+)}}/g, (match, p1) => { 179 | let groupNr = parseInt(p1, 10); 180 | if (groupNr >= forwardResult.length) { return ""; } 181 | return forwardResult[groupNr]; 182 | }); 183 | let regex = new RegExp(regexForwardNextModified, flags); 184 | regex.lastIndex = startForwardNext; 185 | let matchStart = docText.length; 186 | let matchEnd = docText.length; 187 | let result; 188 | let incMatch = forwardNextInclude; 189 | while ((result=regex.exec(docText)) != null) { 190 | matchStart = result.index; 191 | matchEnd = incMatch ? regex.lastIndex : result.index; 192 | break; 193 | } 194 | return [matchStart, matchEnd]; 195 | }; 196 | let startForwardNext = selectEnd; 197 | let forwardResult = []; 198 | let needNewForwardSearch = true; 199 | if (regex && isString(regex)) { 200 | let forwardInclude = getProperty(search, "forwardInclude", true); 201 | let forwardShrink = getProperty(search, "forwardShrink", false); 202 | let incMatch = forwardInclude; 203 | if (regexForwardNext && isString(regexForwardNext)) { // we have to flip the incMatch 204 | incMatch = !incMatch; 205 | } 206 | regex = new RegExp(regex, flags); 207 | if (forwardNextExtendSelection) { 208 | selectStart = offsetCursor; // ignore any backward search 209 | let result; 210 | if (forwardInclude) { 211 | regex.lastIndex = selectStart; // check if forward is found at begin of selection 212 | if ((result=regex.exec(docText)) != null) { 213 | if (result.index === selectStart) { 214 | needNewForwardSearch = false; 215 | forwardResult = result.slice(); 216 | } 217 | } 218 | } else { // check if forward is exact before selection 219 | let matchEnd = docText.length; 220 | regex.lastIndex = 0; 221 | let result; 222 | while ((result=regex.exec(docText)) != null) { 223 | matchEnd = regex.lastIndex; 224 | if (matchEnd >= offsetCursor) break; 225 | } 226 | if (matchEnd === offsetCursor) { 227 | needNewForwardSearch = false; 228 | forwardResult = result.slice(); 229 | } 230 | } 231 | if (!needNewForwardSearch) { // test if forwardNext is at selectEnd 232 | if (searchForwardNext(forwardResult, selectEnd)[0] !== selectEnd) { 233 | needNewForwardSearch = true; 234 | } 235 | } 236 | } 237 | if (needNewForwardSearch) { 238 | if (forwardShrink) { 239 | selectEnd = 0; 240 | regex.lastIndex = 0; 241 | let result; 242 | while ((result=regex.exec(docText)) != null) { 243 | let newSelectEnd = incMatch ? regex.lastIndex : result.index; 244 | if (newSelectEnd >= currentSelectionEnd) { break; } 245 | selectEnd = newSelectEnd; 246 | startForwardNext = regex.lastIndex; 247 | forwardResult = result.slice(); 248 | } 249 | } else { 250 | regex.lastIndex = selectEnd; 251 | selectEnd = docText.length; 252 | startForwardNext = docText.length; 253 | let result; 254 | while ((result=regex.exec(docText)) != null) { 255 | selectEnd = incMatch ? regex.lastIndex : result.index; 256 | startForwardNext = regex.lastIndex; 257 | forwardResult = result.slice(); 258 | if (!forwardAllowCurrent && selectEnd === currentSelectionEnd) { continue; } 259 | break; 260 | } 261 | } 262 | } 263 | } 264 | if (regexForwardNext && isString(regexForwardNext)) { 265 | if (needNewForwardSearch) { 266 | selectStart = selectEnd; 267 | } 268 | selectEnd = searchForwardNext(forwardResult, startForwardNext)[1]; 269 | } 270 | regex = getProperty(search, "surround"); 271 | if (regex && isString(regex)) { 272 | regex = new RegExp(regex, flags); 273 | regex.lastIndex = 0; 274 | let result; 275 | while ((result=regex.exec(docText)) != null) { 276 | if (result.index <= offsetCursor && selectEnd <= regex.lastIndex) { 277 | selectStart = result.index; 278 | selectEnd = regex.lastIndex; 279 | break; 280 | } 281 | } 282 | } 283 | if (selectStart > selectEnd) { 284 | [selectStart, selectEnd] = [selectEnd, selectStart]; 285 | } 286 | if (getProperty(search, "copyToClipboard", false)) { 287 | vscode.env.clipboard.writeText(docText.substring(selectStart, selectEnd)).then((v)=>v, (v)=>null); 288 | } 289 | if (getProperty(search, "showSelection", true)) { 290 | selection = new vscode.Selection(editor.document.positionAt(selectStart), editor.document.positionAt(selectEnd)); 291 | } 292 | return selection; 293 | }; 294 | 295 | var selectbyRegEx = async (editor, args) => { 296 | if (isObject(args)) { 297 | processRegExSearch(args, editor); 298 | return; 299 | } 300 | let regexKey = await configRegexSelectBy.getRegexKey(args); 301 | if (regexKey === undefined) { return; } 302 | if (isArray(regexKey) && regexKey.length > 0) { 303 | regexKey = regexKey[0]; 304 | } 305 | if (isString(regexKey)) { 306 | processRegExKey(regexKey, editor); 307 | } 308 | }; 309 | let lastLineNrExInput = 'c+6k'; 310 | var selectBy_lineNrEx_Ask = async (args) => { 311 | if (args === undefined) { args = {}; } 312 | let lineNrEx = getProperty(args, 'lineNrEx'); 313 | if (lineNrEx) { return lineNrEx; } 314 | return vscode.window.showInputBox({"ignoreFocusOut":true, "prompt": "lineNr Expression to place cursors; c+6k ; inselection", "value": lastLineNrExInput}) 315 | .then( item => { 316 | if (isString(item) && item.length === 0) { item = undefined; } // accepted an empty inputbox 317 | if (item) {lastLineNrExInput = item; } 318 | return item; 319 | }); 320 | }; 321 | var recentlyUsedMoveByAskRegex = []; 322 | var moveBy_regex_Ask = async () => { 323 | return vscode.window.showInputBox({"ignoreFocusOut":true, "prompt": "RegEx to move to"}) 324 | // return vscode.window.showQuickPick(recentlyUsedMoveBy) 325 | .then( item => { 326 | if (isString(item) && item.length === 0) { item = undefined; } // accepted an empty inputbox 327 | if (item) { 328 | recentlyUsedMoveByAskRegex = [item].concat(recentlyUsedMoveByAskRegex.filter( e => e !== item )); 329 | } 330 | return item; 331 | }); 332 | }; 333 | var integer_Ask = async (prompt, positive = false) => { 334 | let validateInput = value => isInteger(value) ? undefined : 'Only integers'; 335 | if (positive) { 336 | validateInput = value => isPosInteger(value) ? undefined : 'Only positive integers'; 337 | } 338 | return vscode.window.showInputBox({ignoreFocusOut:true, prompt, value: "1", validateInput}) 339 | .then( item => { 340 | if (isString(item) && item.length === 0) { item = undefined; } // accepted an empty inputbox 341 | return item; 342 | }); 343 | }; 344 | var positiveInteger_Ask = async (prompt) => { return integer_Ask(prompt, true); }; 345 | 346 | /** @returns {vscode.Position} @param {vscode.TextEditor} editor @param {vscode.Position} currentPosition @param {RegExp} regex @param {boolean} findPrev @param {boolean} findStart @param {boolean} wrapCursor @param {boolean} checkCurrent */ 347 | var findRegexPosition = (editor, currentPosition, regex, findPrev, findStart, wrapCursor=false, checkCurrent=false) => { 348 | var docText = editor.document.getText(); 349 | var wrappedCursor = false; 350 | var offsetCursor = editor.document.offsetAt(currentPosition); 351 | var location = offsetCursor; 352 | if (regex) { 353 | if (checkCurrent) { 354 | regex.lastIndex = 0; 355 | let result; 356 | while ((result = regex.exec(docText))!==null) { 357 | location = findStart ? result.index : regex.lastIndex; 358 | if (location > offsetCursor) { break; } 359 | if (location === offsetCursor) { return editor.document.positionAt(location); } 360 | } 361 | } 362 | regex.lastIndex = findPrev ? 0 : offsetCursor; 363 | while (true) { 364 | let prevLastIndex = regex.lastIndex; 365 | let result = regex.exec(docText); 366 | if (result === null) { 367 | if (wrapCursor) { 368 | wrapCursor = undefined; // only wrap once 369 | if (findPrev) { 370 | if (location !== offsetCursor) { break; } // found one before offsetCursor 371 | return undefined; // leave cursor at current location, not found anywhere in the file 372 | } else { 373 | regex.lastIndex = 0; 374 | result = regex.exec(docText); 375 | if (result == null) { return undefined; } // leave cursor at current location, not found anywhere in the file 376 | if (result.index == 0) { // found at start of file 377 | location = findStart ? result.index : regex.lastIndex; 378 | break; 379 | } 380 | location = 0; 381 | offsetCursor = location; 382 | regex.lastIndex = location; 383 | continue; 384 | } 385 | } 386 | if (location !== offsetCursor) { break; } // found one before offsetCursor 387 | return undefined; // not found, skip cursor or leave at current location 388 | } 389 | var resultLocation = findStart ? result.index : regex.lastIndex; 390 | if (prevLastIndex === regex.lastIndex) { // search for empty line 391 | regex.lastIndex = prevLastIndex + 1; 392 | } 393 | if (findPrev) { 394 | // for the regex in the file: #hit >= 1 395 | if (resultLocation >= offsetCursor) { 396 | if (offsetCursor === docText.length && wrappedCursor && resultLocation === offsetCursor) { 397 | location = offsetCursor; // a hit at the very end of the file 398 | break; 399 | } 400 | if (location !== offsetCursor) { break; } // found one before offsetCursor 401 | if (wrapCursor) { 402 | wrapCursor = undefined; // only wrap once 403 | wrappedCursor = true; 404 | offsetCursor = docText.length; 405 | location = offsetCursor; 406 | regex.lastIndex = 0; 407 | continue; 408 | } 409 | return undefined; // not found, skip cursor or leave at current location 410 | } 411 | location = resultLocation; 412 | } else { // Next 413 | if (resultLocation > offsetCursor) { 414 | location = resultLocation; 415 | break; 416 | } 417 | } 418 | } 419 | } 420 | return editor.document.positionAt(location); 421 | }; 422 | /** @param {vscode.TextEditor} editor @param {vscode.Position} position */ 423 | var newPositionByProperties = (editor, position, props, propsGlobal) => { 424 | if (props === undefined) { return position; } 425 | let regex = getPropertyOrGlobal(props, 'regex', propsGlobal); 426 | if (regex === undefined) { return position; } 427 | let direction = getPropertyOrGlobal(props, 'direction', propsGlobal, 'next'); 428 | let repeat = getPropertyOrGlobal(props, 'repeat', propsGlobal, 1); 429 | let flags = getPropertyOrGlobal(props, 'flags', propsGlobal, '') + 'g'; 430 | regex = new RegExp(regex, flags); 431 | let findPrev = direction === 'prev'; 432 | let findStart = findPrev; 433 | range(repeat).forEach(i => { 434 | let newPosition = findRegexPosition(editor, position, regex, findPrev, findStart); 435 | if (newPosition !== undefined) { position = newPosition; } 436 | }); 437 | return position; 438 | }; 439 | /** @param {vscode.TextEditor} editor @param {vscode.Selection} selection */ 440 | var anchorAndActiveByRegex = (editor, selection, args) => { 441 | if (args === undefined) { args = {}; } 442 | let anchor = newPositionByProperties(editor, selection.anchor, getProperty(args, 'anchor'), args); 443 | let active = newPositionByProperties(editor, selection.active, getProperty(args, 'active'), args); 444 | return new vscode.Selection(anchor, active); 445 | }; 446 | /** @param {vscode.TextEditor} editor */ 447 | var selectbySelections = (editor, newSelection) => { 448 | let selections = editor.selections 449 | .map(newSelection) 450 | .filter( s => s !== undefined); 451 | updateEditorSelections(editor, selections); 452 | }; 453 | var updateEditorSelections = (editor, selections, reveal=true) => { 454 | if (selections.length === 0) return; 455 | editor.selections = selections; 456 | if (reveal) { 457 | var rng = new vscode.Range(editor.selection.start, editor.selection.start); 458 | editor.revealRange(rng, vscode.TextEditorRevealType[vscode.workspace.getConfiguration('moveby', null).get('revealType')]); 459 | } 460 | }; 461 | /** @param {readonly vscode.Selection[]} selections */ 462 | var sortSelections = selections => { 463 | let newSelections = [...selections]; 464 | return newSelections.sort((a, b) => { return a.start.compareTo(b.start); }) 465 | }; 466 | var movebyPositions = (editor, newPosition, repeat) => { 467 | if (!repeat) { repeat = 1; } 468 | let selections = editor.selections; 469 | range(repeat).forEach(i => { 470 | selections = selections 471 | .map(newPosition) 472 | .filter( pos => pos !== undefined) 473 | .map( position => new vscode.Selection(position, position) ); 474 | }); 475 | updateEditorSelections(editor, selections); 476 | }; 477 | var movebyRegEx = async (editor, args) => { 478 | if (args === undefined) { 479 | args = await configRegexMoveBy.getRegexKey(); 480 | } 481 | if (isString(args)) { 482 | args = getConfigMoveByRegEx(args); 483 | } 484 | if (args === undefined) { return; } 485 | let regex, flagsObj, properties; 486 | let checkCurrent = false; 487 | let repeat = '1'; 488 | if (Array.isArray(args)) { 489 | let regexKey = args[0]; 490 | let regexFBM = args[1]; 491 | let search = getConfigSelectByRegEx(regexKey); 492 | if (search === undefined) { return; } 493 | regex = getProperty(search, regexFBM); 494 | flagsObj = search; 495 | properties = args.slice(2); 496 | } else { 497 | regex = getProperty(args, 'regex'); 498 | if (!regex) { 499 | regex = await moveBy_regex_Ask(); 500 | } 501 | flagsObj = args; 502 | properties = getProperty(args, 'properties', []); 503 | checkCurrent = getProperty(args, 'checkCurrent', false); 504 | let repeatProp = getProperty(args, 'repeat'); 505 | if (repeatProp !== undefined) { 506 | repeat = (repeatProp === 'ask') ? await positiveInteger_Ask('Repeat count for move to') : repeatProp; 507 | repeat = String(repeat); 508 | } 509 | } 510 | if (!(regex && isString(regex))) { return; } // not found or Escaped Quickpick 511 | if (!(isPosInteger(repeat) && (Number(repeat)>0) )) { return; } // not found or Escaped InputBox 512 | let flags = getProperty(flagsObj, 'flags', '') + 'g'; 513 | regex = new RegExp(regex, flags); 514 | let findPrev = properties.indexOf('prev') >= 0; 515 | let findStart = properties.indexOf('start') >= 0; 516 | let wrapCursor = properties.indexOf('wrap') >= 0; 517 | movebyPositions(editor, s => findRegexPosition(editor, findPrev ? s.start : s.end, regex, findPrev, findStart, wrapCursor, checkCurrent), Number(repeat)); 518 | }; 519 | function calculatePosition(editor, selection, lineNrExFunc, charNrExFunc, offset, relative) { 520 | let arg = {selection: selection, currentLine: editor.document.lineAt(selection.start.line).text, selections: editor.selections, offset: offset, relative: relative}; 521 | return new vscode.Position(Math.floor(lineNrExFunc(arg)), Math.floor(charNrExFunc(arg))); 522 | } 523 | const transformCalculationEx = str => str.replace(/selections?|currentLine|offset|relative/g, 'arg.$&'); 524 | var movebyCalculation = async (editor, args) => { 525 | if (args === undefined) { args = {}; } 526 | let lineNrEx = getProperty(args, 'lineNrEx', 'selection.start.line'); 527 | let charNrEx = getProperty(args, 'charNrEx'); 528 | if (!charNrEx) { return; } 529 | let offset = {line:0, character:0}; 530 | if (lineNrEx.indexOf('offset') !== -1) { 531 | let offsetInput = await positiveInteger_Ask('Go to offset'); 532 | if (offsetInput === undefined) { return; } 533 | offset = editor.document.positionAt(Number(offsetInput)); 534 | } 535 | let relative = 0; 536 | if (lineNrEx.indexOf('relative') !== -1) { 537 | let relativeInput = await integer_Ask('Go to relative line/character'); 538 | if (relativeInput === undefined) { return; } 539 | relative = Number(relativeInput); 540 | } 541 | let lineNrExFunc = expressionFunc(transformCalculationEx(lineNrEx), 'arg'); 542 | let charNrExFunc = expressionFunc(transformCalculationEx(charNrEx), 'arg'); 543 | movebyPositions(editor, s => calculatePosition(editor, s, lineNrExFunc, charNrExFunc, offset, relative)); 544 | }; 545 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.swapActive', editor => { 546 | editor.selections = editor.selections.map( s => new vscode.Selection(s.active, s.anchor)); 547 | }) ); 548 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.anchorAndActiveSeparate', editor => { 549 | editor.selections = sortSelections(editor.selections).map( s => { 550 | if (s.isEmpty) { return s; } 551 | return [new vscode.Selection(s.start, s.start), new vscode.Selection(s.end, s.end)]; 552 | }).flat(); 553 | }) ); 554 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.anchorAndActiveByRegex', (editor, edit, args) => { 555 | if (args === undefined) { return; } 556 | selectbySelections(editor, s => anchorAndActiveByRegex(editor, s, args)); 557 | }) ); 558 | let markFirst; 559 | let markPositions = []; 560 | function resetMarks() { 561 | markFirst = true; 562 | markPositions = []; 563 | } 564 | resetMarks(); 565 | let markDecoration; 566 | (function createMarkDecoration() { 567 | let options = { before: { contentText: '◆', color: new vscode.ThemeColor('editor.selectionBackground') } }; 568 | markDecoration = vscode.window.createTextEditorDecorationType(options); 569 | })(); 570 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.mark', (editor, edit, args) => { 571 | if (!editor) { return; } 572 | if (args === undefined) { args = {}; } 573 | if (getProperty(args, 'first')) { resetMarks(); } 574 | if (markFirst) { 575 | markPositions = editor.selections.map( s => s.start ); 576 | markFirst = false; 577 | editor.setDecorations(markDecoration, markPositions.map( m => new vscode.Range(m, m) ) ); 578 | } else { 579 | editor.setDecorations(markDecoration, []); 580 | if (editor.selections.length !== markPositions.length ) { 581 | vscode.window.showWarningMessage(`Different number of cursors: ${editor.selections.length} != ${markPositions.length}`); 582 | } else { 583 | editor.selections = editor.selections.map( (s, i) => new vscode.Selection(markPositions[i], s.active) ); 584 | } 585 | resetMarks(); 586 | } 587 | }) ); 588 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.mark-restore', (editor, edit, args) => { 589 | if (!editor) { return; } 590 | if (markPositions.length == 0) { return; } 591 | if (args === undefined) { args = {}; } 592 | editor.selections = markPositions.map( m => new vscode.Selection(m, m) ); 593 | if (!getProperty(args, 'keepMarks')) { 594 | editor.setDecorations(markDecoration, []); 595 | resetMarks(); 596 | } 597 | }) ); 598 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.regex1', (editor, edit, args) => { processRegExKey(1, editor);}) ); 599 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.regex2', (editor, edit, args) => { processRegExKey(2, editor);}) ); 600 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.regex3', (editor, edit, args) => { processRegExKey(3, editor);}) ); 601 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.regex4', (editor, edit, args) => { processRegExKey(4, editor);}) ); 602 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.regex5', (editor, edit, args) => { processRegExKey(5, editor);}) ); 603 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.regex', (editor, edit, args) => { selectbyRegEx(editor, args);}) ); 604 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.pasteClipboard', async (editor, edit, args) => { 605 | if (editor.selections.length > 1) { 606 | vscode.window.showInformationMessage("Multi Cursor Paste and select not supported yet."); 607 | return; 608 | } 609 | let content = await vscode.env.clipboard.readText(); 610 | if (isString(content)) { 611 | let posStart = editor.selection.start; 612 | await editor.edit( editBuilder => editBuilder.replace(editor.selection, content) ); // need new editBuilder after await 613 | if (editor.selection.isEmpty) { 614 | editor.selections = [new vscode.Selection(posStart, editor.selection.end)]; 615 | } 616 | } 617 | }) ); 618 | let transform_line_modulo = (match, number) => `((n-c)%${number}==0 && n>=c)`; 619 | function transform_inselection(startLineNr, endLineNr) { 620 | return match => `((n>=${startLineNr}) && (n<=${endLineNr}))`; 621 | } 622 | class LineNrTest { 623 | /** @param {number} startLineNr @param {string} lineNrEx */ 624 | constructor(startLineNr, lineNrEx) { 625 | this.startLineNr = startLineNr; 626 | this.func = expressionFunc(lineNrEx, 'c,n'); 627 | } 628 | test(n) { return this.func(this.startLineNr+1, n+1); } // zero based line numbers 629 | } 630 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.lineNr', async (editor, edit, args) => { 631 | let lineNrEx = await selectBy_lineNrEx_Ask(args); 632 | if (!isString(lineNrEx) || lineNrEx.length == 0) { return; } 633 | try { 634 | lineNrEx = lineNrEx.replace(/c\s*\+\s*(\d+)\s*k/g, transform_line_modulo); 635 | /** @type {LineNrTest[]} */ 636 | let lineNrTests = []; 637 | if (lineNrEx.indexOf('inselection') >= 0) { 638 | for (const selection of editor.selections) { 639 | let selectionStartLineNr = selection.start.line; 640 | let selectionEndLineNr = selection.end.line; 641 | if (selection.end.character === 0) { 642 | selectionEndLineNr--; // cursor at start of line does not select any on that line 643 | } 644 | lineNrTests.push( new LineNrTest(selectionStartLineNr, lineNrEx.replace(/inselection/g, transform_inselection(selectionStartLineNr+1, selectionEndLineNr+1))) ); 645 | } 646 | } else { 647 | lineNrTests.push( new LineNrTest(editor.selection.start.line, lineNrEx) ); 648 | } 649 | let lineCount = editor.document.lineCount; 650 | let locations = []; 651 | for (let n = 0; n < lineCount; ++n) { 652 | if (lineNrTests.some( lineNrTest => lineNrTest.test(n) )) { 653 | let position = new vscode.Position(n, 0); 654 | if (locations.push(new vscode.Selection(position, position)) >= 10000) { break; } 655 | } 656 | } 657 | updateEditorSelections(editor, locations); 658 | } catch (ex) { 659 | vscode.window.showInformationMessage(ex.message); 660 | } 661 | }) ); 662 | let removeCursor = (editor, args, filterFunc) => { 663 | if (editor.selections.length == 1) { return; } 664 | let locations = editor.selections.sort((a, b) => { return a.start.compareTo(b.start); }).filter(filterFunc); 665 | updateEditorSelections(editor, locations); 666 | }; 667 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.removeCursorBelow', (editor, edit, args) => { 668 | removeCursor(editor, args, (sel, index, arr) => index < arr.length-1 ); 669 | }) ); 670 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.removeCursorAbove', (editor, edit, args) => { 671 | removeCursor(editor, args, (sel, index, arr) => index > 0 ); 672 | }) ); 673 | const positionAtOffset = (editor, position, args) => { 674 | let newOffset = editor.document.offsetAt(position) + getProperty(args, 'offset', 1); 675 | if (newOffset<0 || newOffset>editor.document.getText().length) { return undefined; } 676 | return editor.document.positionAt(newOffset); 677 | }; 678 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.addNewSelection', (editor, edit, args) => { 679 | if (args === undefined) { args = {}; } 680 | let selections = editor.selections.slice(); 681 | const newPosition = positionAtOffset(editor, selections[selections.length-1].end, args); 682 | if (!newPosition) { return; } 683 | selections.push(new vscode.Selection(newPosition, newPosition)); 684 | updateEditorSelections(editor, selections); 685 | }) ); 686 | // TODO name change 2022-04-02 687 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.addNewSelectionAtOffset', (editor, edit, args) => { 688 | vscode.window.showInformationMessage('selectby.addNewSelectionAtOffset renamed to selectby.addNewSelection'); 689 | }) ); 690 | const updateSelection = (editor, selection, argsAnchor, argsActive) => { 691 | let newAnchor = positionAtOffset(editor, selection.anchor, argsAnchor); 692 | let newActive = positionAtOffset(editor, selection.active, argsActive); 693 | if (!newAnchor || !newActive) { return; } 694 | return new vscode.Selection(newAnchor, newActive); 695 | }; 696 | const updateLastSelection = (editor, argsAnchor, argsActive) => { 697 | let selections = editor.selections.slice(); 698 | let lastSelectionIdx = selections.length-1; 699 | let lastSelection = selections[lastSelectionIdx]; 700 | let newAnchor = positionAtOffset(editor, lastSelection.anchor, argsAnchor); 701 | let newActive = positionAtOffset(editor, lastSelection.active, argsActive); 702 | if (!newAnchor || !newActive) { return; } 703 | selections[lastSelectionIdx] = new vscode.Selection(newAnchor, newActive); 704 | updateEditorSelections(editor, selections); 705 | }; 706 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.moveLastSelection', (editor, edit, args) => { 707 | if (args === undefined) { args = {}; } 708 | updateLastSelection(editor, args, args); 709 | }) ); 710 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.moveLastSelectionActive', (editor, edit, args) => { 711 | if (args === undefined) { args = {}; } 712 | updateLastSelection(editor, {offset:0}, args); 713 | }) ); 714 | const getPropertyAsOffset = (args, propName) => { 715 | let offset = getProperty(args, propName); 716 | return (offset !== undefined) ? { offset } : undefined; 717 | }; 718 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.moveSelections', (editor, edit, args) => { 719 | if (args === undefined) { args = {}; } 720 | let offset0 = {offset:0}; 721 | let offset = getProperty(args, 'offset'); 722 | offset = (offset === undefined) ? offset0 : {offset}; 723 | let start = getPropertyAsOffset(args, 'start'); 724 | let end = getPropertyAsOffset(args, 'end'); 725 | let anchor = getPropertyAsOffset(args, 'anchor'); 726 | let active = getPropertyAsOffset(args, 'active'); 727 | if (start || end) { 728 | if (start === undefined) { start = offset0; } 729 | if (end === undefined) { end = offset0; } 730 | } else if (anchor || active) { 731 | if (anchor === undefined) { anchor = offset0; } 732 | if (active === undefined) { active = offset0; } 733 | } else { 734 | anchor = offset; 735 | active = offset; 736 | } 737 | let selections = editor.selections.map(s => { 738 | if (start) { 739 | return updateSelection(editor, s, s.isReversed ? end : start, s.isReversed ? start : end); 740 | } 741 | return updateSelection(editor, s, anchor, active); 742 | }); 743 | updateEditorSelections(editor, selections); 744 | }) ); 745 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('selectby.addSelectionToNextFindMatchMultiCursor', (editor, edit, args) => { 746 | let document = editor.document; 747 | let text = document.getText(); 748 | let selections = [...editor.selections]; 749 | selections.sort((a, b) => { return a.start.compareTo(b.start); }); 750 | selections = selections.flatMap( (selection, idx, arr) => { 751 | if (selection.isEmpty) { return selection; } 752 | let stopPos = (idx < arr.length-1) ? arr[idx+1].start : document.positionAt(text.length); 753 | let searchText = document.getText(selection); 754 | let nextOccur = text.indexOf(searchText, document.offsetAt(selection.end)); 755 | if (nextOccur === -1) { return selection; } 756 | let nextOccurPos = document.positionAt(nextOccur); 757 | if (nextOccurPos.isAfterOrEqual(stopPos)) { return selection; } 758 | return [selection, new vscode.Selection(nextOccurPos, document.positionAt(nextOccur+searchText.length))] 759 | }); 760 | updateEditorSelections(editor, selections); 761 | }) ); 762 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('moveby.regex', (editor, edit, args) => { movebyRegEx(editor, args);}) ); 763 | context.subscriptions.push(vscode.commands.registerTextEditorCommand('moveby.calculation', (editor, edit, args) => { movebyCalculation(editor, args);}) ); 764 | }; 765 | 766 | function deactivate() {} 767 | 768 | module.exports = { 769 | activate, 770 | deactivate 771 | } 772 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The extension has commands for 8 things: 2 | 3 | * [Select By](#select-by): modify the selections based on Regular Expressions 4 | * [Select By Paste Clipboard](#select-by-paste-clipboard): Replace selection with clipboard content 5 | * [Select By Line Number](#select-by-line-number): Place cursor based on line number, uses boolean expression 6 | * [Select By Remove Cursor](#select-by-remove-cursor): Remove one of the multi cursors 7 | * `selectby.swapActive` : Swap anchor and active (cursor) positions of selection(s) 8 | * `selectby.anchorAndActiveSeparate` : Create separate cursors for anchor and active position of the selection(s) 9 | * [Select By Anchor and Active by Regex](#select-by-anchor-and-active-by-regex): Modify the anchor and active position of the selection(s) 10 | * [Select By Mark](#select-by-mark): Mark position of cursor(s), create selection(s) on next mark, restore cursor locations 11 | * [Select By Multi Cursor with keyboard](#select-by-multi-cursor-with-keyboard): Create and modify Multi Cursors with the keyboard 12 | * [Select By Move Selections](#select-by-move-selections): Move selections start/end/anchor/active a given offset 13 | * [Select By Add Selection To Next Find Match Multi Cursor](#select-by-add-selection-to-next-find-match-multi-cursor): Multi Cursor variant of **Add Selection To Next Find Match** (`Ctrl+D`) 14 | * [Move By](#move-by): move the cursor based on Regular Expressions or a Calculation 15 | 16 | # Select By 17 | 18 | The command is **SelectBy: Select text range based on regex** (`selectby.regex`). 19 | 20 | Select part of the file content surrounding the selection based on Regular Expressions. The current selection is extended or shrunk by searching forward and or backward or forward twice. If there is no current selection the cursor position is used. 21 | 22 | `selectby.regex` supports Multi Cursor. Each selection is processed separately. 23 | 24 | You can specify a "Search Back" expression, a "Search Forward" expression and a "Search Forward Next" expression. If they are not specified that search direction is not performed. 25 | 26 | You can extend a side of the selection: 27 | 28 | * Forward: from the selection end (or cursor) to the next occurrence of a Regular Expression or end of the file 29 | * Backward: from the selection start (or cursor) search back for a Regular Expression or start of the file 30 | * ForwardNext: from the end of the Forward search, search for a different Regular Expression in the Forward direction. You can reuse captured groups from the forward Regular Expression. 31 | * ForwardNextExtendSelection: if the Forward Regular Expression matches at the start of the selection the ForwardNext Regular Expression extends the selection. Otherwise a normal ForwardNext search. 32 | * or combine Forward and Backward 33 | * or combine Forward and ForwardNext 34 | 35 | You can shrink a side of the selection: 36 | 37 | * from the selection start (or cursor) to the next occurrence of the Backward Regular Expression or end of the file. Set `backwardShrink` to true. 38 | * from the selection end (or cursor) to the previous occurrence of the Forward Regular Expression or start of the file. Set `forwardShrink` to true. 39 | 40 | You can shrink one side of the selection and expand the other side of the selection. 41 | 42 | You can specify any number of ranges specified by Regular Expressions that can be linked to keyboard shortcuts. A range can have any name. 43 | 44 | The extension exports 5 commands that use a fixed name: `selectby.regex1` to `selectby.regex5` use the respective range names: `regex1`, `regex2`, `regex3`, `regex4`, `regex5`. 45 | 46 | The ranges are specified in the `settings.json` file for entry `selectby.regexes`. 47 | 48 | * the key for the range can have any name 49 | * the parameters for each range are 50 | * `flags`: a string with the regex flags "i" and/or "m" (default: "") 51 | * `backward`: the regular expression to search from the selection start (or cursor) to the start of the file. If you want to select to the file start, construct a regex that will never be found. If this parameter is not present the selection start is not modified or starts at the cursor position. Or `false` if you want to override User setting. 52 | * `forward`: the regular expression to search for from the selection end (or cursor) to the end of the file. If you want to select to the file end, construct a regex that will never be found. If this parameter is not present the selection end is not modified or ends at the cursor position. Or `false` if you want to override User setting. 53 | * `forwardNext`: the regular expression to search for starting at the end of the **forward** search to the end of the file. Or `false` (to override User setting). [See explanation](#select-by-with-forwardnext). 54 | * `backwardInclude`: should the matched **backward** search text be part of the selection (default: `true`) 55 | * `forwardInclude`: should the matched **forward** search text be part of the selection (default: `true`) 56 | * `backwardShrink`: do we reduce (shrink) at the current selection start. Find next `backward` regular expression relative to selection start (default: `false`) 57 | * `forwardShrink`: do we reduce (shrink) at the current selection end. Find previous `forward` regular expression relative to selection end (default: `false`) 58 | * `backwardAllowCurrentPosition`: is the current selection start an allowed backward position (default: `true`) 59 | * `forwardAllowCurrentPosition`: is the current selection end an allowed forward position (default: `true`) 60 | * `forwardNextInclude`: should the matched **forwardNext** search text be part of the selection (default: `true`) 61 | * `forwardNextExtendSelection`: should we extend the selection with the matched **forwardNext** search text if the begin of the selection matches the **forward** regex (default: `false`). [See explanation](#select-by-with-forwardnextextendselection). 62 | * `surround` : select the text around the current selection that matches the regular expression, the selection is somewhere in the text to select, or false (to override User setting). [See explanation](#select-by-with-surround). 63 | * `copyToClipboard`: copy the selection to the clipboard (default: `false`) 64 | * `showSelection`: modify the selection to include the new searched positions. Useful if `copyToClipboard` is `true`. (default: `true`) 65 | * `debugNotify`: show a notify message of the used search properties (User and Workspace properties are merged) (default: `false`) 66 | * `moveby`: the regular expression to search for. Used only by [Move By](#move-by) 67 | * `label`, `description`, `detail`: when SelectBy is called from the command palette it shows a QuickPick list. These 3 properties (`strings`) are used in the construction of the [QuickPickItem](https://code.visualstudio.com/api/references/vscode-api#QuickPickItem). The default value for `label` is the key name of the range. The label is decorated with additional icons in case the range contains the parameters `copyToClipboard` or `debugNotify`. In the 3 properties you can [use other icons](https://microsoft.github.io/vscode-codicons/dist/codicon.html) with the `$()`-syntax. 68 | 69 | If newline characters are part of the regular expression you can determine if it is part of the selection (see example [`SectionContent`](#an-example)). 70 | 71 | ## An example 72 | 73 | ```json 74 | "selectby.regexes": { 75 | "regex1": { 76 | "flags": "i", 77 | "backward": "%% section", 78 | "forward": "%% section", 79 | "backwardInclude": true, 80 | "forwardInclude": false 81 | }, 82 | "SectionContent": { 83 | "backward": "%% section(\\r?\\n)?", 84 | "forward": "%% section", 85 | "forwardInclude": false, 86 | "backwardInclude": false, 87 | "copyToClipboard": true 88 | } 89 | } 90 | ``` 91 | 92 | ## Select By with keybindings 93 | 94 | It could be handy to have a search for a Regular Expression bound to a keyboard shortcut. 95 | 96 | You create [key bindings in `keybindings.json`](https://code.visualstudio.com/docs/getstarted/keybindings). 97 | 98 | If the definition of the search is found in the setting `selectby.regexes` you can specify the name of the search in an array: 99 | 100 | ```json 101 | { 102 | "key": "ctrl+shift+alt+f9", 103 | "when": "editorTextFocus", 104 | "command": "selectby.regex", 105 | "args": ["SectionContent"] 106 | } 107 | ``` 108 | 109 | You can also define the range of the search in the args property by using an object: 110 | 111 | ```json 112 | { 113 | "key": "ctrl+shift+alt+f9", 114 | "when": "editorTextFocus", 115 | "command": "selectby.regex", 116 | "args": { 117 | "backward": "%% section(\\r?\\n)?", 118 | "forward": "%% section", 119 | "forwardInclude": false, 120 | "backwardInclude": false, 121 | "copyToClipboard": true 122 | } 123 | } 124 | ``` 125 | 126 | If you create a keybinding without an `args` property a QuickPick list, with recently used items, will be shown where you can select a range to use. 127 | 128 | ## Select By with backward/forwardAllowCurrentPosition 129 | 130 | If the current selection start or end is a valid position for the given search regex the selection will not be extended if you have the `backward/forwardInclude` set to `false`. You can change this behavior by setting the property `backwardAllowCurrentPosition` or `forwardAllowCurrentPosition` to `false`. Now the search will be at the next possible position before or after, and the selection is extended. 131 | 132 | ## Select By with forwardNext 133 | 134 | By using the **forward** and **forwardNext** Regular Expressions you can modify the selection from the current selection end, or cursor position, by searching first for the **forward** Regular Expression and from that location search again for a Regular Expression to determine the end of the new selection. 135 | 136 | It does not make sense to specify the **backward** Regular Expression. It has no effect on the result. 137 | 138 | It is possible in the **forwardNext** Regular Expression to use captured groups `()` from the **forward** Regular Expression. It uses a special syntax to fill in the text from the captured groups. Use `{{n}}` to use captured group `n` from the **forward** Regular Expression. To use captured group 1 you use `{{1}}`. 139 | 140 | ### An example: Select the next string content 141 | 142 | In python you can specify 4 types of string literals. 143 | 144 | Put this in your `settings.json` file: 145 | 146 | ```json 147 | "selectby.regexes": { 148 | "stringContent": { 149 | "forward": "('''|\"\"\"|'|\")", 150 | "forwardNext": "{{1}}", 151 | "forwardInclude": false, 152 | "forwardNextInclude": false 153 | } 154 | } 155 | ``` 156 | Define a keybinding: 157 | 158 | ```json 159 | { 160 | "key": "ctrl+shift+alt+f10", 161 | "when": "editorTextFocus", 162 | "command": "selectby.regex", 163 | "args": ["stringContent"] 164 | } 165 | ``` 166 | 167 | ## Select By with forwardNextExtendSelection 168 | 169 | Based on idea by [johnnytemp](https://github.com/rioj7/select-by/pull/10). 170 | 171 | If you set `forwardNextExtendSelection` to `true` the selection is extended with the next occurrence of `forwardNext` Regular Expression if the start of the selection matches the `forward` Regular Expression. 172 | 173 | The `forwardNext` Regular Expression must match at the selection end. If there is not a match at the selection end we start a new `forward` search at the selection end, just like a normal `forward`-`forwardNext`. You can extend the `forwardNext` match to any position by prefixing the Regular Expression with `[\s\S]*?` or `.*?` (non greedy anything), depending if you want to include new lines or not. 174 | 175 | At the moment it only works if `forwardNextInclude` is `true`. 176 | 177 | ### Example 1: Extend with the next item of a tuple 178 | 179 | This example extends the selection with the next tuple element if the selection start is after the tuple open paranthesis `(`. 180 | 181 | If there are no more elements in the tuple after the selection go to the next tuple. 182 | 183 | Put this in your `settings.json` file: 184 | 185 | ```json 186 | "selectby.regexes": { 187 | "extendNextTupleItem": { 188 | "forward": "\\(", 189 | "forwardNext": "[^,)]+(\\s*,\\s*)?", 190 | "forwardInclude": false, 191 | "forwardNextExtendSelection": true, 192 | "label": "Extend next tuple item $(arrow-right)", 193 | "description": "from tuple start" 194 | } 195 | } 196 | ``` 197 | 198 | And define a keybinding. 199 | 200 | If it is not important that the selection starts at the first tuple item and the items are all word characters you can use: 201 | 202 | ```json 203 | "selectby.regexes": { 204 | "extendNextTupleItem2": { 205 | "forward": "(?=\\w+)", 206 | "forwardNext": "\\w+(\\s*,\\s*)?", 207 | "forwardNextExtendSelection": true 208 | } 209 | } 210 | ``` 211 | 212 | The `forward` Regular Expression searches for a location that is followed by a tuple item. It is an empty match. 213 | 214 | ### Example 2: Extend selection always with forwardNext 215 | 216 | If you want to extend the selection always with `forwardNext`, you can set the `forward` Regular Expression to the string `(?=[\s\S])` or `(?=.)`, depending if you want to include new lines or not. 217 | 218 | The examples are to extend the selection with the next part of the sentence. If you have line breaks in the sentence you should use the second alternative. 219 | 220 | ```json 221 | "selectby.regexes": { 222 | "extendWithSentensePart": { 223 | "forward": "(?=.)", 224 | "forwardNext": ".*?[,.]", 225 | "forwardNextExtendSelection": true 226 | } 227 | } 228 | ``` 229 | 230 | or 231 | 232 | ```json 233 | "selectby.regexes": { 234 | "extendWithSentensePart": { 235 | "forward": "(?=[\\s\\S])", 236 | "forwardNext": "[\\s\\S]*?[,.]", 237 | "forwardNextExtendSelection": true 238 | } 239 | } 240 | ``` 241 | 242 | But this could already be done with this setting: 243 | 244 | ```json 245 | "selectby.regexes": { 246 | "extendWithSentensePart": { 247 | "forward": "[\\s\\S]*?[,.]" 248 | } 249 | } 250 | ``` 251 | 252 | ## Select By with Surround 253 | 254 | If the cursor or selection is inside of the text you want to select and can be described with a single regular expression you use the `surround` Regular Expression. 255 | 256 | If you place the cursor somewhere inside a floating point number and you want to select the number you can use the following setting: 257 | 258 | ```json 259 | "selectby.regexes": { 260 | "selectFloat": { 261 | "surround": "[-+]?\\d+(\\.\\d+)?([eE][-+]?\\d+)?[fF]?" 262 | } 263 | } 264 | ``` 265 | 266 | For fast access you can [create a keybinding](#select-by-with-keybindings) for this just like the `Ctrl+D` for select word. 267 | 268 | ## User and Workspace settings 269 | The Workspace/folder setting does override the global User setting. The settings are deep-merged. 270 | 271 | If you have defined this User setting: 272 | 273 | ```json 274 | "selectby.regexes": { 275 | "regex1": { 276 | "flags": "i", 277 | "backward": "%% article", 278 | "forward": "%% article", 279 | "backwardInclude": true, 280 | "forwardInclude": false 281 | } 282 | } 283 | ``` 284 | 285 | And this Workspace setting: 286 | 287 | ```json 288 | "selectby.regexes": { 289 | "regex1": { 290 | "flags": "i", 291 | "forward": "%% section", 292 | "forwardInclude": false 293 | } 294 | } 295 | ``` 296 | 297 | There will be still a search done backward for: `%% article`. The extension does not know which file has defined a particular setting. You have to disable the backward search in the Workspace setting: 298 | 299 | ```json 300 | "selectby.regexes": { 301 | "regex1": { 302 | "flags": "i", 303 | "forward": "%% section", 304 | "forwardInclude": false, 305 | "backward": false 306 | } 307 | } 308 | ``` 309 | 310 | # Select By Paste Clipboard 311 | 312 | If you paste the clipboard content with Ctrl+V you loose the selection. 313 | 314 | The command **Paste clipboard and select** (`selectby.pasteClipboard`) replaces the current selection with the content of the clipboard and keep it selected. 315 | 316 | If you need it regularly a keybinding can be handy 317 | 318 | ```json 319 | { 320 | "key": "ctrl+k ctrl+v", 321 | "when": "editorTextFocus", 322 | "command": "selectby.pasteClipboard" 323 | } 324 | ``` 325 | 326 | It only works for single selection. If you use a copy with multi cursor selections the content of the clipboard does not show where each selection begins. There are extra empty lines added but they could also be part of a selection. 327 | 328 | # Select By Line Number 329 | 330 | If you want to place a cursor on each line where the line number matches multiple boolean expressions you can use the command **Place cursor based on line number, uses boolean expression** (`selectby.lineNr`). 331 | 332 | The boolean expression uses the following variables: 333 | 334 | * `c` : contains the line number of the cursor or the start of the first selection. When using [`inselection`](#inselection) it means the start of the selection. 335 | * `n` : contains the line number of the line under test, each line of the current document is tested 336 | * `k` : is a placeholder to signify a modulo placement. Can only be used in an expression like `c + 6 k`. Meaning every line that is a multiple (k ∈ ℕ) of 6 from the current line. Every expression of the form `c + 6 k` is transformed to ((n-c)%number==0 && n>=c). 337 | 338 | The input box for the lineNr expression remembers the last entered lineNr expression for this session. 339 | 340 | The command can be used in a keybinding: 341 | 342 | ```json 343 | { 344 | "key": "ctrl+k ctrl+k", 345 | "when": "editorTextFocus", 346 | "command": "selectby.lineNr", 347 | "args": { "lineNrEx": "c+5k && n-c<100" } 348 | } 349 | ``` 350 | 351 | This selects every 5th line for the next 100 lines. 352 | 353 | ## Place multiple cursors per block or relative to block start 354 | 355 | The expression `c + 6 k` places a cursor at the start of a modulo _block_. Maybe you want to place cursors at lines 1, 3 and 4 relative to the block start. You can use an expression like: 356 | 357 | ``` 358 | n>=c && ( (n-c)%6==1 || (n-c)%6==3 || (n-c)%6==4 ) 359 | ``` 360 | 361 | If you want to place cursors at the first 3 lines of a block use: 362 | 363 | ``` 364 | n>=c && (n-c)%6<3 365 | ``` 366 | 367 | This can also be achieved with `c+6k` followed by **Selection** | **Add Cursor Below** 2 times 368 | 369 | ## `inselection` 370 | 371 | Feature request by [blueray](https://stackoverflow.com/questions/69263442/how-to-put-cursor-on-every-other-line-on-alternate-lines) 372 | 373 | If you want every selection to be treated separately or you want the command to figure out the end line test (`&& n<=100`) you can add `&& inselection`. The text `inselection` is transformed to ((n>=startLineNr) && (n<=endLineNr)). Where startLineNr and endLineNr are from each selection. If the end of a selection is at the start of a line that line is not considered to be part of the selection. 374 | 375 | This is also useful to add to a keybinding, now the end line test depends on the selected text. 376 | 377 | ```json 378 | { 379 | "key": "ctrl+k ctrl+k", 380 | "when": "editorTextFocus", 381 | "command": "selectby.lineNr", 382 | "args": { "lineNrEx": "c+5k && inselection" } 383 | } 384 | ``` 385 | 386 | # Select By Remove Cursor 387 | 388 | If you have a Multi Cursor you can't remove a cursor with **Cursor Undo** (`cursorUndo`) when you have done some edit action. 389 | 390 | The following commands remove a cursor/selection: 391 | 392 | * `selectby.removeCursorBelow` : remove the last cursor/selection 393 | * `selectby.removeCursorAbove` : remove the first cursor/selection 394 | 395 | A suggestion for keybinding: 396 | 397 | ```json 398 | { 399 | "key": "ctrl+alt+/", 400 | "command": "selectby.removeCursorAbove", 401 | "when": "editorTextFocus" 402 | }, 403 | { 404 | "key": "ctrl+alt+'", 405 | "command": "selectby.removeCursorBelow", 406 | "when": "editorTextFocus" 407 | } 408 | ``` 409 | 410 | # Select By Mark 411 | 412 | ## selectby.mark 413 | 414 | The command is `selectby.mark`. 415 | 416 | The `"args"` argument of the command can have the following properties: 417 | 418 | * `first`: boolean, when `true` this command will behave as if it is the first call (remembers the start positions), usefull on command scripts like multi-command 419 | 420 | The first time you call the command it remembers the start positions of the current selections. 421 | 422 | The second call of the command creates selections from the marked (stored) positions to the active positions of the current selections. If the number of cursors differ it shows a warning. 423 | 424 | You can create a key binding for this command or call it from the Command Palette. 425 | 426 | Currently they are not bound to the particular editor/file so you can use cursor positions from one file (first mark) in another file (second mark) 427 | 428 | You can use a second mark to view the selections up to now. Follow it by an immediate first mark to remember the selection starts if you want to continue to modify the cursor positions. 429 | 430 | You can combine it with the `moveby.regex` command of this extension to move the cursors by a search for a regular expression. 431 | 432 | The marked positions are decorated with a ◆ character using the `editor.selectionBackground` color. 433 | 434 | ## selectby.mark-restore 435 | 436 | The command is `selectby.mark-restore`. 437 | 438 | The `"args"` argument of the command can have the following properties: 439 | 440 | * `keepMarks`: boolean, when `true` the marks are not removed (default: `false`) 441 | 442 | This command restores the cursor positions to the mark locations. It will clear the mark positions unless you set the argument `keepMarks` to true. 443 | 444 | # Select By Anchor and Active by Regex 445 | 446 | The command `selectby.regex` modifies the `start` and `end` of the selection. Standard Expand/Shrink Selection (Ctrl+Shift+Arrow) modifies the `active` position (cursor) based on the word definition of the language. With the command `selectby.anchorAndActiveByRegex` you can modify the `anchor` and `active` position of the selection(s). 447 | 448 | At the moment the command `selectby.anchorAndActiveByRegex` accepts all arguments in an object that is part of the key binding or command call in cases like the extension [multi-command](https://marketplace.visualstudio.com/items?itemName=ryuta46.multi-command). 449 | 450 | The argument of the command is an object with the following properties: 451 | 452 | * `regex` : global definition of the regular expression to search for 453 | * `flags` : global definition of the flags used by the regular expression (`g` is added by the extension) (default: `""` [no flags]) 454 | * `direction` : global definition of the direction to search. (default: `next`) 455 | Possible values: 456 | * `next` : determine the first position of the regex towards the **end** of the file. 457 | the new position will be the _end_ of the text matched by the regex 458 | * `prev` : determine the first position of the regex towards the **begin** of the file. 459 | the new position will be the _start_ of the text matched by the regex 460 | * `repeat` : global definition of how often to search for (default: `1`) 461 | * `anchor` : an object, can be empty. Modify the `anchor` position of the selection based on the properties: 462 | * `regex` : replace global definition if present 463 | * `flags` : replace global definition if present 464 | * `direction` : replace global definition if present 465 | * `repeat` : replace global definition if present 466 | * `active` : an object, can be empty. Modify the `active` position of the selection based on the properties: 467 | * `regex` : replace global definition if present 468 | * `flags` : replace global definition if present 469 | * `direction` : replace global definition if present 470 | * `repeat` : replace global definition if present 471 | 472 | If `anchor` or `active` is not present then that position will not change. 473 | 474 | This command supports Multi Cursor (multiple selections). 475 | 476 | ## Example 477 | 478 | Modify the `active` position (cursor) to the next/prev double character that is not a space or tab 479 | 480 | By adding an extra modifier key (`alt`) you can make a bigger jump by setting a `repeat`. 481 | 482 | ```json 483 | { 484 | "key": "ctrl+shift+right", 485 | "command": "selectby.anchorAndActiveByRegex", 486 | "when": "editorTextFocus", 487 | "args": { 488 | "active": { "regex": "([^ \\t])\\1", "direction": "next" } 489 | } 490 | }, 491 | { 492 | "key": "ctrl+shift+left", 493 | "command": "selectby.anchorAndActiveByRegex", 494 | "when": "editorTextFocus", 495 | "args": { 496 | "active": { "regex": "([^ \\t])\\1", "direction": "prev" } 497 | } 498 | }, 499 | { 500 | "key": "ctrl+shift+alt+right", 501 | "command": "selectby.anchorAndActiveByRegex", 502 | "when": "editorTextFocus", 503 | "args": { 504 | "active": { "regex": "([^ \\t])\\1", "direction": "next", "repeat": 5 } 505 | } 506 | }, 507 | { 508 | "key": "ctrl+shift+alt+left", 509 | "command": "selectby.anchorAndActiveByRegex", 510 | "when": "editorTextFocus", 511 | "args": { 512 | "active": { "regex": "([^ \\t])\\1", "direction": "prev", "repeat": 5 } 513 | } 514 | } 515 | ``` 516 | 517 | # Select By Multi Cursor with keyboard 518 | 519 | You can create and modify Multi Cursors with the keyboard with the commands: 520 | 521 | * `selectby.addNewSelection` : Add a new selection at an offset (default: 1) 522 | * `selectby.moveLastSelectionActive` : Modify (extend/reduce) the last selection by moving the Active position `offset` characters left/right (default: 1) 523 | * `selectby.moveLastSelection` : Move the last selection number of characters left/right (default: 1) 524 | 525 | All 3 commands have 1 property, set in the `args` property of the key binding. If called from the Command Palette the value of `offset` is `1`. 526 | 527 | You can define a set of key bindings to use these commands. 528 | 529 | By using a custom context variable (extension [Extra Context](https://marketplace.visualstudio.com/items?itemName=rioj7.extra-context)) to set a mode, and use the `when` clause to determine if we use the default key binding for the arrow keys or our custom key bindings. 530 | 531 | With the command `extra-context.toggleVariable` you can toggle the variable and thus the mode. 532 | 533 | ```json 534 | { 535 | "key": "alt+F5", // or some other key combo 536 | "when": "editorTextFocus", 537 | "command": "extra-context.toggleVariable", 538 | "args": {"name": "multiCursorByKeyboard"} 539 | }, 540 | { 541 | "key": "ctrl+alt+right", 542 | "when": "editorTextFocus && extraContext:multiCursorByKeyboard", 543 | "command": "selectby.addNewSelection", 544 | "args": {"offset": 1} 545 | }, 546 | { 547 | "key": "ctrl+alt+left", 548 | "when": "editorTextFocus && extraContext:multiCursorByKeyboard", 549 | "command": "selectby.removeCursorBelow" 550 | }, 551 | { 552 | "key": "shift+right", 553 | "when": "editorTextFocus && extraContext:multiCursorByKeyboard", 554 | "command": "selectby.moveLastSelectionActive", 555 | "args": {"offset": 1} 556 | }, 557 | { 558 | "key": "shift+left", 559 | "when": "editorTextFocus && extraContext:multiCursorByKeyboard", 560 | "command": "selectby.moveLastSelectionActive", 561 | "args": {"offset": -1} 562 | }, 563 | { 564 | "key": "right", 565 | "when": "editorTextFocus && extraContext:multiCursorByKeyboard", 566 | "command": "selectby.moveLastSelection", 567 | "args": {"offset": 1} 568 | }, 569 | { 570 | "key": "left", 571 | "when": "editorTextFocus && extraContext:multiCursorByKeyboard", 572 | "command": "selectby.moveLastSelection", 573 | "args": {"offset": -1} 574 | } 575 | ``` 576 | 577 | # Select By Move Selections 578 | 579 | Sometimes the selection commands select a few characters too much or too little. 580 | 581 | With the command `selectby.moveSelections` you can adjust the selection ends a few characters. 582 | 583 | The _number_ values of the properties can be positive, negative or zero. 584 | 585 | The `args` property of the command has the following properties: 586 | 587 | * `offset` : _number_, move both **start** and **end** _number_ characters 588 | * `start` : _number_, move **start** _number_ characters 589 | * `end` : _number_, move **end** _number_ characters 590 | * `anchor` : _number_, move **anchor** _number_ characters 591 | * `active` : _number_, move **active** _number_ characters 592 | 593 | `active` side of the selection is the side where the cursor is. 594 | 595 | Which properties are used is determined by: 596 | 597 | 1. `start` or `end` defined. Use `start` and `end`. The property not defined has a value of `0` 598 | 1. `anchor` or `active` defined. Use `anchor` and `active`. The property not defined has a value of `0` 599 | 1. use `offset` 600 | 601 | Example: 602 | 603 | Reduce the selections 1 character at the start and end. 604 | 605 | ```json 606 | { 607 | "key": "ctrl+i r", 608 | "when": "editorTextFocus", 609 | "command": "selectby.moveSelections", 610 | "args": {"start": 1, "end": -1} 611 | } 612 | ``` 613 | 614 | It can also be combined to modify a selection command using the extension [multi-command](https://marketplace.visualstudio.com/items?itemName=ryuta46.multi-command). 615 | 616 | If you don't want the brackets to be selected when using the **Select to Bracket** command: 617 | 618 | ```json 619 | { 620 | "key": "ctrl+i ctrl+b", // or any other combo 621 | "command": "extension.multiCommand.execute", 622 | "args": { 623 | "sequence": [ 624 | "editor.action.selectToBracket", 625 | { "command": "selectby.moveSelections", "args": {"start": 1, "end": -1} } 626 | ] 627 | } 628 | } 629 | ``` 630 | 631 | # Select By Add Selection To Next Find Match Multi Cursor 632 | 633 | Multi Cursor variant of **Add Selection To Next Find Match** (`Ctrl+D`). 634 | 635 | The commandID is: `selectby.addSelectionToNextFindMatchMultiCursor` 636 | 637 | Consider each selection separate. Find the **next** occurrence of the same text. If this happens **before** the next selection add this **next** occurrence to the selections. 638 | 639 | # Move By 640 | 641 | You can move the cursor based on [Regular Expressions](#move-by-regular-expression) or using a [Calculation](#move-by-calculation). 642 | 643 | ## Move By Regular Expression 644 | 645 | The exported command is: `moveby.regex` (**MoveBy: Move cursor based on regex**) 646 | 647 | To use fixed Regular Expressions or different properties for MoveBy you can create: 648 | 649 | * 1 or more [key bindings in `keybindings.json`](https://code.visualstudio.com/docs/getstarted/keybindings). In the `args` property of the key binding you define the regex and properties. 650 | * define named regex and properties in the setting `moveby.regexes` and select one from a QuickPick list. 651 | 652 | The argument of the command can be: 653 | 654 | * `undefined` : when called from Command Palette or key binding without argument 655 | You are presented with a QuickPick list of arguments defined in the setting `moveby.regexes`. 656 | If `moveby.regexes` is empty it behaves as if the argument of the key binding or command was: 657 | ```json 658 | "args": { 659 | "ask": true, 660 | "properties": ["next", "end", "nowrap"] 661 | } 662 | ``` 663 | * a **string** : this is used as the key in the setting `moveby.regexes` to get an array or object. 664 | * an **array** : [key binding with an `"args"` property that is an Array](#args-of-keybinding-is-an-array) 665 | * an **object** : [key binding with an `"args"` property that is an Object](#args-of-keybinding-is-an-object) 666 | 667 | In the setting `moveby.regexes` you can define frequently used regex searches that you select from a QuickPick list. These searches are named (_`key`_). The name can also be used as string argument in a key binding or multi-command sequence. 668 | 669 | The setting `moveby.regexes` is an object with key-value pairs. The value can be an [array](#args-of-keybinding-is-an-array) or an [object](#args-of-keybinding-is-an-object). 670 | 671 | * the key for the search arguments can have any name 672 | * if the value is an object a few extra properties can be used 673 | * `debugNotify`: show a notify message of the used search properties (User and Workspace properties are merged) (default: `false`) 674 | * `label`, `description`, `detail`: (Optional) when MoveBy is called from the command palette it shows a QuickPick list. These 3 properties (`strings`) are used in the construction of the [QuickPickItem](https://code.visualstudio.com/api/references/vscode-api#QuickPickItem). The default value for `label` is the key name. The label is decorated with an additional icon in case the object contains the parameter `debugNotify`. In the 3 properties you can [use other icons](https://microsoft.github.io/vscode-codicons/dist/codicon.html) with the $(name)-syntax. 675 | 676 | An example for the setting `moveby.regexes`: 677 | 678 | ```json 679 | "moveby.regexes": { 680 | "Go to Last Dot": { 681 | "regex": "\\.(?!.*\\.)", 682 | "properties": ["next", "start"] 683 | }, 684 | "--Ask-- $(regex) next end": { 685 | "ask": true, 686 | "properties": ["next", "end"] 687 | }, 688 | "--Ask-- $(regex) next start": { 689 | "ask": true, 690 | "properties": ["next", "start"] 691 | } 692 | } 693 | ``` 694 | 695 | You can use [icons](https://microsoft.github.io/vscode-codicons/dist/codicon.html) in the _key_ of the setting `moveby.regexes`. 696 | 697 | If you define setting `moveby.regexes` and you want the **Ask** regex functionality if called from the Command Palette you have to add a definition for this in the setting `moveby.regexes`. 698 | 699 | The details of the search are specified in the `"args"` property of the key binding. The `"args"` property can be an Array or an Object. 700 | 701 | ## args of keybinding is an Array 702 | 703 | If the `"args"` property of the key binding is an Array the meaning of the 5 strings are: 704 | 705 | * index 0: the key/name of the range you want to use as defined in the settings option `selectby.regexes` 706 | * index 1: `"forward"` | `"backward"` | `"moveby"` | `"forwardNext"` - which regex string should be used 707 | * index 2: `"prev"` | `"next"` - search direction - do you want to search for the **previous** or **next** occurrence of the Regular Expression (default: `"next"`) 708 | * index 3: `"start"` | `"end"` - should the cursor move to the **start** or the **end** of the found Regular Expression (default: `"end"`) 709 | * index 4: `"wrap"` | `"nowrap"` - optional argument: do we wrap to other side of the file and continue search if not found (default: `"nowrap"`) (proposed by [Arturo Dent](https://github.com/rioj7/select-by/issues/8)) 710 | 711 | If the last element of the array is a default value you can omit that argument. You can apply this rule multiple times. But naming the first 4 arguments helps in the readability of the keybinding. 712 | 713 | To use regular expressions that are not used in selections you can use the `"moveby"` property of the `selectby.regexes` elements or you can duplicate the `"forward"` or `"backward"` field. This property is just added to prevent confusion in the specification of `"args"` (`"forward"` does not mean to search in the forward direction) 714 | 715 | ## args of keybinding is an Object 716 | 717 | If the `"args"` property of the key binding is an Object it can have the following properties: 718 | 719 | * `flags`: a string with the regex flags "`i`" and/or "`m`" (default: "") 720 | * `regex`: the regular expression to use, (default: ask for regular expression with InputBox) 721 | * `ask`: a boolean to signal that the regex should be asked from the user, it is optional because if the `regex` property is missing it will be asked.
Just to remind you later which regex is used. 722 | * `properties`: an Array of strings with the values corresponding to Array indexes (2,3,4) from the section: [args of keybinding is an Array](#args-of-keybinding-is-an-array)
The order of the properties is not important. (default: `["next", "end", "nowrap"]`) 723 | * `repeat`: how many times to repeat the `moveby`. An integer or a string of an integer. If value is `"ask"` the user needs to enter a number. Choose the correct `"end"` or `"start"` or it will only happen once (default: `1`) 724 | * `checkCurrent`: check if current cursor position is a possible match (default: `false`) 725 | 726 | If you want to move the cursor(s) to the first character inside the next Python string you can use: 727 | 728 | ```json 729 | { 730 | "key": "ctrl+f6", // or any other key combo 731 | "when": "editorTextFocus", 732 | "command": "moveby.regex", 733 | "args": { 734 | "regex": "('''|\"\"\"|'|\")", 735 | "properties": ["next", "end"] 736 | } 737 | } 738 | ``` 739 | 740 | If you want to move the cursor(s) to the start of the next regex asked from the user you can use: 741 | 742 | ```json 743 | { 744 | "key": "ctrl+shift+f6", // or any other key combo 745 | "when": "editorTextFocus", 746 | "command": "moveby.regex", 747 | "args": { 748 | "ask": true, 749 | "properties": ["next", "start"] 750 | } 751 | } 752 | ``` 753 | 754 | If you want to move _n_, ask user how often, `` tags forward use: 755 | 756 | ```json 757 | { 758 | "key": "alt+f6", // or any other key combo 759 | "when": "editorTextFocus", 760 | "command": "moveby.regex", 761 | "args": { 762 | "regex": "]*>", 763 | "properties": ["next", "end"], 764 | "repeat": "ask" 765 | } 766 | } 767 | ``` 768 | 769 | if you want to insert a snippet at the end of an HTML open tag 770 | 771 | ```json 772 | { 773 | "key": "alt+f7", // or any other key combo 774 | "when": "editorTextFocus", 775 | "command": "extension.multiCommand.execute", 776 | "args": { 777 | "sequence": [ 778 | { "command": "moveby.regex", 779 | "args": { "regex": ">", "properties": ["next", "start"]}, "checkCurrent": true }, 780 | { "command": "editor.action.insertSnippet", 781 | "args": { "snippet": " class=\"$1\"$0" } } 782 | ] 783 | } 784 | } 785 | ``` 786 | 787 | In a next version it will use a selection list with recently used entries on top. The default QuickPick list does not allow to enter a new item. And a list of starting Regular Expressions. 788 | 789 | ## Move By Calculation 790 | 791 | The exported command is: `moveby.calculation` 792 | 793 | **All positions (line and char) are 0 based.** The first line has number 0. 794 | 795 | Move By uses 2 calculation expressions defined in the `"args"` property of the key binding. 796 | 797 | * `lineNrEx` : calculate the new line number of the selection, default value is `"selection.start.line"` (the line number of the start of the selection) 798 | * `charNrEx` : calculate the new character position of the selection 799 | 800 | The expressions can be any JavaScript expression that results in a number. The number is converted to an int with [`Math.floor`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor) 801 | 802 | The expressions can use a number of variables: 803 | 804 | * `selection.start.line` 805 | * `selection.start.character` 806 | * `selection.end.line` 807 | * `selection.end.character` 808 | * `selections` : the full array of all current selections/cursors. You can use the `start` and `end` property of a particular selection. To get the last selection use `selections[selections.length-1]` 809 | * `currentLine` : a string with the text of the line where the selection starts 810 | * `currentLine.length` : the length of `currentLine` variable 811 | * `offset.line` : Ask the user for an offset (relative to start of file) and calculate line and character position 812 | * `offset.character` 813 | * `relative` : Ask the user for a number (positive or negative) to be used to calculate a relative line or character position 814 | 815 | If you want to move the cursor to the midpoint of the line the cursor is on you can use 816 | 817 | ```json 818 | { 819 | "key": "ctrl+i ctrl+m", // or any other key binding 820 | "when": "editorTextFocus", 821 | "command": "moveby.calculation", 822 | "args": { 823 | "charNrEx": "currentLine.length / 2" 824 | } 825 | } 826 | ``` 827 | 828 | If something reports a problem at a character offset (relative to start of file) you can use: 829 | 830 | ```json 831 | { 832 | "key": "ctrl+i ctrl+f", // or any other key binding 833 | "when": "editorTextFocus", 834 | "command": "moveby.calculation", 835 | "args": { 836 | "lineNrEx": "offset.line", 837 | "charNrEx": "offset.character" 838 | } 839 | } 840 | ``` 841 | 842 | If you want a **Go To Line** but enter a relative line number use: 843 | 844 | ```json 845 | { 846 | "key": "ctrl+alt+g", // or any other key binding 847 | "when": "editorTextFocus", 848 | "command": "moveby.calculation", 849 | "args": { 850 | "lineNrEx": "selection.start.line+relative", 851 | "charNrEx": "selection.start.character" 852 | } 853 | } 854 | ``` 855 | 856 | ## `moveby` and Multi Cursor 857 | 858 | `moveby.regex` and `moveby.calculation` support multi cursor. For each cursor the search is performed or the cursor is moved to the new location. 859 | 860 | If the Regular Expression is not found for a particular selection/cursor the behavior depends on the number of cursors that have found a new location: 861 | 862 | * if none have found a new location nothing happens and selections/cursors do not change 863 | * if one or more cursors have found a new location this selection/cursor is removed, so you are sure the cursors remaining are at valid locations, they can be at previous locations of another cursor 864 | 865 | If more than one cursor end at the same location they will be collapsed into one. 866 | 867 | ## Reveal the new cursor locations 868 | 869 | With the setting `"moveby.revealType"` you can change the behavior of how the cursor should be revealed after the move. In the Settings UI, group **Extensions** | **Select By**, it is a dropdown box with possible values. These strings are identical to the VSC API enum [TextEditorRevealType](https://code.visualstudio.com/api/references/vscode-api#TextEditorRevealType). If there are multiple cursors the first cursor is used. 870 | 871 | ## Move By and `keybindings.json` 872 | 873 | You can create a key binding with the UI of VSC but you have to add the `"args"` property by modifying `keybindings.json`. If you do not define the `"args"` property it behaves as called from the Command Palette. 874 | 875 | An example key binding: 876 | 877 | ```json 878 | { 879 | "key": "ctrl+shift+alt+s", 880 | "when": "editorTextFocus", 881 | "command": "moveby.regex", 882 | "args": ["SectionContent", "forward", "prev", "start"] 883 | } 884 | ``` 885 | 886 | ## Move to previous and next empty line 887 | 888 | Created by [Arturo Dent](https://github.com/rioj7/select-by/issues/7) and it needed a small code change to work because the regex matches an empty string. 889 | 890 | Add the following to `selectby.regexes` 891 | 892 | ```json 893 | "goToEmptyLine": { 894 | "flags": "m", 895 | "moveby": "^$" 896 | } 897 | ``` 898 | 899 | Define 2 key bindings (you can change the assigned keys) 900 | ```json 901 | { 902 | "key": "ctrl+shift+f7", 903 | "when": "editorTextFocus", 904 | "command": "moveby.regex", 905 | "args": ["goToEmptyLine", "moveby", "prev", "start"] 906 | }, 907 | { 908 | "key": "ctrl+shift+f8", 909 | "when": "editorTextFocus", 910 | "command": "moveby.regex", 911 | "args": ["goToEmptyLine", "moveby", "next", "start"] 912 | } 913 | ``` 914 | --------------------------------------------------------------------------------