Why Tab is different from the autocomplete you know
In VS Code, autocomplete finishes the word you are typing. Cursor's Tab tries to guess your next edit. That edit might be several lines long, it might change code you already wrote rather than add new code, and it might sit somewhere other than where your cursor is now. In practice it does three things:
- Multi-line edits. It suggests changes across a block. It can also add a missing import or make matching changes in related code.
- Jumps inside the file. After you accept a suggestion, Tab may predict where you will edit next. Press Tab again and the editor moves you there.
- Edits in other files. If a change in one file means another file needs updating, a small preview window appears at the bottom of the editor.
A good way to think of it: Tab is a pair partner who has seen your last few keystrokes and says "you're about to do the same thing on line 40, aren't you?" Often they're right. Sometimes they're confidently wrong. The skill is telling which, in under a second.
The three keys you need
Suggestions show up as grey text ahead of your cursor (or as a highlighted diff when they change existing code). Then:
| You want to... | Windows / Linux | macOS |
|---|---|---|
| Accept the whole suggestion | Tab |
Tab |
| Accept the next word only | Ctrl + → |
Cmd + → |
| Reject it | Esc, or just keep typing |
Esc, or keep typing |
Two things catch people out:
- Partial accept takes over a key you already use. In VS Code,
Ctrl + →moves you one word along. While a suggestion is showing, it accepts a word of the suggestion instead. If that annoys you, remap it. Search forAccept Cursor Tab Suggestionsin Keyboard Shortcuts to change the full-accept key too. - You don't have to dismiss anything. If the suggestion is wrong, keep typing and it disappears.
Escis for when you want it gone now.
Walkthrough: a rename refactor in the practice repo
Open src/cart.ts in the practice repository. It has a function with an unhelpful parameter name:
// src/cart.ts (before)
export function total(xs: CartItem[]): number {
let sum = 0;
for (const x of xs) { // 'xs' is used again here
sum += x.price * x.qty;
}
return xs.length ? sum : 0; // ...and here
}
- Change
xstoitemsin the signature by hand. - Wait a moment. Tab should offer to change the other two
xsas well, either as one diff or one at a time with a jump. - Press
Tabto accept. If a jump marker appears, pressTabagain and you land on the next spot. - Now change
xtoitemin the loop header. This time accept only part of the suggestion. If Tab offersitem.price * item.qty;plus a comment you don't want, pressCtrl + →until you've taken the code, thenEsc.
// src/cart.ts (after)
export function total(items: CartItem[]): number {
let sum = 0;
for (const item of items) {
sum += item.price * item.qty;
}
return items.length ? sum : 0;
}
Check: run npm test (the repo has a cart.test.ts). It should pass exactly as before, because a rename shouldn't change behaviour.
Plausible but wrong: the suggestions to reject
Tab writes code that looks like your code, and that's exactly why a wrong suggestion slips past. Now add an average function under total and type the signature:
export function averagePrice(items: CartItem[]): number {
// Tab may suggest something like:
return items.reduce((a, i) => a + i.price, 0) / items.length;
}
It reads fine, but with an empty cart it returns NaN (0 / 0). Your total function handled the empty case, and this suggestion doesn't. Reject it and write the guard yourself, or accept it and fix it right away:
export function averagePrice(items: CartItem[]): number {
if (items.length === 0) return 0; // the edge case Tab skipped
return items.reduce((a, i) => a + i.price, 0) / items.length;
}
The other common failure is a made-up API: a method that sounds right but isn't real, such as items.sumBy('price') on a plain array, or an option your library version doesn't have. A quick test: would I bet a coffee this method exists? If not, reject it, or accept it and watch for a red squiggle from TypeScript. The type checker is your cheapest second opinion.
Rule of thumb from production use: accept freely when the edit is mechanical and repeats something you just did (renames, matching changes, imports). Slow down when it brings in new logic or new APIs. That's where it guesses.
Turning Tab down where it gets in the way
In some files Tab is mostly noise. Markdown, JSON and commit messages are the usual offenders. Click the Tab status indicator in the bottom-right corner of the window. It gives you:
- Snooze: turns Tab off for a length of time you choose, for example while you write a design doc.
- Disable globally: turns it off everywhere until you turn it back on.
- Disable for certain file types: turns it off for, say, Markdown, and leaves it on for everything else.
More options live under Cursor Settings > Tab.
Task: disable Tab for Markdown, open README.md and type a sentence. No grey text should appear. Now close and reopen Cursor, open the README again and check that suggestions are still off. Then open cart.ts and check they still work there.
If you want to see where the setting ended up, open your user settings.json. Recent builds store the per-language list under an internal key that looks like this:
{
// Language IDs where Tab is off. The key name is internal and may change.
"cursor.cpp.disabledLanguages": ["markdown"]
}
Gotcha: users have reported that this list doesn't apply to new unsaved files until you save them, so check the setting on a saved file.
Key takeaways
- Tab predicts your next edit: multi-line changes, jumps within the file, and changes that span files.
Tabaccepts all of it,Ctrl/Cmd + →accepts word by word, andEscor carrying on typing rejects it.- Trust it with mechanical, repeated edits. Look closely at new logic, edge cases and APIs you don't recognise.
- The type checker and your tests catch wrong suggestions cheaply. Run them after you accept.
- Use the status-bar indicator to snooze Tab or turn it off per file type, and check that the setting is still there after a restart.