find-and-transformHighlights
Below you will find information on using the Table of Contents 1. 2. 4. a. 5. Using numbered capture groups in a 6. How to Insert a value at the Cursor 7. Running Multiple finds or replaces 8. Running Javascript Code, and Named Scripts see Script Operations for the full details: a. Math Operations in Replacements a. Path Variables: Launch or Task-like Variables 10. Using 11. Using a. Some a. 14. Demonstrating 15. preCommands and postCommands
Above is an example of the
Use the commands from vscode's Keyboard Shortcuts context menu and
For example, some replacements are much easier to do when the current line is selected first. That way, when the replacement occurs it replaces the entire line. Otherwise you would first have to select that line and then run the keybinding. The following example only works if the current line is selected first. In the example below there is no Note also in the demo that cursors are placed at the end of all the lines thanks to the
Contributed SettingThis extension contributes one setting relevant to the
This setting controls whether the extension will attempt to find errors in your keybinding or settings argument keys and values.
If the The dialogs are modal for the keybindings, and non-modal for the settings. The command can then be run or aborted. Running Javascript Code, and Named ScriptsYou can run JavaScript in a
Not supported in See Script Operations for the full details: writing jsOps (math, strings, the vscode api, side effects with Using newlines
These forms work for newlines in a jsOperation
Newline examples that work and don't work:
I suggest using backticks whenever there is a newline or tab,
What arguments can a
|
| value | Fires w/ 0 matches? | Fires w/ matches? | Times | Match context ($1, etc.) |
|---|---|---|---|---|
onceAlways |
yes | yes | always exactly 1 | if a match exists, its capture groups are available (same as onceIfAMatch); otherwise $1 etc. resolve to "" |
onceIfAMatch (default) |
no | yes | exactly 1, using the first match even if there are more | first match only |
onEveryMatch |
no | yes | once per match, in document order | that match's own capture groups |
onceOnNoMatches |
yes | no | exactly 1 | none ($1 etc. resolve to "") |
Using numbered capture groups in a find
Example : "find": "\\$1 (\\d+)" with text: "const 123"
Any numbered capture group, like the double-escaped
\\$1above, will be replaced in the find query by the first selection in the current file (\\$2will be replaced by the second selection and so on). You can easily make generic find regex's this way, that are determined by your selections not by hard-coding them first. After these replacements, thefindis run.
a. This works for both find in a file or search across files, keybindings or settings.
b. The first selection, which can be just a cursor in a word, is really the first selection made in the file - it may actually appear before or after the second selection!
c. The selections can be words or longer parts of text.
d. If you use a numbered capture group higher than the number of selections, those are replaced with "", the empty string.
\\$nin afindsubstitutes text, it does not create a capture group. The\\$nnumbering counts your selections; the$nnumbering in areplacecounts the parentheses in the find that actually ran. They are separate things that happen to share the same digits. So"find": "\\$1 (\\d+)"becomesconst (\d+)- its only group is(\d+), which makes$1the digits. If you want the selection itself back in thereplace, put your own parentheses around it:
// cursor in "const", document text: const 111
"find": "\\$1 (\\d+)", // becomes const (\d+) -> $1 = "111"
"replace": "\\U$1-$2", // gives "111-" ($2 has no group to resolve against)
"find": "(\\$1) (\\d+)", // becomes (const) (\d+) -> capture group $1 = "const", capture group $2 = "111"
"replace": "\\U$1-$2", // gives "CONST-111"
If the find ends up with no capture groups at all and your
replaceuses$1, the extension wraps the whole find in one group for you, so$1is the entire match. That happens whether or not you setisRegexyourself.
{
"key": "alt+r", // as a keybinding in keybindings.json
"command": "findInCurrentFile", // or "runInSearchPanel" to search across files
"args": {
"find": "\\$1 (\\d+)", // double-escaping necessary
// "find": "(\\$1|\\$2)-${lineNumber}" // selection 1 or selection 2 followed by its line number
// "find": "\\$1(\\d+)\\$2", // up to 9 selections, \\$1 through \\$9
// "replace": "", // if no replace, matches will be highlighted
// "isRegex": true necessary if other parts of the find use regexp's, like \\d, etc.
"isRegex": true // not necessary for the \\$n's + other plain text
}
},
{
"key": "alt+b",
"command": "runInSearchPanel", // uses the Search Panel
"args": {
"find": "\\$1\\.decode\\([^)]+\\)",
"isRegex" : true,
"triggerSearch": true
// "replace": "?????", // not necessary
// "filesToInclude": "${relativeFileDirname} or other path variables",
// "filesToExclude": "<other path variables>",
// "onlyOpenEditors": true
// other options: matchCase/matchWholeWord/preserveCase/useExcludeSettingsAndIgnoreFiles
}
},
Make it into a setting:
"findInCurrentFile": { // in settings.json or a .code-workspace file (in its settings object)
"findRequireDecodeReferences": {
"title": "Find in file: package function references",
"find": "\\$1\\.decode\\([^)]+\\)",
"isRegex": true
}
},
"runInSearchPanel": {
"searchRequireDecodeReferences": {
"title": "Search files: package function references",
// "preCommands": "editor.action.clipboardCopyAction",
"find": "\\$1\\.decode\\([^)]+\\)",
"isRegex": true,
"triggerSearch": true,
// "filesToInclude": "${fileDirname}"
// "onlyOpenEditors": true
// and more options
}
},
And then those settings' commands can be triggered by the Command Palette or by a keybinding like:
{
"key": "alt+k",
"command": "findInCurrentFile.findRequireDecodeReferences"
}
How to insert a value at the cursor
If you do not want to find something and replace it but just want to insert some value at the cursor use a keybinding or setting like the following:
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
// no find key!!
"replace": "\\U${relativeFileDirname}", // insert at cursor
"replace": "Chapter ${matchNumber}", // Chapter 1, Chapter 2, etc. for each cursor
"replace": "Chapter $${ return ${matchNumber} * 10 }$$", // Chapter 10, Chapter 20, etc.
}
}
There are two ways to use this - when there is no find:
The cursor is at a word (or a word is selected, same thing). The
findis constructed from that word/selection and thereplacewill replace any matches.The cursor is not at any word - on a blank line or separated by spaces from any word. Then there is no find constructed and the
replaceis just inserted where the cursor(s) are located.
Demo using "replace": "Chapter ${matchNumber}" and no find:

Explanation for above: In the first case, the cursor is placed on Chapter, so that is the find and each occurrence of it is replaced with Chapter ${matchNumber}. In the second case, multiple cursors are placed on empty lines so there is no find, in which case "Chapter ${matchNumber}" is inserted at each cursor.
Running multiple finds and replaces with a single keybinding or setting
The find and replace fields can either be one string or an array of strings. Examples:
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "(trouble)", // single string - runs once
"find": ["(trouble)"], // an array of one string is allowed - runs once
"replace": "\\U$1", // replace "trouble" with "TROUBLE"
"find": ["(trouble)", "(more trouble)"], // as many comma-separated strings as you want
"replace": ["\\U$1", "\\u$1"], // replace "trouble" with "TROUBLE" and
// replace "more trouble" with "More trouble"
"isRegex": true
}
}
- If there are more
findstrings thanreplacestrings: then the lastreplacevalue will be used for any remaining runs. - If there are more
replace's thanfind's: then a generated find (see more at the "words at cursors" discussion below) using the cursor selections will be used for any remaining runs. This is usually the prior replacement text.
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": ["(trouble)", "(more trouble)"], // two finds
"replace": "\\U$1", // \\U$1 will be used for both replaces so
// replace "trouble" with "TROUBLE" and "more trouble" with "MORE TROUBLE"
"isRegex": true
}
}
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "(trouble)", // one find
"replace": ["\\U$1", "\\u$1"], // more replaces than finds
// replace "trouble" with "TROUBLE" on first run and
// on second run replace any selected words with their capitalized version
"isRegex": true
}
}
You might want to run two or more commands in a sequence like this to accomplish some replacements that are difficult to do in one regexp but much simpler with two find/replaces in sequences. Like:
"find": ["(${relativeFile})", "(${fileExtname})"],
"replace": ["\\U$1", ""],
"isRegex": true
On the first pass above, the fileName will be uppercased. On the second run, the file extension (like .js) will be matched and replaced with nothing (the empty string) and so will be removed.
"find": ["(someWord)", "(WORD)"],
"replace": ["\\U$1", "-\\L$1"],
"isRegex": true,
"matchCase": true
On the first pass above, "someWord" will be replaced with "SOMEWORD". On the second pass, find "WORD" and replace it with "-word". So you will replace "someWord" with "SOME-word" after both runs. Yes, you could make a single regex to do this in one run, but in more complicated cases using two or more runs can make it simpler.
Special variables
Variables defined by this extension for use in args
${resultsFiles} ** explained below ** Only available in a 'runInSearchPanel' command
- ${getFindInput} deprecated, use ${getInput}
+ ${getInput} to enter the Find query or replacement text or cursorMoveSelect text or postCommand text
+ or filesToInclude or filesToExclude via an INPUT BOX rather than in the keybinding/setting
${getDocumentText} get the entire text of the current document
${getTextLines:n} get the text of a line, 'n' is 0-based, so ${getLineText:1} gets the second line of the file
${getTextLines:n-p} get the text of lines n through p inclusive, example ${getTextLines:2-4}
${getTextLines:(n-p)} get the text of a line n-p (i.e., n minus p), example ${getTextLines:(${lineIndex}-1)} : get previous line
use the parentheses, if you want to do math to resolve to a line. Can use `+-/*%`. ${getTextLines:(${lineIndex}+p)}, etc.
${getTextLines:n,p,q,r} get the text from line `n`, column `p` through line `q`, column `r` inclusive,
example ${getTextLines:2,0,4,15}
INTELLISENSE: can be used in the keybindings or settings showing where the variables can be used. You will also get intellisense for any unused args (like
find,isRegex,matchCase, etc.). You can always get more intellisense by triggering it manually with Ctrl/Cmd+Space at many locations in your keybindings or settings.
${resultsFiles}is a specially created variable that will scope the next search to those files in the previous search's results. In this way you can run successive searches narrowing the scope each time to the previous search results files. See Search using the Panel.
- Here is an example using
${getDocumentText}:
{
"key": "alt+e",
"command": "findInCurrentFile",
"args": {
"replace": [
"$${",
// note the variables should be wrapped in backticks, so they are interpreted as strings
"const fullText = `${getDocumentText}`;",
// "const fullText = `${vscode.window.activeTextEditor.document.getText()}`;", // same as above
// "const fullText = `${document.getText()}`;", // same as above
// "const fullText = `${getLineText:3}`;", // if you knew which line you wanted to get
"let foundClass = '';",
"const match = fullText.match(/class ([^\\s]+) extends/);",
"if (match?.length) foundClass = match[1];",
"return `export default connect(mapStateToProps, mapDispatchToProps)(${foundClass})`;",
"}$$"
],
"postCommands": "cancelSelection"
},
},
Notice there is no find, so that the result of the replace will be inserted at the cursor(s). In this case, the replace will get the entire text and then match it looking for a certain class name as a capture group. If found, it will be added to a value that is returned. See this Stack Overflow question to see this in action.
The ${getDocumentText} variable allows you to look anywhere in a document for any text or groups of text that you can find with a regex. You are not limited to the current line or the clipboard or selection for example.
- Here is an example using
${getInput}:
{
"key": "alt+c",
"command": "findInCurrentFile",
"args": {
"description": "I want to enter the Find query in an input box.", // whatever text you want
"find": "${getInput}", // enter plain text or a regular expression in the input box that pops up
"find": "${getInput} stuff \\U${getInput}", // can use multiple ${getInput} variables
// you can mix text with what you will input
"find": "before ${getInput} after",
// ${getInput} inside a js operation
"find": "$${return '${getInput}' + 'end';}$$", // treat '${getInput}' as a string and add 'end' to it and match
"find": "(${getInput})", // wrap in a capture group to use later
"isRegex": true, // treat $1 as a capture group and do string operations on it
"replace": "${BLOCK_COMMENT_START} $${return '$1'.toLocaleUpperCase();}$$ ${BLOCK_COMMENT_START}",
// "replace": "${BLOCK_COMMENT_START} \\U$1 ${BLOCK_COMMENT_START}", // simpler version of above
"isRegex": true, // if you want that input treated as a regular expression ***
"replace": "everything is fine",
"replace": "${getInput} is my replacement", // input text added to any other replace text
// make sure to surround the '${getInput}' value with backticks if you want it treated as a string in a jsOperation
"replace": "$${return '${getInput}' was added;}$$",
// no backticks around ${getInput} because we will enter a number, it would be an error to enter a string
"replace": "$${return ${getInput} * ${lineNumber};}$$",
// below: on every match an input box will be presented, any text entered there will be written to a new file
"run": [
"$${",
"vscode.env.clipboard.writeText('${getInput}');", // get and write input to the clipBoard
"vscode.commands.executeCommand('workbench.action.files.newUntitledFile');", // open a new file
"vscode.commands.executeCommand('editor.action.clipboardPasteAction');", // paste to the new file
"}$$",
],
"runWhen": "onEveryMatch",
// below: for every find match, an input box will be shown, the text you enter will be inserted at rhe cursor(s)
"postCommands": [
{
"command": "type",
"args": {
"text": " from the input: ${getInput}"
}
}
],
"runPostCommands": "onEveryMatch",
"cursorMoveSelect": "${getInput}" // the input text will be selected after any replacement
}
When using a regex in a ${getInput} do not double-escape any characters like \n or \s. Just use the same regex you would use in the Find Widget in vscode.
Launch or task variables: path variables
These can be used in the find or replace fields of the findInCurrentFile command or in the find, replace, and perhaps most importantly, the filesToInclude and filesToExclude fields of the runInSearchPanel command:
${file} easily limit a search to the current file, full path
${fileBasename}
${fileBasenameNoExtension}
${fileExtname}
${relativeFile} current file relative to the workspaceFolder
${fileDirname} the current file's parent directory, full path
${relativeFileDirname} the current file's parent directory only
${fileWorkspaceFolder}
${workspaceFolder}
${workspaceFolderBasename}
${userHome} the current user's home folder, full path
${pathSeparator}
${/} same as ${pathSeparator}
${selectedText} can be used in the find/replace/cursorMoveSelect fields
${CLIPBOARD}
${lineIndex} line index starts at 0
${lineNumber} line number start at 1
${columnNumber} character position on the line
${matchIndex} 0-based, replace with the find match index - first match, second, etc.
${matchNumber} 1-based, replace with the find match number
These variables should have the same resolved values as found at vscode's pre-defined variables documentation.
On Windows, these path variables always return forward slashes (
C:/Users/yourName/myProject/folder/file.ext) rather than vscode's usual backslashes. This keeps a resolved path safe to drop into a$${ ... }$$script, where a backslash would otherwise be misread as a JavaScript string-escape character (e.g.\tbecoming a tab). Forward slashes work fine in Windows paths forfs/pathcalls and everywhere else these variables are used.
These path variables can also be used in a conditional like
${1:+${relativeFile}}. If capture group 1, insert the relativeFileName.
Examples are given below using
lineIndex/NumberandmatchIndex/Number.
Snippet variables
${TM_CURRENT_LINE} The text of the current line for each selection.
${TM_CURRENT_WORD} The word at the cursor for each selection or the empty string.
${CURRENT_YEAR} The current year.
${CURRENT_YEAR_SHORT} The current year's last two digits.
${CURRENT_MONTH} The month as two digits (example '02').
${CURRENT_MONTH_NAME} The full name of the month (example 'July').
${CURRENT_MONTH_NAME_SHORT} The short name of the month (example 'Jul').
${CURRENT_DATE} The day of the month as two digits (example '08').
${CURRENT_DAY_NAME} The name of day (example 'Monday').
${CURRENT_DAY_NAME_SHORT} The short name of the day (example 'Mon').
${CURRENT_HOUR} The current hour in 24-hour clock format.
${CURRENT_MINUTE} The current minute as two digits.
${CURRENT_SECOND} The current second as two digits.
${CURRENT_SECONDS_UNIX} The number of seconds since the Unix epoch.
${CURRENT_TIMEZONE_OFFSET} Modified from Date.prototype.getTimezoneOffset()
and see https://github.com/microsoft/vscode/issues/151220
Thanks to https://github.com/microsoft/vscode/pull/170518 and
https://github.com/MonadChains
${RANDOM} Six random Base-10 digits.
${RANDOM_HEX} Six random Base-16 digits.
${BLOCK_COMMENT_START} Example output: in PHP `/*` or in HTML `<!--`.
${BLOCK_COMMENT_END} Example output: in PHP `*/` or in HTML `-->`.
${LINE_COMMENT} Example output: in PHP `//`.
These snippet variables are used just like the path variables mentioned above. With \\U${CURRENT_MONTH_NAME} to uppercase the current month name for example.
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"replace": "$${ return ${CURRENT_HOUR} - 1 }$$"
}
}
Explanation: The above keybinding (or it could be a command) will insert the result of (current hour - 1) at the cursor, if the cursor is not at a word - so on a empty line or with a space separating the cursor from any other word. Otherwise, if the cursor is on a word that word will be treated as the find and all its occurrences (within the restrictFind scope: entire document/selections/onceIncludeCurrentWord/onceExcludeCurrentWord/line/next..) will be replaced by (current hour - 1).
To insert a timestamp try this keybinding:
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"replace": "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}${CURRENT_TIMEZONE_OFFSET}",
}
}
Result for the above would be 2023-02-24T03:52:55-08:00 for a locale with UTC-8. Since there is no find argument just make sure your cursor is not at a word when this is triggered (or that word will be replaced, which may be what you want in some cases).
And the same as a setting in your settings.json:
"findInCurrentFile": {
"AddTimeStampWithTimeZoneOffset": { // this line cannot have spaces
// this will appear in the Command Palette as 'Find-Transform: Insert a timestamp with timezone offset'
"title": "Insert a timestamp with timezone offest", // whatever you want here
"replace": "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}${CURRENT_TIMEZONE_OFFSET}"
}
}
The above will, after a reload, appear in the Command Palette as 'Find-Transform: Insert a timestamp with timezone offset' which text you can change as you want.
- Note that vscode can do fancy things with snippet comment variables like
${LINE_COMMENT}by examining the language of individual tokens so that, for example, css in js would get its correct comment characters if within the css part of the code. This extension cannot do that and will get the proper comment characters for the overall file type only.
Case modifier transforms
The find query and the replace transforms can include case modifiers like:
Can be used in the `replace` field:
\\U$n uppercase the entire following capture group as in `\\U$1`
\\u$n capitalize the first letter only of the following capture group: `\\u$2`
\\L$n lowercase the entire following capture group: `\\L$2`
\\l$n lowercase the first letter only of the following capture group: `\\l$3`
Can be used in either the `replace` or `find` fields:
\\U${relativeFile} or any launch/task-like variable listed above
\\u${any launch variable}
\\L${any launch variable}
\\l${any launch variable}
These work in both the findInCurrentFile and runInSearchPanel commands or keybindings.
Example:
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
// find the lowercased version of the relativeFileName
"find": "(\\L${relativeFile})", // note the outer capture group
"replace": "\\U$1", // replace with the uppercased version of capture group 1
"matchCase": true, // this must be set or the find case will be ignored!
"isRegex": true
}
}
Note, the above case modifiers must be double-escaped in the settings or keybindings. So
\U$1should be\\U$1in the settings. VS Code will show an error if you do not double-escape the modifiers (similar to other escaped regexp items like\\w).
Conditional replacements in findInCurrentFile commands or keybindings
Vscode snippets allow you to make conditional replacements, see vscode's snippet grammar documentation. However you cannot use those in vscode's find/replace widget. This extension allows you to use those conditionals in a findInCurrentFile command or keybinding. Types of conditionals and their meaning:
${1:+add this text} If found a capture group 1, add the text. `+` means `if`
${1:-add this text} If *NO* capture group 1, add the text. `-` means `else`
${1:add this text} Same as `else` above, can omit the `-`
${1:?yes:no} If capture group 1, add the text at `yes`, otherwise add the text at `no` `?` means `if/else`
Examples:
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "(First)|(Second)|(Third)", // your regexp with possible capture groups
"replace": "${3:-yada3} \\U$1", // if no group 3, add "yada3" then upcase group 1
// groups within conditionals may be surrounded by backticks `$2`, but it should not be necessary
"replace": "${2:+abcd `\\U$2` efgh}", // if group 2, add capitalized group 2 plus surrounding text
"replace": "${2:+abcd \\U$2 efgh}", // same as above
"replace": "${1:+aaa\\}bbb}", // must double-escape closing brackets if want it as text
"replace": "\\U${1:+aaa-bbb}", // to capitalize the entire replacement
"replace": "${1:+*`$1``$1`*}${2:+*`$2``$2`*}", // lots of combinations possible
"replace": "$0", // can use whole match as a replacement
"replace": "", // the match will be replaced with nothing, i.e., an empty string
"replace": "${2:?yada2:yada3}\\U$1", // if group 2, add "yada2", else add "yada3"
// then follow with upcased group 1
"replace": "${2:?`$3`:`$1`}", // if group 2, add group 3, else add group 1
"replace": "${2:?$3:$1}", // if group 2, add group 3, else add group 1
"isRegex": true
}
}
- Groups within conditional text to be added (which is not possible even in a vscode snippet), must be surrounded by backticks.
- If you want to use the character
}in a replacement within a conditional, it must be double-escaped\\}. - The
"replace": ""above deletes matches immediately, as part of the same edit as the find - that's specific tofindInCurrentFile's direct document edits.runInSearchPanelbehaves differently:replaceonly fills in the Search panel's Replace field and has no effect until a real "Replace All" is triggered. See Search using the Panel for those rules.
Snippet-like transforms: replacements in findInCurrentFile commands or keybindings
The following can be used in a replace field for a findInCurrentFile command:
${1:/upcase} if capture group 1, transform it to uppercase (same as `\\U$1`)
${2:/downcase} if capture group 2, transform it to uppercase (same as `\\L$1`)
${3:/capitalize} if capture group 3, transform it to uppercase (same as `\\u$1`)
${1:/pascalcase} if capture group 1, transform it to pascalcase
(`first_second_third` => `FirstSecondThird` or `first second third` => `FirstSecondThird`)
${1:/camelcase} if capture group 1, transform it to camelcase
(`first_second_third` => `firstSecondThird` or `first second third` => `firstSecondThird`)
${1:/snakecase} if capture group 1, transform it to snakecase
(`firstSecondThird` => `first_second_third`, so camelcase only to snakecase)
${1:/kebabcase} if capture group 1, transform it to kebab-case
(`first_second_third`, `first second third`, or `firstSecondThird` => `first-second-third`)
Examples:
If you wanted to find multiple items and then transform each in its own way one match at a time:
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "(first)|(Second)|(Third)",
"replace": "${1:+ Found first!!}${2:/upcase}${3:/downcase}",
"isRegex": true,
"restrictFind": "nextSelect" // one match at a time !!
// 'nextMoveCursor' would do the same, moving the cursor but not selecting
}
}

Explanation for above:
"restrictFind": "nextSelect"do the following one at a time, selecting each in turnIf you want to skip transforming a match, just move the cursor beyond it (rightArrow).
${1:+ Found first!!}if find a capture group 1, replace it with text "Found First!!"${2:/upcase}if find a capture group 2, uppercase it${3:/downcase}if find a capture group 3, lowercase it
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"description": "transform existing fileBaseName in the text to SCREAMING_SNAKE_CASE",
"find": "(${fileBasenameNoExtension})", // make a capture group 1
"replace": "\\U${1:/snakecase}",
"isRegex": true // necessary because the {1:/snakecase} needs to refer to some capture group
}
}
Here is a neat trick to insert a SCREAMING_SNAKE_CASE version of the ${fileBasenameNoExtension} at the cursor(s):
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"description": "insert the fileBaseName and change to SCREAMING_SNAKE_CASE",
"replace": ["${fileBasenameNoExtension}", "\\U${1:/snakecase}"],
"isRegex": true // necessary because the ${1:/snakecase} needs to refer to some capture group
}
}
The above works by performing 2 replacements (with no find). First, insert at the cursor(s) the ${fileBasenameNoExtension} and second, replace that (since it will be selected by this extension) with the capitalized, snake-case version.

Careful: with
isRegexset to true and you use settings like:
"args": {
"find": "(trouble)", // only a capture group 1
// "find": "trouble", // no capture groups!, same bad result
"replace": "\\U$2", // but using capture group 2!!, so replacing with nothing
// "replace": "${2:/pascalcase}", // same bad result, refers to capture group 2 that doesn't exist
"isRegex": true
}
You would effectively be replacing the match trouble with nothing, so all matches would disappear from your code. This is the correct result, since you have chosen to match something and replace it with something else that may not exist.
If
isRegexis set tofalse(the same as not setting it at all), the replace value, even one like\\U$2will be interpreted as literal plain text.
Using restrictFind with the matchAroundCursor option
Example keybinding:
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "<(Element)(>[\\s\n\\S]*?<\/)(Element)>", // $1 and $3 capture groups = Element
"isRegex": true,
"replace": "<\\U$1$2\\U$3>", // \\U$1 = capitalize group 1
"restrictFind": "matchAroundCursor"
}
}
The above keybinding would select the entire Element and capitalize groups 1 and 3, so the result would look like
<ELEMENT>
stuff
more stuff
</ELEMENT>
matchAroundCursorwill select any find match that surrounds the cursor. In the above example the cursor only needs to be somewhere within the text that matches the find. Ths option can be used for quickly extracting a block of text with a SINGLE regular expression. And then that block of text can be manipulated in areplaceorrunargument.You can also use the
cursorMoveSelectargument with thematchAroundCursorresult.
Example: this run argument will take the selected text - like from the find match - and create a new file with that text pasted in:
"run": [
"$${",
"let block = '```';", // start a code fence
"block += document.languageId;", // use the current editor's languageId as the code fence language
"block += `\\n\\t${selectedText}\\n`;", // strip off trailing newline(s)?
"block += '```';", // end a code fence
"vscode.env.clipboard.writeText(block);", // write that text to the clipBoard
"vscode.commands.executeCommand('workbench.action.files.newUntitledFile');", // open a new file
"vscode.commands.executeCommand('editor.action.clipboardPasteAction');", // paste to the new file
// go back to original file
"vscode.commands.executeCommand('workbench.action.openPreviousRecentlyUsedEditor');",
"}$$",
],
Details on the restrictFind and cursorMoveSelect arguments
Example keybinding:
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "FIXME", // or use the word at the cursor
"replace": "DONE",
"restrictFind": "nextDontMoveCursor"
// "cursorMoveSelect": "FIXME" // will be ignored with the 'next...` options
}
}

These will all reveal the replacement so you can see the change, but not necessarily move the cursor.
"restrictFind": "nextDontMoveCursor"make the next replacement but leave the cursor at the original position."restrictFind": "nextMoveCursor"make the next replacement and move the cursor to end of next replaced match. Does not select."restrictFind": "nextSelect"make the next replacement and select it."restrictFind": "previousDontMoveCursor"make the previous replacement but leave the cursor at the original position."restrictFind": "previousMoveCursor"make the previous replacement and move the cursor to start of previous replaced match. Does not select."restrictFind": "previousSelect"make the previous replacement and select it.
The
next...andprevious...options will wrap. This means for example if there is no match in the document after the cursor that the first match from the beginning of the document will be used (when using anext...option).
When using the above
restrictFindoptions thecursorMoveSelectoption will be ignored.
And these options above do not currently work with multiple selections. Only the first selection made in the document will be used as a
findvalue - so the order you make selections matters. If you made multiple selections from the bottom of the document up, the first selection made (which would appear after other selections) would be used.
You can use the cursorMoveSelect option with the below restrictFind options.
"restrictFind": "document"the default, make all replacements in the document, select all of them."restrictFind": "onceIncludeCurrentWord"make the next replacement from the beginning of the current word on the same line only."restrictFind": "onceExcludeCurrentWord"make the next replacement after the cursor on the same line only."restrictFind": "line"make all replacements on the current line where the cursor is located."restrictFind": "selections"make all replacements in the selections only.
Note that for all of the above, the replacement text might include more or fewer newlines so that although the find did occur on one line, the cursorMoveSelect match might actually occur on a different line. That is okay, the entire replacement text will be matched against, whether some of it is on the same line or a subsequent line.
New `once...` restrictFind Values. `once` deprecated:
The once argument to restrictFind is being deprecated in favor of two related values: onceExcludeCurrentWord and onceIncludeCurrentWord. onceExcludeCurrentWord functions exactly as once does, the searched text begins strictly at the cursor position - even if that is in the middle of a word. That does allow you to use that ${TM_CURRENT_WORD} in a find or replace and not actually change the current word, but the next instance. But sometimes you do want to change the current word and then onceIncludeCurrentWord is what you want. Then the entire word at the cursor is part of the search text and it will be selected or replaced according to your keybinding/setting.
The cursorMoveSelect option takes any text as its value, including anything that resolves to text, like $ or any variable. That text, which can be a result of a prior replacement, will be searched for after the replacement and the cursor will move there and that text will be selected. If you have "isRegex": true in your command/keybinding then the cursorMoveSelect will be interpreted as a regexp (as well as the find). matchCase and matchWholeWord settings will be honored for both the cursorMoveSelect and find text.
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "(trouble)",
"replace": "\\U$1",
"isRegex": true,
// "matchWholeWord": true, // applies to both find and cursorMoveSelect
// "matchCase": true, // applies to both find and cursorMoveSelect
// select only if at beginning of line: ^
"cursorMoveSelect": "^\\s*pa[rn]am", // will be interpreted as a regexp since 'isRegex' is true
"restrictFind": "line", // select 'pa[rn]am' on the current line after making the replacement(s)
// "restrictFind": "selections", // select 'pa[rn]am' only in the selection(s)
// "restrictFind": "line",
// "cursorMoveSelect": "^" // cursor will go to beginning of line
// "cursorMoveSelect": "$" // cursor will go to end of line (after replacement, which may include newlines)
// "restrictFind": "onceIncludeCurrentWord/onceExcludeCurrentWord",
// "cursorMoveSelect": "^" // cursor will go to beginning of the first match (after replacement)
// "cursorMoveSelect": "$" // cursor will go to end of the first match (after replacement)
// "restrictFind": "selections",
// "cursorMoveSelect": "^" // cursor will go to beginning of each selection
// "cursorMoveSelect": "$" // cursor will go to end of each selection
// selections are directional,
// the cursor will go to the start or end (the end is where the cursor was in the original selection)
}
}
Note ^ and $ work for restrictFind selections/line/onceIncludeCurrentWord/onceExcludeCurrentWord/document.
cursorMoveSelectwill select all matches in eachselectionsonly if there was a match in the same selection.cursorMoveSelectwill select the firstcursorMoveSelectmatch usingrestrictFind:onceIncludeCurrentWordoronceExcludeCurrentWordonly if there was a match on the same line before acursorMoveSelectmatch. So afindmatch first and then acursorMoveSelectmatch after that on the same line.cursorMoveSelectwill select allcursorMoveSelectmatches in thedocumentonly if there was a find match and only within the range of the find match!! This may seem like a limitation but it makes possible some nice funtionality usingpostCommands.cursorMoveSelectwill select all matches on a line usingrestrictFind:lineonly if there was a match on the same line.
When you use the
cursorMoveSelectargument for arestrictFind: documentor thenextMoveCursorornextSelectoptions for therestrictFindkey, it is assumed that you actually want to go there and see the result. So the editor will be scrolled to reveal the line of that match if it is not curently visible in the editor's viewport. Forselections/line/onceIncludeCurrentWord/onceExcludeCurrentWordno scrolling will occur - it is assumed that you can see the resulting match already (the only way that wouldn't typically be true is if you had a long selection that went off-screen).
Note: if there is no find and no replace or a find but no replace, the
cursorMoveSelectargument is ignored.
Some "restrictFind": "next... option examples
{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "FIXME",
"replace": "DONE!",
"restrictFind": "nextMoveCursor"
}
}

{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
"find": "FIXME",
"replace": "DONE!",
"restrictFind": "nextSelect"
}
}

{
"key": "alt+r",
"command": "findInCurrentFile",
"args": {
// "find": "FIXME", // !! no find or replace !!
// "replace": "DONE",
"restrictFind": "nextMoveCursor" // or try `nextSelect` here
}
}

Explanation for above: With no find argument, the current nearest word to the cursor (see more on this below) will be used as the find value. So, in the above example FIXME will be used as the find query. And with nextMoveCursor the cursor will move to the next match. nextSelect could be used here as well.
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
// "find": "$", // go to the end '$' of each line one at a time
// go to the end '$' of each line if it isn't an empty line - using a positive lookbehind - one at a time
"find": "(?<=\\w)$",
"replace": "-${lineNumber}", // insert the lineNumber (1-based) at the match (the end of a line)
// "replace": "${lineIndex}", // insert the lineIndex (0-based) at the match (the end of a line)
"isRegex": true,
"restrictFind": "nextSelect"
// note in the demo that nextSelect/nextMoveCursor/nextDontMoveCursor will wrap back to start of the file
}
}

Explanation for above: Find the end of non-empty lines and append '-' and that line number. nextSelect => do one at a time.
{
"key": "alt+n",
"command": "findInCurrentFile",
"args": {
// "description": "capitalize 'first' or 'second' one at a time",
"find": "(first|second)",
"replace": "\\U$1",
"isRegex": true,
"matchCase": true, // necessary if not moving the cursor, so don't select the same entry
"restrictFind": "nextSelect"
}
}
Sample Settings
Note: commands that you create in the settings can be removed by deleting or commenting out the associated settings and re-saving the
settings.jsonfile and reloading VS Code.
In your settings.json:
"findInCurrentFile": { // perform a find/replace in the current file or selection(s)
"upcaseSwap2": { // <== the "name" that can be used in a keybinding, no spaces
"title": "swap iif <==> hello", // title that will appear in the Command Palette
"find": "(iif) (hello)",
"replace": "_\\u$2_ _\\U$1_", // double-escaped case modifiers
"isRegex": true,
"restrictFind": "selections"
},
"capitalizeIIF": {
"title": "capitalize 'iif'", // all settings must have a "title" field
"find": "^(iif)",
"replace": "\\U$1",
"isRegex": true
},
"addClassToElement": {
"title": "Add Class to Html Element",
"find": ">",
"replace": " class=\"@\">",
"restrictFind": "selections",
"cursorMoveSelect": "@" // after the replacement, move to and select this text
}
}
// perform a search/replace using the Search Panel, optionally in current file/folder/workspace/etc.
"runInSearchPanel": { // use this as first part of command name in keybindings
"removeDigits": { // used in the keybindings so no spaces allowed
"title": "Remove digits from Art....",
"find": "^Arturo \\+ \\d+", // double-escaped '+' and '\d'
"replace": "",
"triggerSearch": "true",
"isRegex": true
}
}
Note that
removeDigitsabove sets"replace": ""but onlytriggerSearch- notriggerReplaceAll. UnlikefindInCurrentFile, which edits the document directly,runInSearchPanelonly fills in the Search panel's Find/Replace fields;replacehas no effect on the file(s) until a real "Replace All" actually fires (either you click it yourself, ortriggerReplaceAllis set for that search). See the "Other defaults" section of Search using the Panel for the rules onreplace/triggerReplaceAll, especially with multiple, chained searches.
If you do not include a
titlevalue, one will be created using the name (likeremoveDigitsin the last example immediately above. Then you can look forFind-Transform:removeDigitsin the Command Palette. Since in the last example atitlewas supplied, you would seeFind-Transform: Remove digits from Art....in the Command Palette. All the commands are grouped under theFind-Transform:category.
In a .code-workspace file (for multi-root workspaces):
{
"folders": [
{
"path": ".."
},
{
"path": "../../select-a-range"
}
],
"settings": {
"findInCurrentFile": {
"bumpSaveVersion": { // use this name in the codeActionsOnSave setting
"title": "bump the save version on each save",
"find": "(?<=#### Save Version )(\\d+)",
"replace": "$${ return $1 + 1 }$$",
"isRegex": true,
"ignoreWhiteSpace": false,
"matchCase": false
}
},
"runInSearchPanel": {
"inSearchPanel": {
"title": "some title",
"ignoreWhiteSpace": false,
"delay": 2000,
"isRegex": true,
"matchCase": false,
"useExcludeSettingsAndIgnoreFiles": true,
"triggerSearch": true
}
}
}
}
Sample Keybindings
Examples of keybindings (in your keybindings.json):
// below: keybindings generated from commands in the settings
{
"key": "alt+u",
"command": "findInCurrentFile.upcaseKeywords" // from the settings
} // any "args" here will be ignored, they are in the settings
// below: a generic "findInCurrentFile" keybinding command, no need for any settings to run these
{
"key": "alt+y",
"command": "findInCurrentFile", // note no second part of a command name
"args": { // must set the "args" here since no associated settings command
"find": "^(this)", // note the ^ = beginning of selection because restrictFind = selections
// or ^ = beginning of line within a selection
// "find": "^(${CLIPBOARD})", // same result as above if 'this' on the clipboard
// remember to have the matching capture group used in the replace in your find!
"replace": "\\U$1",
// "replace": "${1:/upcase}", // same as '\\U$1'
"isRegex": true,
"matchCase": true,
"restrictFind": "selections",
"cursorMoveSelect": "THIS" // this text will be selected; "$" goes to the end of ALL the selections
}
}
Using ${lineNumber} or ${lineIndex} in the find:
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "(${lineNumber})", // find the matching line number on its line
// so find a 1 on line 1, find a 20 on line 20
"replace": "$${ return `found ` + ($1*10) }$$",
// a 1 on line 1 => 'found 10'
// a 20 on line 20 => 'found 200'
// demo below
"replace": "$${ if ($1 <= 5) return $1/2; else return $1*2; }$$",
// if a number is on its lineNumber, like a 5 on line number 5 = a find match
// if that match <= 4 return that lineNumber / 2
// else return that lineNumber * 2
"isRegex": true,
}
}

When you save a change to the settings, you will get the message notification below. This extension will detect a change in its settings and create corresponding commands. The commands will not appear in the Command Palette without saving the new setting and reloading vscode.

An example of keybinding with NO associated setting, in keybindings.json:
{
"key": "alt+y",
"command": "findInCurrentFile", // note no setting command here, like findInCurrentFIle.removeDigits
"args": {
// multiline regexp ^ and $ are supported, "m" flag is automatically applied to all searches
// if finding within a selection(s), '^' refers to the start of a selection, NOT the start of a line
// if finding within a selection(s), '$' refers to the end of a selection, NOT the end of a line
"find": "^([ \\t]*const\\s*)(\\w*)", // note the double escaping
"replace": "$1\\U$2", // capitalize the word following "const"
"isRegex": true,
"restrictFind": "selections" // find only in selections
}
}

In this way you can specify a keybinding to run a generic findInCurrentFile command with all the arguments right in the keybinding and nowhere else. There is no associated setting and you do not need to reload vscode for this version to work. You can have an unlimited number of keybindings (with separate trigger keys and/or when clauses, of course) using the findInCurrentFile version.
The downside to this method is that these findInCurrentFile keybinding-only versions cannot be found through the Command Palette.
"nearest words at cursors"
Important: What are
"nearest words at cursors"? In VS Code, a cursor immediately next to or in a word is a selection (even though no text may actually be selected!). This extension takes advantage of that: if you run afindInCurrentFilecommand with nofindarg it will treat any and all "nearest words at cursors" as if you were asking to find those words. Actual selections and "nearest words at cursors" can be mixed by using multiple cursors and they will all be searched for in the document. It appears that a word at a cursor is defined generally as this:\b[a-zA-Z0-9_]\b(consult the word separators for your given language) although some languages may define it differently.
If a cursor is on a blank line or next to a non-word character, there is no "nearest word at cursor" by definition and this extension will simply return the empty string for such a cursor.
So with the cursor at the start or end of
FIXMEor anywhere within the word,FIXMEis the word at the cursor.FIXME-Soonconsists of two words (in most languages). If the cursor followed the*inFIXME*thenFIXMEis not the word at the cursor.
This is demonstrated in some of the demos below.
- Generic
runcommand inkeybindings.json, nofindorreplacekeys in theargs
{
"key": "alt+y",
"command": "findInCurrentFile"
},

Explanation for above: With no find key, find matches of selections or nearest words at cursors (multi-cursors work) and select all those matches. Blue text are selections in the demo gif.
Important: If there is no
findkey and there are mutiple selections then this extension will create afindquery using all those selections. The generatedfindwill be in the form of"find": "(word1|word2|other selected text). Note the use of the alternation pipe|so any of those selected words can be found. Thus, the find in file or find across files must have the regex flag enabled. Therefore, if you have multiple selections with nofindkey,"isRegex": truewill be automatically set - possibly overriding what you had in the settings or keybindings.
That should only be a problem if you select text that gets generated into a
findterm that itself contains regexp special characters, like.?*^$, etc. They will not be treated as literal characters but as their usual regexp functionality.
If your
replace/runuses$1- including inside a$${ jsOperation }$$or a$${script:name}$$reference (see Script Operations) - you don't need to setisRegexyourself at all, for either a single selection or multiple. The extension detects the$1(checking inside a named script's saved code too) and automatically escapes any regex-special characters in the generated find, wraps it in(...), and turnsisRegexon for you - so$1correctly resolves to your literal selected text, whether that's a single word or a whole line full of().+*$and other regex-special characters. Don't set"isRegex": trueyourself just to make$1work - the$1will resolve either way, but setting it skips that automatic escaping, and a selection containing regex-special characters (like most real code) will then fail to match itself at all.
If you are using no
findbut are selecting text that you want treated as a regular expression (like\n text (\d)) do not double-escape those special regex characters. Just use the same regex you would use in the Find Widget. Remember to haveisRegexset to true in this case.
Finally, if you select multiple instances of the same text the generated
findterm will have any duplicates removed. Javascript'sSet.add()is a beautiful thing.
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"matchCase": true,
"restrictFind": "nextSelect"
}
}
The above will repeatedly select the next matching word under the cursor (the 'matchCase' option is up to you).
find and replace keys with no restrictFind
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "(create|table|exists)",
"replace": "\\U$1",
"isRegex": true
}
}

Explanation for above: Find and replace each with its value in the args field. Since there is no restrictFind key, the default document will be used.
find and replace with "restrictFind": "selections"
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "(create|table|exists)", // find each of these words
"replace": "_\\U$1_", // capitalize _capture group 1_
"isRegex": true,
"restrictFind": "selections",
"cursorMoveSelect": "TABLE" // will select 'TABLE' only if it is within a selection
}
}

Explanation for above: Using restrictFind arg set to selections, find will only occur within any selections. Selections can be multiple and selections do include "nearest words at cursors". Using cursorMoveSelect to select all instances of TABLE.
Note a subtle difference in the above demo. If you make an actual full selection of a word or words, only text within that selection will be searched. But if you make a "nearest word"-type selection (a cursor in or next to a word) then all matching words in the document will be searched for, even if they are not in a selection of their own. If you want to restrict the search to a selection, make an actual selection - do not rely on the nearest word functionality.
If restrictFind is not set to anything, it defaults to document. So the entire document will be searched and any selections will be ignored, since a find has been set. Remember if no find is set, then any selections will be interpreted as the find values.
The above keybinding is no different than this setting (in your settings.json):
"findInCurrentFile": {
"upcaseSelectedKeywords": {
"title": "Uppercase selected Keywords", // a "title" is required in the settings
"find": "(create|table|exists)",
"replace": "_\\U$1_",
"isRegex": true,
"restrictFind": "selections",
"cursorMoveSelect": "TABLE"
}
}
except that a reload of vscode is required prior to using the generated command from this setting (no reload necessary for the keybinding) and the title, in this case "Uppercase selected Keywords" will appear and be searchable in the Command Palette (not true for keybinding "commands").
find but no replace key
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "(create|table|exists)",
"isRegex": true
}
}

Explanation for above: Will find according to the find value and select all those matches. No replacement.
find and no replace with `"restrictFind": "selections"
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "(create|table|exists)",
// "replace": "_\\U$1_",
"isRegex": true,
"restrictFind": "selections"
}
}

Explanation for above: Using restrictFind arg set to selections, find will only occur within any selections. All find matches within selections will be selected.
If you have set "restrictFind": "document" any actual selections in the file will be ignored and the find/replace will be applied to the entire file.
with a replace key but NO find key
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
// "find": "(create|table|exists)",
"replace": "\\U$1",
"isRegex": true,
"matchWholeWord": true
}
}

Explanation for above: With no find value find all the words at the cursors or selections and apply the replacement.
In a keybinding/setting with no find BUT a capture group in the replace, like in the last example, the isRegex argument can work in two ways:
true: the word at the cursor (or more likely the selection) is treated as a regex. So it may contain special regular expression characters like*^$?!.[]()\. Example:find*meso thatfindmeorfinddddmeorfinmeare found.false: the word at the cursor (or more likely the selection) is NOT treated as a regex. It is treated as plain text, so any special regex characters will be escaped so that a regex match can be performed on that text - necessary since the replace may contain a capture group reference, like$1. Example:find*mebecomesfind\*meso that the literal textfind*meis searched for.
Demonstrating cursorMoveSelect after replacement
"findInCurrentFile": { // in settings.json
"addClassToElement": {
"title": "Add Class to Html Element",
"find": ">",
"replace": " class=\"@\">",
"isRegex": true,
"restrictFind": "onceExcludeCurrentWord",
"cursorMoveSelect": "@" // select the next '@'
}
}
{ // a keybinding for the above setting
"key": "alt+q", // whatever you want
// should get intellisense for available settings commands after typing `findInCurrentFile.`
"command": "findInCurrentFile.addClassToElement"
// "when": "" // can be used here
// "args": {} // will be ignored, the args in the settings rule
}

Explanation for above: Find the first > within selection(s) and replace them with class=\"@\">. Then move the cursor(s) to @ and select it. cursorMoveSelect value can be any text, even the regexp delimiters ^ and $.
"restrictFind": "onceExcludeCurrentWord"=> find the FIRST instance of thefindquery AFTER the cursor (so if your cursor is in the middle of a word, only part of that word is after the cursor), replace it and then go to and select thecursorMoveSelectvalue if any. Works the same for multiple cursors."restrictFind": "onceIncludeCurrentWord"=> find the FIRST instance of thefindquery from the BEGINNING of the current word (so if your cursor is in the middle of a word, that entire word will be searched), replace it and then go to and select thecursorMoveSelectvalue if any. Works the same for multiple cursors."restrictFind": "line"=> find all instances of thefindquery on the entire line with the cursor, replace them and then go to and select AllcursorMoveSelectvalues if any. Works on each line if multiple cursors. But it only considers the line where the cursor(s) is, so if there is a multi-line selection, only the line with the cursor is searched.
${matchNumber} and ${matchIndex}
These variables can be used in the replace and/or cursorMoveSelect positions. You cannot use them in find.
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "text$", // find lines that end with text
"replace": "text_${matchIndex}", // replace with 'text_' than match index 0-based
"isRegex": true,
"cursorMoveSelect": "${matchIndex}" // now select each of those matchNumbers
// if you don't want the text to be selected, just right or left arrow to lose the selections
// but maintain all the multiple cursors.
}
}

Explanation for above: The match in this case is "text$" ('text' at the end of a line). The first instance of a match has matchNumber = 1 and that will be used in the replacement. ${matchIndex} is the same but 0-based.
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "(text)", // capture group 1
"replace": "\\U$1_${matchNumber}", // upcase group 1 and add _ then that match number
"isRegex": true,
"cursorMoveSelect": "${matchNumber}" // select the matchNumber part of each instance
}
}

reveal Options
The reveal argument to the findInCurrentFile command can take three options:
"reveal": "first"scroll the viewport to show the first find match in the document, if necessary."reveal": "next"scroll the viewport to show the next find match in the document after the cursor, if necessary."reveal": "last "scroll the viewport to show the last find match in the document, if necessary.
If you do not want the editor to scroll to reveal any find match, simply do not include a reveal option at all. Certain other arguments like "restrictFind": "nextMoveCursor/previousMoveCursor/previousSelect/nextSelect/nextDontMoveCursor/previousDontMoveCursor" etc. will always scroll to reveal, even if there is no reveal argument.
- Note: The
revealargument will do nothing if you have acursorMoveSelectargument in your keybinding or setting.cursorMoveSelectwill take precedence.
Using the ignoreWhiteSpace argument
The ignoreWhiteSpace argument, a boolean, will change the find value so that any whitespace in the find will be treated as if it is \s*. And the find regex will otherwise be modified so that you do not need to explicity specify a \n character to get newlines to be recognized. In other words, any whitespace characters in the find value will result in the find regex working across lines. With these arguments:
"find": "someWord-A someWord-B",
"ignoreWhiteSpace": true,
"isRegex": true
text like these will be matched:
someWord-A someWord-B
someWord-A
someWorb-B
So it will match any consecutive 'someWord-A' and 'someWord-B' as long as there is only some kind of whitespace between them, be that spaces, tabs, newlines, etc.
And the ignoreWhiteSpace argument can be used in a search across files too.
Using the preserveSelections argument
This is a boolean option - default is false.
Normally, all find matches are selected, thus losing any cursor positions or other selections that might have existed before running the command. This does allow other options like replace or run or even postCommands to use these find matches, i.e., selections, in many interesting ways.
But, you may not need that functionality (which is the default). Perhaps you are doing a find and replace with no need to examine the find matches at all and so wish to preserve all existing selections and cursor positions. If so, set "preserveSelections: true. Although, one nice advantage of selecting the find matches is to make it more obvious where changes actually occurred in the document.
For certain options, preserveSelections has no effect. For instance, if you have a find and no replace (or no find and no replace) then the find matches will be selected regardless of the preserveSelections setting. If you use the cursorMoveSelect argument then naturally any of its matches will be selected. If you are using one of the next/previous options, then preserveSelections has no effect as those options call for a new selection or already prevent the cursor from moving.
Note: Regex lookbehinds that are not fixed-length (also called fixed-width sometimes), like
(?<=^Art[\w]*)are not supported in the Search Panel. But non-fixed-length lookbehinds are supported in vscode's Find in a file (as in using the Find widget) so they can be used infindInCurrentFilesettings or keybindings.
This works:
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "(?<=^Art[\\w]*)\\d+", // not fixed-length, but okay in findInCurrentFile
"replace": "###",
"isRegex": true
}
}
but the same keybinding in runInSearchPanel will error and not produce any results:
{
"key": "alt+y",
"command": "runInSearchPanel", // runInSearchPanel
"args": {
"find": "(?<=^Art[\\w]*)\\d+", // not fixed-length: ERROR will not run
"replace": "###",
"isRegex": true
}
}
The above command will put (?<=^Art[\w]*)\d+ into the Search Panel find input and ### into the replace but will error when actually triggered.
Matching empty lines
You can match all empty lines in the document with this keybinding:
{
"key": "alt+l",
"command": "findInCurrentFile",
"args": {
"replace": "Found empty line. Match number: ${matchNumber}" // replace is optional
}
}
And put your cursor on any empty line. They will be matched and replaced. If you don't have a replace, then all the empty lines will have a cursor placed on them. This only works when searching the entire document.
With the following keybinding, you can easily go to the next matching word (note there is no find). So if your cursor starts on an empty line, it will match the next empty line. If your cursor started on a word, then the cursor would go to that word.
{
"key": "alt+l",
"command": "findInCurrentFile",
"args": {
// also works with the nextMoveCursor or nextDontMoveCursor or previousMoveCursor or previousDOntMoveCursor
"restrictFind": "nextSelect", // or previousSelect, etc.
"replace": "Found empty line." // replace is optional
// but moving to a replace will change the 'word at cursor' and thus the find!!
// so if you are using a replace, 'nextDontMoveCursor' may be the better choice so the cursor
// stays on the blank line (after replacement)
}
}
And, if you want to put a cursor on all empty lines within your selections, use this keybinding:
{
"key": "alt+y",
"command": "findInCurrentFile",
"args": {
"find": "^$",
"isRegex": true,
"restrictFind": "selections"
}
}
If you want to find two consecutive empty lines use (^$)\n(^$). For three empty lines use (^$)\n(^$)\n(^$).
TODO
- Add more error messages, e.g., if a capture group used in replace but none in the find.
- Internally modify
replacekey name to avoidstring.replaceworkarounds. - Explore adding a command
setCategorysetting. Separate category for Search Panel commands? - Support the
preserveCaseoption infindInCurrentFile. - Check
cursorMoveSelectand${TM_CURRENT_LINE}interaction. - Deal with redundant "Extensions have been modified on disk. Please reload..." notification.
- Move
preCommandsto the script file?
Release Notes
See CHANGELOG for notes on prior releases.
- 6.0.0 Added script files for
replaceandrun: "replace":"$${script:math_with_numbers}$$"
- extensive test coverage
- clarified using$1, etc. infind.
6.1.0 - addedonceAlwaysfor runWHen and runPostCommands.
6.1.0 - fixed selection positions when text added/removed of a different length.
6.1.0 - addedkebabcasetransform support:${1:/kebabcase}.
6.1.1 - fixed capture groups in conditional - no backticks.

