Problem: Refactoring 200 Files by Hand Is Burning Your Afternoon
You renamed a core interface. Now UserData is UserProfile — and it appears in 47 files, sometimes as a type, sometimes as a prop, sometimes embedded in JSDoc. Find-and-replace breaks context. Doing it file-by-file takes hours.
Cursor's multi-cursor editing combined with AI gives you a third option: describe the transformation once and apply it intelligently across your entire codebase.
You'll learn:
- How to combine multi-cursor selection with AI inline edits
- Four production-grade bulk refactoring patterns (rename, type migration, API shape change, comment sync)
- When to use Cmd+Shift+L vs Cursor's Composer vs inline AI edit
Time: 20 min | Difficulty: Intermediate
Why Cursor's Approach Is Different
Standard find-and-replace is text-blind. It doesn't know whether UserData is a type annotation, a variable name, or part of a string literal. It can't update the JSDoc comment that describes the field, and it won't rename a destructured alias.
Cursor solves this with two primitives that compound each other:
Multi-cursor selection — select every instance of a pattern across a file or across files simultaneously.
AI inline edit (Cmd+K) — apply a natural-language transformation to every selected region at once, with context from surrounding code.
Used together, you get semantically-aware bulk edits that would take an hour in a normal editor in under two minutes.
The Four Patterns
Pattern 1: Rename Across Context
Use when: a type, interface, or function name changes and you need the rename to understand context (generics, props, destructuring).
Step 1: Select All Occurrences in the File
Place your cursor on UserData. Press Cmd+Shift+L (macOS) or Ctrl+Shift+L (Windows/Linux) to select every occurrence in the current file.
Cmd+Shift+L → selects all occurrences of word under cursor
You now have multi-cursors on every instance simultaneously.
Step 2: Apply AI Inline Edit
With all occurrences selected, press Cmd+K to open the inline AI prompt. Type:
Rename to UserProfile. If this is a type annotation, update generics too.
If it appears in a JSDoc @param or @returns, update that line as well.
Cursor applies the rename with awareness of each cursor's surrounding context — a UserData[] becomes UserProfile[], a Record<string, UserData> becomes Record<string, UserProfile>.
Step 3: Scale to the Full Repo with Composer
Open Composer (Cmd+Shift+I), switch to Agent mode, and run:
Rename UserData to UserProfile across the entire codebase.
- Update type annotations, generics, and JSDoc comments
- Do NOT rename string literals or log messages that say "userData"
- Do NOT rename local variables named userData (only the type)
Agent mode reads every relevant file, stages the changes, and lets you review diffs before applying.
Expected output: A file-by-file diff showing only type-level renames, not string literal replacements.
Pattern 2: Type Migration (e.g., any → Specific Type)
Use when: you're tightening types after a prototype phase and need to replace any with real types based on how values are actually used.
Step 1: Find All any Annotations in a File
Use Cmd+F to search for : any in the file. Then press Alt+Enter to place a cursor on every match.
Step 2: Inline AI Edit Per Match
With multi-cursors on every : any, press Cmd+K:
Infer the correct TypeScript type from how this variable is used in the function body.
Use the most specific type possible. If unsure, use unknown instead of any.
Cursor reads the surrounding code for each cursor position independently and infers types from usage — response.data.items.map(...) becomes { items: unknown[] }, for example.
Step 3: Validate
# Confirm no new type errors introduced
npx tsc --noEmit
If it fails:
Type 'X' is not assignable to type 'Y'→ Cursor over-inferred. PressCmd+Kon that line:Widen this type to accept both X and Y using a union.- Hundreds of errors → The
anyvalues were load-bearing. Run Composer:Add explicit type assertions where needed to preserve runtime behavior while removing any.
Pattern 3: API Shape Change
Use when: a function's signature changed — new required param, renamed field in a return object, callback → promise.
This is the pattern most likely to introduce subtle bugs with plain find-and-replace.
Step 1: Define the Change in a Composer Prompt
Open Composer in Agent mode. Be explicit:
The fetchUser function signature changed from:
fetchUser(id: string): Promise<{ name: string; email: string }>
To:
fetchUser(id: string, options?: { includeProfile: boolean }): Promise<UserProfile>
Update every call site to:
1. Add options parameter only where the caller previously accessed response.profile
2. Destructure name and email from UserProfile shape (they're still present)
3. Do NOT add the options param where it's not needed
The specificity here matters. Vague prompts like "update fetchUser calls" produce inconsistent results. Define the before/after contract exactly.
Step 2: Review by File
Composer will stage diffs file-by-file. Review each one. If a file has an incorrect change, click into that file and press Cmd+K inline:
This call site doesn't need the options parameter. Remove it and keep the rest.
Step 3: Run Tests
# Run your test suite before committing
npm test -- --passWithNoTests
Any call site the AI got wrong will surface as a failing test rather than a runtime surprise.
Pattern 4: Comment and Documentation Sync
Use when: you've already refactored the code but JSDoc, inline comments, and README snippets are now stale.
This is the most underused pattern. Stale comments are a hidden maintenance cost.
Step 1: Select All Comment Blocks
In a refactored file, use the multi-cursor shortcut to select every /** block:
Cmd+F → search for /**
Alt+Enter → cursor on every match
Cmd+Shift+L → extend to end of comment block
Step 2: AI Sync
Rewrite each JSDoc comment to accurately describe the current function signature and behavior.
Remove any @param entries for parameters that no longer exist.
Add @param entries for any new parameters not yet documented.
Keep the @returns description but update the type to match the current return type.
Step 3: Sync README Code Snippets with Composer
In README.md, find every code block that calls fetchUser.
Update them to match the new signature. Keep the surrounding explanation text unchanged.
Keyboard Reference
| Action | macOS | Windows / Linux |
|---|---|---|
| Select all occurrences in file | Cmd+Shift+L | Ctrl+Shift+L |
| Add next occurrence to selection | Cmd+D | Ctrl+D |
| Skip current, add next | Cmd+K Cmd+D | Ctrl+K Ctrl+D |
| Multi-cursor from search results | Cmd+F → Alt+Enter | Ctrl+F → Alt+Enter |
| Inline AI edit on selection | Cmd+K | Ctrl+K |
| Open Composer | Cmd+Shift+I | Ctrl+Shift+I |
| Toggle Agent mode in Composer | Click "Agent" in Composer bar | Same |
When to Use Each Tool
| Situation | Best tool |
|---|---|
| Same transformation in one file | Multi-cursor + Cmd+K |
| Same transformation across 2–10 files | Composer chat mode |
| Complex semantic change across entire repo | Composer Agent mode |
| Rename with full symbol awareness | LSP rename (F2) first, then AI for comments |
| Exploratory refactor (not sure what to change) | Composer chat — ask first, apply second |
One rule: if a change would take fewer than 5 manual edits, just do it manually. The overhead of prompting and reviewing AI output isn't worth it below that threshold.
Verification
After any bulk refactor, run this sequence before committing:
# 1. Type check
npx tsc --noEmit
# 2. Lint
npx eslint . --max-warnings 0
# 3. Tests
npm test
# 4. Quick grep to confirm old name is gone (adjust for your rename)
grep -r "UserData" src/ --include="*.ts" --include="*.tsx"
You should see: zero TypeScript errors, zero lint warnings related to the refactor, all tests passing, and no grep results for the old identifier (in type positions).
Real-World Time Comparison
Renaming UserData → UserProfile across a 40-file TypeScript codebase including types, generics, JSDoc, and README:
| Method | Time | Error rate |
|---|---|---|
| Manual, file by file | ~90 min | ~5% miss rate |
| Find-and-replace | ~15 min | Renames string literals, misses aliases |
| LSP rename only | ~2 min | Misses comments and README |
| Multi-cursor + Composer Agent | ~8 min | Near zero with test coverage |
The 8 minutes includes review time. Without tests, you pay for any errors later — that's true of any method.
What You Learned
Cmd+Shift+L+Cmd+Kis the fastest path for single-file bulk edits- Composer Agent handles cross-file semantic changes — but you need to describe the before/after contract precisely
- Comment sync is a first-class refactoring step, not an afterthought
- Always validate with
tsc --noEmit+ tests; AI edits are fast but not infallible
Limitation: Composer Agent mode works best in repos under ~50k lines. For monorepos, scope the agent to a specific package directory rather than running it at the root.
Tested on Cursor 0.45, TypeScript 5.7, macOS Sequoia and Ubuntu 24.04