Why this matters
A project has many files. A name you want to change may appear in twenty places, and a few of those places may not mean what you think. A beginner who opens one file, renames a word, and moves on will miss some uses and break others.
Project-wide search lets you see every use of a word before you change anything. Filters let you limit the search to the files that matter. A replace that uses a pattern can change many places in one step, and the replace preview lets you check each change first.
This lesson covers the search view, the include and exclude boxes, regular expressions in search, and a safe way to rename across files. The single-symbol rename with F2 and the find box for one file are in the lesson on extensions and shortcuts. This lesson is about the whole project.
The examples and their output
The examples use a small project. Its files are shown below:
project/
.gitignore
README.md
src/
app.js
user.js
api/
users.js
test/
user.test.js
dist/
bundle.js
node_modules/
lib/
index.jsThe dist folder holds a built copy of the code, and node_modules holds installed libraries. Neither is code you edit by hand.
The output lines below come from ripgrep, a command-line search program. VS Code uses ripgrep for its project search, so the matches it shows for the same patterns should be the same, apart from the default excludes described below. The version used here is ripgrep 15.1.0. Each output line has the form file:line:text, where line is the line number. To check a pattern yourself, type it in the search view and compare the list with the output below.
The regular expression output comes from Node.js, a program that runs JavaScript outside the browser, version 24.20.0. Node follows the same regular expression rules as the search box for the patterns in this lesson, so you can test a pattern in a terminal before you type it into the editor.
Open the search view
Press Ctrl+Shift+F on Windows or Linux, or Cmd+Shift+F on macOS. A search view opens in the side bar. It has a search box at the top and a replace box below it, which you open with the small arrow beside the search box.
Type a word in the search box. Results appear below it, grouped by file. Each file name has a count, and each result row shows a line number and the text of that line. Click a result to open the file at that line.
The search also reads the whole project, including files you have not opened. By default, VS Code leaves out folders such as node_modules and some files listed in .gitignore. That default is useful, but it can hide a match you wanted to see. Check the exclude settings if a result you expect is missing.
Here is what a plain search for getUser finds in the example project:
$ rg -n getUser .
./test/user.test.js:1:const { getUser } = require("../src/user");
./test/user.test.js:3:console.log(getUser(1));
./test/user.test.js:4:// getUser is used in the old API docs
./README.md:2:Call getUser(id) to load one user.
./dist/bundle.js:1:console.log("built getUser");
./src/api/users.js:1:const { getUser } = require("../user");
./src/api/users.js:4: return [getUser(1), getUser(2)];
./src/app.js:1:const { getUser } = require("./user");
./src/app.js:4: const u = getUser(req.id);
./src/user.js:1:function getUser(id) {
./src/user.js:2: console.log("getUser called with", id);
./src/user.js:6:function getUserName(id) {
./src/user.js:7: return getUser(id).name;
./src/user.js:10:module.exports = { getUser, getUserName };
./node_modules/lib/index.js:1:module.exports = function getUser() { return {}; };That is 15 matches in 7 files. Some are in the code you want to change. Others are a comment, a README, a built file, and a library copy that you should not change. The filters below remove the ones you do not want.
The line that starts with ./node_modules appears here because this example folder has no .git folder for ripgrep to read .gitignore from. The order of the lines can also differ from run to run.
Narrow the search with include and exclude
Click the ... button at the right of the search box to show two more boxes. The top one is files to include. The bottom one is files to exclude.
- Files to include limits the search to matching files. If you leave it empty, every file is searched.
- Files to exclude removes matching files from the search. It adds to the default excludes; it does not replace them.
Both boxes take patterns called globs. A glob is a short pattern for file paths, with wildcards. Three wildcards cover most cases:
*matches any number of characters, but it stops at a slash.*.jsmatches every file that ends in.jsin the folder where you start, and no file in a subfolder.**matches any number of folders, including none.src/**matches every file under thesrcfolder, at any depth.,separates several patterns in the same box.src/**, test/**means files undersrcor undertest.
In the exclude box, you write the pattern without a leading !. The examples below use -g on the command line. There, -g 'src/**' means include, and -g '!test/**' means exclude. The ! is how ripgrep writes an exclude.
Worked example 1: a name that appears only in the source
You want to find every call to getUser in the source code, and nothing else. Put src/** in the include box and search for getUser.
$ rg -n -g 'src/**' getUser .
./src/api/users.js:1:const { getUser } = require("../user");
./src/api/users.js:4: return [getUser(1), getUser(2)];
./src/user.js:1:function getUser(id) {
./src/user.js:2: console.log("getUser called with", id);
./src/user.js:6:function getUserName(id) {
./src/user.js:7: return getUser(id).name;
./src/user.js:10:module.exports = { getUser, getUserName };
./src/app.js:1:const { getUser } = require("./user");
./src/app.js:4: const u = getUser(req.id);The test file, the built file and the library copy are gone. The README is gone too, because it sits in the top folder, not in src. Some rows in this list are not calls. Line 1 of src/user.js is the definition, line 1 of src/app.js is an import, and line 2 of src/user.js is a log message that only contains the word. You will decide what to do with each one when you read the replace preview later.
Worked example 2: a pattern that stops at a folder
Suppose you only want the files directly inside src, not the ones in src/api. A pattern with one star, src/*.js, looks right. Here is what it finds:
$ rg -n -g 'src/*.js' getUser .
./src/user.js:1:function getUser(id) {
./src/user.js:2: console.log("getUser called with", id);
./src/user.js:6:function getUserName(id) {
./src/user.js:7: return getUser(id).name;
./src/user.js:10:module.exports = { getUser, getUserName };
./src/app.js:1:const { getUser } = require("./user");
./src/app.js:4: const u = getUser(req.id);The two src/api/users.js rows are gone. That is the rule for one star: it does not cross a slash. Use src/** when you want every subfolder, and src/*.js when you want only the files at that level.
Worked example 3: leave out tests and built files
You want to search the whole project except the tests and the dist and node_modules folders. Put this in the exclude box:
test/**, dist/**, node_modules/**On the command line, the same filter is three -g options:
$ rg -n -g '!test/**' -g '!dist/**' -g '!node_modules/**' -w getUser .
./README.md:2:Call getUser(id) to load one user.
./src/api/users.js:1:const { getUser } = require("../user");
./src/api/users.js:4: return [getUser(1), getUser(2)];
./src/app.js:1:const { getUser } = require("./user");
./src/app.js:4: const u = getUser(req.id);
./src/user.js:1:function getUser(id) {
./src/user.js:2: console.log("getUser called with", id);
./src/user.js:7: return getUser(id).name;
./src/user.js:10:module.exports = { getUser, getUserName };The -w option means whole word, so getUserName on line 6 of src/user.js is left out. The README is still listed, because the exclude patterns name folders, and the README is a file in the top folder. To leave it out too, add -g '!README.md'. Worked example 7 shows the same whole-word search written as a pattern.
Filters are for the search you are doing now. For patterns you want on every search, set them in the settings under search.exclude, as the settings section of the first lesson in this module describes.
Regular expressions in search
A regular expression, or regex, is a pattern that describes text. A regex can say "a word that starts with get, followed by a capital letter" without listing every possible word. In the search view, click the .* button to switch regex on. The lesson on extensions and shortcuts covers the find box and its buttons, so this section goes straight to patterns you will use in a project.
For the parts below, the search view matches the same text as JavaScript regular expressions. Lookahead is the exception, covered in example 8. Here are the parts that matter most for a beginner:
.matches any one character. To match a real dot, write\..\wmatches a letter, a digit or an underscore.\bmatches the edge of a word. It is zero characters wide, so it marks a boundary without matching a character.[A-Z]matches one capital letter.+means one or more of the item before it.*means zero or more.(and)group part of a pattern and also save what it matched, so a replace can use it.
Worked example 4: find every function name
Search for function, followed by a name, followed by an open bracket. The pattern function (\w+)\( does this. The brackets around \w+ save the name so that the replace can use it later.
$ rg -n -g 'src/**' 'function (\w+)\(' .
./src/api/users.js:3:function listUsers() {
./src/app.js:3:function handle(req) {
./src/user.js:1:function getUser(id) {
./src/user.js:6:function getUserName(id) {If you run the same pattern in Node from the folder that contains project, you can print only the saved names:
const fs = require("fs");
const text = fs.readFileSync("project/src/user.js", "utf8");
console.log([...text.matchAll(/function (\w+)\(/g)].map((m) => m[1]));[ 'getUser', 'getUserName' ]The g at the end of the pattern means every match in the file, not just the first. The search view always finds every match, so you do not need the g there.
Worked example 5: find calls to functions that start with get
A word-start mark and a capital letter let you find every function whose name starts with get followed by a capital letter:
\bget[A-Z]\w*\($ rg -n -g 'src/**' '\bget[A-Z]\w*\(' .
./src/app.js:4: const u = getUser(req.id);
./src/api/users.js:4: return [getUser(1), getUser(2)];
./src/user.js:1:function getUser(id) {
./src/user.js:6:function getUserName(id) {
./src/user.js:7: return getUser(id).name;The \b at the start makes sure the match begins at a word edge, so forgetUser( would not match. The ( at the end means the name is a function call or a function definition, not a plain word.
The replace preview
Click the arrow beside the search box to open the replace box. Type the new text there. Before you click anything, read the results. Each row shows a line from a file. When you type text in the replace box, the rows also show what the line will look like after the change, so you can compare the old and new text.
Two things to check in the preview:
- Does every row match the change you meant? A row that changes a word you did not intend to change is a warning.
- Is every file one you want to change? A match in a built file or a library copy should be removed from the list, not replaced.
The results list lets you remove one match, so that a later replace skips it. When you hover over a row, a remove control appears. Use it for the rows you do not want.
If a match is right but you want a different new text for it, you can replace that single match with its own button rather than replacing everything at once.
Worked example 6: a replace that uses a captured name
The pattern and replace below change function definitions in src/user.js so that each name gets the suffix Item. Put src/user.js in the include box so that the replace touches no other file. The replace text $1 stands for whatever the first saved group matched.
Search: function (\w+)\(
Replace: function $1Item(
In the Node check, the same replace runs on that line's text:
console.log("function getUser(id) {".replace(/function (\w+)\(/, "function $1Item("));function getUserItem(id) {The $1 works only when regex is on in the search view. With regex off, the search box looks for the text function (\w+)\( as plain text, so nothing matches. That is a common mistake.
A safe rename across files
Renaming a function, a variable or a file name across a project is a common task. The whole-word match and the preview are the two tools that make it safe. A rename has five steps:
- Search for the exact word with
\baround it, so thatgetUserNameis not changed when you renamegetUser. - Add include and exclude filters so the search covers the code you own and not the built files or libraries.
- Read the preview. Remove any row that should not change, such as a line in a README that describes the old name.
- Replace. Then search again for the old name. A correct rename leaves no matches in the places you chose to change.
- Run the tests or the program. A rename can work in one file and fail in a file you did not change.
Before a large rename, save your work with Git, a tool that records each version of your files, so that you can see every change with git diff. The Git module covers that command. If you have no Git repository, copy the folder first.
Worked example 7: rename getUser to fetchUser
The goal is to rename the function everywhere in src and test. The README, the built file and the library copy stay as they are for now. The first step is a search with a whole-word match and filters:
Search: \bgetUser\b
Include: src/**, test/**
Before any change, the preview of the rename is a list of rows like the ones below. A small script prints the same rows for the lines that match:
const fs = require("fs");
const files = ["project/src/user.js", "project/src/app.js", "project/src/api/users.js", "project/test/user.test.js"];
for (const file of files) {
fs.readFileSync(file, "utf8").split("\n").forEach((line, i) => {
if (/\bgetUser\b/.test(line)) {
console.log(`${file}:${i + 1}`);
console.log(" old: " + line);
console.log(" new: " + line.replace(/\bgetUser\b/g, "fetchUser"));
}
});
}Here is part of its output. The full output has the same pattern for every matching line.
project/src/user.js:1
old: function getUser(id) {
new: function fetchUser(id) {
project/src/user.js:2
old: console.log("getUser called with", id);
new: console.log("fetchUser called with", id);
project/src/user.js:7
old: return getUser(id).name;
new: return fetchUser(id).name;
project/src/user.js:10
old: module.exports = { getUser, getUserName };
new: module.exports = { fetchUser, getUserName };Two things stand out. The getUserName on line 10 is unchanged, because \b stops the match at the end of the word getUser. The text inside the console.log string also changed. That is a decision for you. For this rename it is fine, because the message describes the function. If the message had to stay the same, for example because a script searches for it, you would remove that row from the list.
Apply the change to the four files. The test then calls the new name:
$ node test/user.test.js
fetchUser called with 1
{ id: 1, name: 'user1' }After the rename, search for the old name again:
$ rg -n -w getUser .
./dist/bundle.js:1:console.log("built getUser");
./README.md:2:Call getUser(id) to load one user.
./node_modules/lib/index.js:1:module.exports = function getUser() { return {}; };The three remaining rows are the ones you left out on purpose. Each one is a decision: the built file is rebuilt by a command, the library copy belongs to a package, and the README is documentation that you may want to update by hand.
Worked example 8: change the name only where a bracket follows it
Some rows that a whole-word search finds should not change. Line 10 of src/user.js is an export list, where a name is followed by a comma or a brace, not by a call. A pattern can require a bracket after the name:
getUser(?=\()The (?= part is a lookahead. It checks that the text after the name starts with an open bracket, but it does not include that bracket in the match. The check is done in the same step as the match, so the replace does not touch the bracket.
Run the pattern in Node over the same four files:
const fs = require("fs");
const files = ["project/src/user.js", "project/src/app.js", "project/src/api/users.js", "project/test/user.test.js"];
for (const file of files) {
fs.readFileSync(file, "utf8").split("\n").forEach((line, i) => {
if (/getUser(?=\()/.test(line)) console.log(`${file}:${i + 1}: ${line.trim()}`);
});
}project/src/user.js:1: function getUser(id) {
project/src/user.js:7: return getUser(id).name;
project/src/app.js:4: const u = getUser(req.id);
project/src/api/users.js:4: return [getUser(1), getUser(2)];
project/test/user.test.js:3: console.log(getUser(1));The export line is not in the list, and it stays unchanged after the replace. The definition on line 1 and the calls match, which is what a rename needs. Lookahead is part of JavaScript's regular expression rules. ripgrep's default mode rejects it with an error, so rg needs the --pcre2 flag for this pattern. In the search view, turn on the search.usePCRE2 setting first, or use the Node check.
Common mistakes
Replacing without reading the list
The most common mistake is clicking replace all before reading the rows. A pattern that looks right can match more than you expected. For example, replacing user with account also changes username, users and userId. Read the rows, and use \b to match whole words.
A star that stops at a folder
A pattern like src/*.js finds files directly inside src, and nothing in src/api. If a file you expected is missing from the results, check whether it is in a subfolder. Use src/** to include subfolders.
An unescaped bracket
With regex on, a bracket has a special meaning. If you search for getUser( without escaping it, the search view reports an error instead of finding the text. In Node, the same mistake gives this message:
new RegExp("getUser(");SyntaxError: Invalid regular expression: /getUser(/: Unterminated groupThe message means the pattern opens a group with ( and never closes it. To search for a real bracket, write \(. Turning regex off also works when you want plain text, because then the characters are matched exactly.
A dot that matches anything
With regex on, . matches any one character. A search for v1.2 also matches v1x2, v1-2 and v1 2. In the example project, the only real version text is in src/app.js, so the escaped pattern finds just that line:
$ rg -n 'v1\.2' .
./src/app.js:5: console.log("version v1.2 ok");Node shows the same difference:
console.log(/v1.2/.test("v1x2"), /v1\.2/.test("v1x2"));true falseRenaming the source but not the callers
If you rename a function in src/user.js but not in the files that import it, the other files break. A run of the test shows the mistake. This is the output after the rename was applied to src but not to test:
.../test/user.test.js:3
console.log(getUser(1));
^
TypeError: getUser is not a function
at Object.<anonymous> (.../test/user.test.js:3:13)The message getUser is not a function means the name the test asked for does not exist in the file it imported. The fix is to search for the old name, check every match, and rename the callers too. The search in the rename steps is how you find them.
Forgetting a folder outside the filter
A filter that includes only src/** leaves out every other folder. A rename then misses code in a scripts or docs folder that calls the same function. Before you rename, list the top-level folders in the project and decide which ones to include.
Practice
Exercise 1. Find every console.log call in the src folder only. Use the include box or the -g option.
Show the answer
$ rg -n -g 'src/**' 'console\.log' .
./src/app.js:5: console.log("version v1.2 ok");
./src/user.js:2: console.log("getUser called with", id);The include filter removes the console.log in test/user.test.js and the one in dist/bundle.js.
Exercise 2. Write a regex that finds calls to functions whose names start with get followed by a capital letter, and make sure the match cannot start in the middle of a word such as forgetUser.
Show the answer
\bget[A-Z]\w*\(The \b marks the start of a word. The [A-Z] requires a capital letter after get. The \w* accepts any letters and digits after that, and the \( requires an open bracket after the name.
Exercise 3. Find the text v1.2 and nothing else, with regex on.
Show the answer
v1\.2The backslash turns the dot into a literal dot. Without it, v1x2 would also match.
Exercise 4. You want to rename getUser to fetchUser in the src and test folders. A plain search for getUser with the replace box set to fetchUser would also change getUserName. Write the search pattern that avoids that, and say which folders the include box should contain.
Show the answer
Search pattern: \bgetUser\b
Include: src/**, test/**
The \b at both ends means the match must be the whole word. The include box limits the change to the two folders, so the built file in dist and the library copy in node_modules are not changed.
Exercise 5. A teammate renamed getUser in src/user.js and src/app.js, and then ran the tests. The test file fails with TypeError: getUser is not a function. Explain what the rename missed and what the search for the old name would show.
Show the answer
The rename missed test/user.test.js and src/api/users.js. That file still imports getUser from src/user.js, but the module now exports fetchUser, so the imported name is undefined and calling it fails. A search for the whole word getUser in src and test would show the rows still using the old name, in src/api/users.js and in the test file. Running the search before the test would have shown the same rows.
Summary
- The search view (
Ctrl+Shift+F, orCmd+Shift+Fon macOS) searches every file in the project, and each result shows its file and line. - The include and exclude boxes take glob patterns:
*stops at a slash,**crosses folders, and a comma separates patterns. - With regex on,
\bmatches a word edge,\.matches a real dot, and parentheses save a match for$1in the replace text. - Read the replace preview, remove the rows you do not want, and then replace.
- A safe rename searches for the whole word, limits the folders, checks the preview, searches for the old name again, and runs the tests.