rioj7 / select-by

Visual Studio Code extension to select text by criteria like Regular Expressions
19 stars 4 forks source link

The extension has commands for 8 things:

Select By

The command is SelectBy: Select text range based on regex (selectby.regex).

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.

selectby.regex supports Multi Cursor. Each selection is processed separately.

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.

You can extend a side of the selection:

You can shrink a side of the selection:

You can shrink one side of the selection and expand the other side of the selection.

You can specify any number of ranges specified by Regular Expressions that can be linked to keyboard shortcuts. A range can have any name.

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.

The ranges are specified in the settings.json file for entry selectby.regexes.

If newline characters are part of the regular expression you can determine if it is part of the selection (see example SectionContent).

An example

    "selectby.regexes": {
      "regex1": {
        "flags": "i",
        "backward": "%% section",
        "forward": "%% section",
        "backwardInclude": true,
        "forwardInclude": false
      },
      "SectionContent": {
        "backward": "%% section(\\r?\\n)?",
        "forward": "%% section",
        "forwardInclude": false,
        "backwardInclude": false,
        "copyToClipboard": true
      }
    }

Select By with keybindings

It could be handy to have a search for a Regular Expression bound to a keyboard shortcut.

You create key bindings in keybindings.json.

If the definition of the search is found in the setting selectby.regexes you can specify the name of the search in an array:

  {
    "key": "ctrl+shift+alt+f9",
    "when": "editorTextFocus",
    "command": "selectby.regex",
    "args": ["SectionContent"]
  }

You can also define the range of the search in the args property by using an object:

  {
    "key": "ctrl+shift+alt+f9",
    "when": "editorTextFocus",
    "command": "selectby.regex",
    "args": {
      "backward": "%% section(\\r?\\n)?",
      "forward": "%% section",
      "forwardInclude": false,
      "backwardInclude": false,
      "copyToClipboard": true
    }
  }

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.

Select By with backward/forwardAllowCurrentPosition

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.

Select By with forwardNext

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.

It does not make sense to specify the backward Regular Expression. It has no effect on the result.

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}}.

An example: Select the next string content

In python you can specify 4 types of string literals.

Put this in your settings.json file:

    "selectby.regexes": {
      "stringContent": {
        "forward": "('''|\"\"\"|'|\")",
        "forwardNext": "{{1}}",
        "forwardInclude": false,
        "forwardNextInclude": false
      }
    }

Define a keybinding:

  {
    "key": "ctrl+shift+alt+f10",
    "when": "editorTextFocus",
    "command": "selectby.regex",
    "args": ["stringContent"]
  }

Select By with forwardNextExtendSelection

Based on idea by johnnytemp.

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.

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.

At the moment it only works if forwardNextInclude is true.

Example 1: Extend with the next item of a tuple

This example extends the selection with the next tuple element if the selection start is after the tuple open paranthesis (.

If there are no more elements in the tuple after the selection go to the next tuple.

Put this in your settings.json file:

    "selectby.regexes": {
      "extendNextTupleItem": {
        "forward": "\\(",
        "forwardNext": "[^,)]+(\\s*,\\s*)?",
        "forwardInclude": false,
        "forwardNextExtendSelection": true,
        "label": "Extend next tuple item $(arrow-right)",
        "description": "from tuple start"
      }
    }

And define a keybinding.

If it is not important that the selection starts at the first tuple item and the items are all word characters you can use:

    "selectby.regexes": {
      "extendNextTupleItem2": {
        "forward": "(?=\\w+)",
        "forwardNext": "\\w+(\\s*,\\s*)?",
        "forwardNextExtendSelection": true
      }
    }

The forward Regular Expression searches for a location that is followed by a tuple item. It is an empty match.

Example 2: Extend selection always with forwardNext

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.

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.

    "selectby.regexes": {
      "extendWithSentensePart": {
        "forward": "(?=.)",
        "forwardNext": ".*?[,.]",
        "forwardNextExtendSelection": true
      }
    }

or

    "selectby.regexes": {
      "extendWithSentensePart": {
        "forward": "(?=[\\s\\S])",
        "forwardNext": "[\\s\\S]*?[,.]",
        "forwardNextExtendSelection": true
      }
    }

But this could already be done with this setting:

    "selectby.regexes": {
      "extendWithSentensePart": {
        "forward": "[\\s\\S]*?[,.]"
      }
    }

Select By with Surround

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.

If you place the cursor somewhere inside a floating point number and you want to select the number you can use the following setting:

    "selectby.regexes": {
      "selectFloat": {
        "surround": "[-+]?\\d+(\\.\\d+)?([eE][-+]?\\d+)?[fF]?"
      }
    }

For fast access you can create a keybinding for this just like the Ctrl+D for select word.

User and Workspace settings

The Workspace/folder setting does override the global User setting. The settings are deep-merged.

If you have defined this User setting:

    "selectby.regexes": {
      "regex1": {
        "flags": "i",
        "backward": "%% article",
        "forward": "%% article",
        "backwardInclude": true,
        "forwardInclude": false
      }
    }

And this Workspace setting:

    "selectby.regexes": {
      "regex1": {
        "flags": "i",
        "forward": "%% section",
        "forwardInclude": false
      }
    }

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:

    "selectby.regexes": {
      "regex1": {
        "flags": "i",
        "forward": "%% section",
        "forwardInclude": false,
        "backward": false
      }
    }

Select By Paste Clipboard

If you paste the clipboard content with Ctrl+V you loose the selection.

The command Paste clipboard and select (selectby.pasteClipboard) replaces the current selection with the content of the clipboard and keep it selected.

If you need it regularly a keybinding can be handy

  {
    "key": "ctrl+k ctrl+v",
    "when": "editorTextFocus",
    "command": "selectby.pasteClipboard"
  }

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.

Select By Line Number

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).

The boolean expression uses the following variables:

The input box for the lineNr expression remembers the last entered lineNr expression for this session.

The command can be used in a keybinding:

  {
    "key": "ctrl+k ctrl+k",
    "when": "editorTextFocus",
    "command": "selectby.lineNr",
    "args": { "lineNrEx": "c+5k && n-c<100" }
  }

This selects every 5th line for the next 100 lines.

Place multiple cursors per block or relative to block start

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:

n>=c && ( (n-c)%6==1 || (n-c)%6==3 || (n-c)%6==4 )

If you want to place cursors at the first 3 lines of a block use:

n>=c && (n-c)%6<3

This can also be achieved with c+6k followed by Selection | Add Cursor Below 2 times

inselection

Feature request by blueray

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.

This is also useful to add to a keybinding, now the end line test depends on the selected text.

  {
    "key": "ctrl+k ctrl+k",
    "when": "editorTextFocus",
    "command": "selectby.lineNr",
    "args": { "lineNrEx": "c+5k && inselection" }
  }

Select By Remove Cursor

If you have a Multi Cursor you can't remove a cursor with Cursor Undo (cursorUndo) when you have done some edit action.

The following commands remove a cursor/selection:

A suggestion for keybinding:

  {
    "key": "ctrl+alt+/",
    "command": "selectby.removeCursorAbove",
    "when": "editorTextFocus"
  },
  {
    "key": "ctrl+alt+'",
    "command": "selectby.removeCursorBelow",
    "when": "editorTextFocus"
  }

Select By Mark

selectby.mark

The command is selectby.mark.

The "args" argument of the command can have the following properties:

The first time you call the command it remembers the start positions of the current selections.

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.

You can create a key binding for this command or call it from the Command Palette.

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)

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.

You can combine it with the moveby.regex command of this extension to move the cursors by a search for a regular expression.

The marked positions are decorated with a ◆ character using the editor.selectionBackground color.

selectby.mark-restore

The command is selectby.mark-restore.

The "args" argument of the command can have the following properties:

This command restores the cursor positions to the mark locations. It will clear the mark positions unless you set the argument keepMarks to true.

Select By Anchor and Active by Regex

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).

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.

The argument of the command is an object with the following properties:

If anchor or active is not present then that position will not change.

This command supports Multi Cursor (multiple selections).

Example

Modify the active position (cursor) to the next/prev double character that is not a space or tab

By adding an extra modifier key (alt) you can make a bigger jump by setting a repeat.

  {
    "key": "ctrl+shift+right",
    "command": "selectby.anchorAndActiveByRegex",
    "when": "editorTextFocus",
    "args": {
      "active": { "regex": "([^ \\t])\\1", "direction": "next" }
    }
  },
  {
    "key": "ctrl+shift+left",
    "command": "selectby.anchorAndActiveByRegex",
    "when": "editorTextFocus",
    "args": {
      "active": { "regex": "([^ \\t])\\1", "direction": "prev" }
    }
  },
  {
    "key": "ctrl+shift+alt+right",
    "command": "selectby.anchorAndActiveByRegex",
    "when": "editorTextFocus",
    "args": {
      "active": { "regex": "([^ \\t])\\1", "direction": "next", "repeat": 5 }
    }
  },
  {
    "key": "ctrl+shift+alt+left",
    "command": "selectby.anchorAndActiveByRegex",
    "when": "editorTextFocus",
    "args": {
      "active": { "regex": "([^ \\t])\\1", "direction": "prev", "repeat": 5 }
    }
  }

Select By Multi Cursor with keyboard

You can create and modify Multi Cursors with the keyboard with the commands:

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.

You can define a set of key bindings to use these commands.

By using a custom context variable (extension 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.

With the command extra-context.toggleVariable you can toggle the variable and thus the mode.

  {
    "key": "alt+F5", // or some other key combo
    "when": "editorTextFocus",
    "command": "extra-context.toggleVariable",
    "args": {"name": "multiCursorByKeyboard"}
  },
  {
    "key": "ctrl+alt+right",
    "when": "editorTextFocus && extraContext:multiCursorByKeyboard",
    "command": "selectby.addNewSelection",
    "args": {"offset": 1}
  },
  {
    "key": "ctrl+alt+left",
    "when": "editorTextFocus && extraContext:multiCursorByKeyboard",
    "command": "selectby.removeCursorBelow"
  },
  {
    "key": "shift+right",
    "when": "editorTextFocus && extraContext:multiCursorByKeyboard",
    "command": "selectby.moveLastSelectionActive",
    "args": {"offset": 1}
  },
  {
    "key": "shift+left",
    "when": "editorTextFocus && extraContext:multiCursorByKeyboard",
    "command": "selectby.moveLastSelectionActive",
    "args": {"offset": -1}
  },
  {
    "key": "right",
    "when": "editorTextFocus && extraContext:multiCursorByKeyboard",
    "command": "selectby.moveLastSelection",
    "args": {"offset": 1}
  },
  {
    "key": "left",
    "when": "editorTextFocus && extraContext:multiCursorByKeyboard",
    "command": "selectby.moveLastSelection",
    "args": {"offset": -1}
  }

Select By Move Selections

Sometimes the selection commands select a few characters too much or too little.

With the command selectby.moveSelections you can adjust the selection ends a few characters.

The number values of the properties can be positive, negative or zero.

The args property of the command has the following properties:

active side of the selection is the side where the cursor is.

Which properties are used is determined by:

  1. start or end defined. Use start and end. The property not defined has a value of 0
  2. anchor or active defined. Use anchor and active. The property not defined has a value of 0
  3. use offset

Example:

Reduce the selections 1 character at the start and end.

  {
    "key": "ctrl+i r",
    "when": "editorTextFocus",
    "command": "selectby.moveSelections",
    "args": {"start": 1, "end": -1}
  }

It can also be combined to modify a selection command using the extension multi-command.

If you don't want the brackets to be selected when using the Select to Bracket command:

  {
    "key": "ctrl+i ctrl+b",  // or any other combo
    "command": "extension.multiCommand.execute",
    "args": { 
        "sequence": [
            "editor.action.selectToBracket",
            { "command": "selectby.moveSelections", "args": {"start": 1, "end": -1} }
        ]
    }
  }

Select By Add Selection To Next Find Match Multi Cursor

Multi Cursor variant of Add Selection To Next Find Match (Ctrl+D).

The commandID is: selectby.addSelectionToNextFindMatchMultiCursor

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.

Move By

You can move the cursor based on Regular Expressions or using a Calculation.

Move By Regular Expression

The exported command is: moveby.regex (MoveBy: Move cursor based on regex)

To use fixed Regular Expressions or different properties for MoveBy you can create:

The argument of the command can be:

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.

The setting moveby.regexes is an object with key-value pairs. The value can be an array or an object.

An example for the setting moveby.regexes:

  "moveby.regexes": {
    "Go to Last Dot": {
      "regex": "\\.(?!.*\\.)",
      "properties": ["next", "start"]
    },
    "--Ask-- $(regex) next end": {
      "ask": true,
      "properties": ["next", "end"]
    },
    "--Ask-- $(regex) next start": {
      "ask": true,
      "properties": ["next", "start"]
    }
  }

You can use icons in the key of the setting moveby.regexes.

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.

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.

args of keybinding is an Array

If the "args" property of the key binding is an Array the meaning of the 5 strings are:

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.

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)

args of keybinding is an Object

If the "args" property of the key binding is an Object it can have the following properties:

If you want to move the cursor(s) to the first character inside the next Python string you can use:

  {
    "key": "ctrl+f6",  // or any other key combo
    "when": "editorTextFocus",
    "command": "moveby.regex",
    "args": {
      "regex": "('''|\"\"\"|'|\")",
      "properties": ["next", "end"]
    }
  }

If you want to move the cursor(s) to the start of the next regex asked from the user you can use:

  {
    "key": "ctrl+shift+f6",  // or any other key combo
    "when": "editorTextFocus",
    "command": "moveby.regex",
    "args": {
      "ask": true,
      "properties": ["next", "start"]
    }
  }

If you want to move n, ask user how often, <td> tags forward use:

  {
    "key": "alt+f6",  // or any other key combo
    "when": "editorTextFocus",
    "command": "moveby.regex",
    "args": {
      "regex": "<td[^>]*>",
      "properties": ["next", "end"],
      "repeat": "ask"
    }
  }

if you want to insert a snippet at the end of an HTML open tag

  {
    "key": "alt+f7", // or any other key combo
    "when": "editorTextFocus",
    "command": "extension.multiCommand.execute",
    "args": {
      "sequence": [
        { "command": "moveby.regex",
          "args": { "regex": ">", "properties": ["next", "start"]}, "checkCurrent": true },
        { "command": "editor.action.insertSnippet",
          "args": { "snippet": " class=\"$1\"$0" } }
      ]
    }
  }

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.

Move By Calculation

The exported command is: moveby.calculation

All positions (line and char) are 0 based. The first line has number 0.

Move By uses 2 calculation expressions defined in the "args" property of the key binding.

The expressions can be any JavaScript expression that results in a number. The number is converted to an int with Math.floor

The expressions can use a number of variables:

If you want to move the cursor to the midpoint of the line the cursor is on you can use

  {
    "key": "ctrl+i ctrl+m",  // or any other key binding
    "when": "editorTextFocus",
    "command": "moveby.calculation",
    "args": {
      "charNrEx": "currentLine.length / 2"
    }
  }

If something reports a problem at a character offset (relative to start of file) you can use:

  {
    "key": "ctrl+i ctrl+f",  // or any other key binding
    "when": "editorTextFocus",
    "command": "moveby.calculation",
    "args": {
      "lineNrEx": "offset.line",
      "charNrEx": "offset.character"
    }
  }

If you want a Go To Line but enter a relative line number use:

  {
    "key": "ctrl+alt+g",  // or any other key binding
    "when": "editorTextFocus",
    "command": "moveby.calculation",
    "args": {
      "lineNrEx": "selection.start.line+relative",
      "charNrEx": "selection.start.character"
    }
  }

moveby and Multi Cursor

moveby.regex and moveby.calculation support multi cursor. For each cursor the search is performed or the cursor is moved to the new location.

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:

If more than one cursor end at the same location they will be collapsed into one.

Reveal the new cursor locations

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. If there are multiple cursors the first cursor is used.

Move By and keybindings.json

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.

An example key binding:

  {
    "key": "ctrl+shift+alt+s",
    "when": "editorTextFocus",
    "command": "moveby.regex",
    "args": ["SectionContent", "forward", "prev", "start"]
  }

Move to previous and next empty line

Created by Arturo Dent and it needed a small code change to work because the regex matches an empty string.

Add the following to selectby.regexes

    "goToEmptyLine": {
      "flags": "m",
      "moveby": "^$"
    }

Define 2 key bindings (you can change the assigned keys)

  {
    "key": "ctrl+shift+f7",
    "when": "editorTextFocus",
    "command": "moveby.regex",
    "args": ["goToEmptyLine", "moveby", "prev", "start"]
  },
  {
    "key": "ctrl+shift+f8",
    "when": "editorTextFocus",
    "command": "moveby.regex",
    "args": ["goToEmptyLine", "moveby", "next", "start"]
  }