Applied Coding & Editor Primitives
Medium-hard problems on real codebase slices
The shape of Cursor's coding questions
After this you can calibrate to applied, editor-flavored problems.
Cursor's technical screen is not LeetCode theater. You get one medium-hard problem and it usually sits on top of a real slice of the codebase rather than in a blank file.
The role builds the editor itself, so the screen probes whether you can reason about the structures an editor actually runs on. A Software Engineer, Product who has shipped real features recognizes these problems instantly; someone who crammed abstract algorithm puzzles the night before tends to flounder the moment the problem touches a file system or a text buffer.
Candidates have reported a consistent flavor of question. Treat these as the genre, not a leaked answer key.
- Reported question
- Build a hash tree to organize repository data
- Underlying skill
- Merkle-style hashing, recursion over a tree
- Editor connection
- Detecting changed files, deduping content at repo scale
- Reported question
- Find duplicate files in a file system
- Underlying skill
- Traversal, grouping, content hashing
- Editor connection
- Indexing a workspace without re-reading every byte
- Reported question
- Print the top view of a binary tree
- Underlying skill
- BFS with horizontal-distance tracking
- Editor connection
- Rendering a code outline or symbol tree
| Reported question | Underlying skill | Editor connection |
|---|---|---|
| Build a hash tree to organize repository data | Merkle-style hashing, recursion over a tree | Detecting changed files, deduping content at repo scale |
| Find duplicate files in a file system | Traversal, grouping, content hashing | Indexing a workspace without re-reading every byte |
| Print the top view of a binary tree | BFS with horizontal-distance tracking | Rendering a code outline or symbol tree |
Different prompts, but each maps to something the editor does for real.
The deeper pattern is that applied problems lean on AI-editor primitives. Interviewers have framed problems around multi-file edit coordination, context retrieval for LLM prompts and streaming edit application. You are not expected to have built Cursor, but you are expected to reason about these like an engineer who uses the product daily.
Apply a set of changes across files
Keep offsets correct as text shifts
Pick the right slices to feed a prompt
Rank, dedupe, fit a token budget
Apply partial model output as it arrives
Never corrupt the buffer mid-stream
What they actually gradeclean and correct beats clever
The bar is clean, correct, readable code with clear reasoning out loud. A working O(n log n) solution you can explain beats a half-finished O(n) trick you can't. Name your invariants, handle the empty and single-element cases before the interesting ones and say why you chose a Map over an object.
// Find duplicate files: group paths by identical content.
// Input: Array<{ path: string; bytes: Uint8Array }>
// Output: string[][] - each inner array is one duplicate group (size >= 2)
// Constraints: 100k files, files up to 100MB - cannot hash everything blindly.
function findDuplicates(files: FileEntry[]): string[][] { /* ... */ }Spend the first two minutes restating the problem as a typed signature and a constraint sentence. It pins down the contract, surfaces the scale assumption and signals you scope before you sprint - the exact instinct the role rewards.
Do not reach for a fancy data structure before you have the brute force working and named. Interviewers read a premature optimization as someone pattern-matching to a memorized solution rather than reasoning from the problem.
Takeaway. Expect one medium-hard, editor-flavored problem on a real codebase slice; they grade clean, correct, explained code over clever code.
Self-check
QCursor's technical screen tends to differ from a generic LeetCode interview in which way?
Text buffers and edit primitives
After this you can implement the data structures an editor lives on.
An editor is a machine for inserting and deleting characters in huge documents, fast. The naive answer - a single string you keep re-slicing - collapses the moment a file is large or edits are frequent.
Three representations come up. You rarely have to implement a rope from scratch in a 45-minute screen, but you must be able to name them, sketch one and reason about why an editor picks the one it does.
- Structure
- Flat string
- Insert / delete
- O(n) - copies the whole buffer
- Index / random read
- O(1)
- Why an editor uses it
- Fine for tiny files; dies at scale
- Structure
- Gap buffer
- Insert / delete
- O(1) near the cursor, O(n) to move the gap
- Index / random read
- O(1)
- Why an editor uses it
- Great for localized typing at one caret
- Structure
- Piece table
- Insert / delete
- O(1) amortized, never mutates original text
- Index / random read
- O(log n) with an index tree
- Why an editor uses it
- Cheap undo and clean append-only edit history
- Structure
- Rope
- Insert / delete
- O(log n)
- Index / random read
- O(log n)
- Why an editor uses it
- Balanced for big files and edits anywhere
| Structure | Insert / delete | Index / random read | Why an editor uses it |
|---|---|---|---|
| Flat string | O(n) - copies the whole buffer | O(1) | Fine for tiny files; dies at scale |
| Gap buffer | O(1) near the cursor, O(n) to move the gap | O(1) | Great for localized typing at one caret |
| Piece table | O(1) amortized, never mutates original text | O(log n) with an index tree | Cheap undo and clean append-only edit history |
| Rope | O(log n) | O(log n) | Balanced for big files and edits anywhere |
The tradeoff is always insert/delete cost versus random-access cost.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Every representation trades edit cost against random-access cost; the upper-right corner is what scales.
Applying a single editoffset + length + replacement
Model an edit as a range you cut and a string you splice in. The minimal shape is { offset, length, text }: delete length characters at offset, then insert text. Everything harder is built on this.
interface Edit {
offset: number; // 0-based start in the current buffer
length: number; // chars to delete (0 = pure insert)
text: string; // chars to insert ('' = pure delete)
}
function applyEdit(buf: string, e: Edit): string {
return buf.slice(0, e.offset) + e.text + buf.slice(e.offset + e.length);
}Composing many edits without corrupting positionswhere off-by-one bugs are born
The trap: every edit you apply shifts the offsets of every later edit. Apply edit A at offset 10 and an edit that used to target offset 50 now targets a different character. Two disciplines avoid the corruption.
- 1Resolve against one snapshot. Compute all edit offsets relative to the original buffer, never against the partially-mutated one.
- 2Apply right-to-left. Sort edits by descending offset and apply from the end of the file backward, so each edit lands before any unprocessed edit's position can shift.
function applyEdits(buf: string, edits: Edit[]): string {
const sorted = [...edits].sort((a, b) => b.offset - a.offset);
for (const e of sorted) {
if (e.offset < 0 || e.offset + e.length > buf.length) {
throw new Error(`edit out of range at ${e.offset}`);
}
buf = buf.slice(0, e.offset) + e.text + buf.slice(e.offset + e.length);
}
return buf;
}If two edits overlap, you have a conflict, not just a sort order. Detect it: after sorting by offset, if any edit's range intersects the next one's, reject the batch or merge deliberately. Saying this out loud, even if you don't code the merge, shows you've met this bug before.
Empty buffer with one insert. Pure delete at end-of-file. Two adjacent non-overlapping edits. One edit whose range runs off the end - does it throw or silently corrupt? Walk these in the interview; they're the cases that separate correct from almost-correct.
Takeaway. Edits are {offset, length, text}; resolve them against one snapshot and apply right-to-left so earlier offsets stay valid - that single discipline kills most off-by-one bugs.
Self-check
QYou must apply a batch of non-overlapping edits to a buffer. Why apply them sorted by descending offset?
Multi-file edits and diff coordination
After this you can coordinate changes across many files safely.
When an agent rewrites a function and updates its three call sites in other files, that is a multi-file edit. It must land as one unit or not at all.
Model the whole change as a map from file path to a list of in-file edits. The unit of work is the batch, not the individual edit, because a half-applied refactor leaves the repo broken.
type MultiFileEdit = Map<string, Edit[]>;
interface ApplyResult {
applied: Map<string, string>; // path -> new contents
conflicts: string[]; // paths that could not apply
}Apply atomically, report conflictsall-or-nothing across files
Compute every new file body first, in memory, before you write a single byte to disk. If any file fails - overlapping edits, a stale base, an out-of-range offset - you abort the whole batch and report which paths conflicted. The user sees one clean failure, not a repo in a torn state.
- 1Validate per file. Run each file's edits against its current contents in memory; collect failures.
- 2Gate on conflicts. If the conflict list is non-empty, return it and write nothing.
- 3Commit together. Only when every file validated, persist all of them in one pass.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Everything happens in memory until one gate passes; nothing touches disk before it.
Computing the diffline-level for review, char-level for precision
A diff is the minimal set of changes that turns the old text into the new. The textbook engine is a longest-common-subsequence (LCS) computation: find the longest run of unchanged lines and everything outside it is an insertion or deletion. Most production diffs run LCS at line granularity for a readable review, then refine to character granularity inside changed lines.
- Line-level
- Readable review UI, cheap; coarse on one-character changes
- Character-level
- Precise highlighting; noisier and more expensive
- Hybrid (line then char)
- What editors ship: line diff, char-refine changed regions
Know that LCS underlies the minimal diff; you rarely code full Myers in a screen.
Streaming edit applicationapply model output as it arrives
The model streams tokens, so you can't wait for the full edit before showing anything. The danger is applying a partial edit and corrupting the buffer when the next chunk arrives. The safe pattern keeps a stable base and replaces a pending region on each chunk.
- 1Anchor a region. Pick the offset range the stream is rewriting and treat it as a single replaceable span.
- 2Buffer the partial. Accumulate streamed text; render it into the anchored region for preview without committing.
- 3Commit on completion. When the stream ends, apply one final, validated edit and clear the pending state. If the stream errors, drop the pending region - the base buffer was never mutated.
If your edits are reified objects, undo/redo is free: keep a stack of applied edits and their inverses. The inverse of {offset, length, text} is {offset, length: text.length, text: deletedText}. Mention this when asked about undo - it shows the data model was designed for it, not bolted on.
Don't apply streamed chunks directly into the live buffer offset-by-offset. A retry, a reorder or a truncated final chunk will leave half an edit in place. Keep the pending region separate and commit once.
Takeaway. Model a multi-file change as path -> Edit[], validate everything in memory and commit all-or-nothing; for streaming, hold a pending region and commit once so a dropped chunk never corrupts the buffer.
Self-check
Trees, files and repo-scale structures
After this you can solve the file-system and tree problems they favor.
Cursor indexes whole repositories, so the file-system and tree problems are not arbitrary - they are simplified versions of work the editor does every time you open a project.
Find duplicate files at scalethe reported classic
Hashing every file's full contents is the obvious answer and the wrong first move at repo scale. The cheap optimization: two files can only be identical if they're the same size, so group by size first and hash only within groups that have more than one member. Most files are unique by size, so you skip hashing them entirely.
function findDuplicates(files: FileEntry[]): string[][] {
const bySize = new Map<number, FileEntry[]>();
for (const f of files) {
const g = bySize.get(f.bytes.length) ?? [];
g.push(f);
bySize.set(f.bytes.length, g);
}
const groups: string[][] = [];
for (const sameSize of bySize.values()) {
if (sameSize.length < 2) continue; // unique by size - skip hashing
const byHash = new Map<string, string[]>();
for (const f of sameSize) {
const h = hashContent(f.bytes); // sha-256 of bytes
(byHash.get(h) ?? byHash.set(h, []).get(h)!).push(f.path);
}
for (const paths of byHash.values()) {
if (paths.length >= 2) groups.push(paths);
}
}
return groups;
}Volunteer the next refinement before they ask: for large files, hash the first 4KB as a prefilter and only do a full content compare on prefix-collisions. It shows you think about IO cost, not just Big-O - exactly the repo-scale judgment the role wants.
Hash trees to detect changed filesMerkle-style structure
A hash tree (Merkle tree) hashes each file, then hashes each directory as a hash of its children's hashes, up to a single root hash. Two trees with the same root are identical. When something changes, only the path from that file to the root changes, so you find the changed subtree in O(log n) of the directory depth instead of rescanning everything.
type FsNode =
| { kind: 'file'; name: string; bytes: Uint8Array }
| { kind: 'dir'; name: string; children: FsNode[] };
function merkleHash(node: FsNode): string {
if (node.kind === 'file') return hashContent(node.bytes);
const childHashes = node.children
.map((c) => `${c.name}:${merkleHash(c)}`)
.sort(); // stable order -> stable hash
return hashContent(new TextEncoder().encode(childHashes.join('|')));
}Sort children before hashing a directory. If the hash depends on file-system enumeration order, two identical directories can produce different roots and your change detection breaks. This is the bug interviewers wait to see if you spot.
Tree traversals as outline problemstop view, level order
Reframe these the way the editor uses them: a code outline is a tree and rendering it is a traversal. The top view of a binary tree - the nodes visible from above - is a BFS where you track each node's horizontal distance from the root and keep the first node seen at each distance.
interface TreeNode { val: number; left?: TreeNode; right?: TreeNode; }
function topView(root?: TreeNode): number[] {
if (!root) return [];
const firstAtHd = new Map<number, number>(); // horizontalDist -> value
const queue: Array<{ node: TreeNode; hd: number }> = [{ node: root, hd: 0 }];
let minHd = 0, maxHd = 0;
while (queue.length) {
const { node, hd } = queue.shift()!;
if (!firstAtHd.has(hd)) firstAtHd.set(hd, node.val); // BFS => topmost first
minHd = Math.min(minHd, hd);
maxHd = Math.max(maxHd, hd);
if (node.left) queue.push({ node: node.left, hd: hd - 1 });
if (node.right) queue.push({ node: node.right, hd: hd + 1 });
}
const out: number[] = [];
for (let hd = minHd; hd <= maxHd; hd++) out.push(firstAtHd.get(hd)!);
return out;
}A DFS can reach a node at a given horizontal distance before a shallower node at the same distance, giving you the wrong answer. BFS guarantees you see the topmost node at each distance first. State this reason aloud - choosing the traversal because of the invariant is the signal.
Takeaway. Group by size before you hash to find duplicates, hash directories from sorted child hashes for Merkle change-detection and use BFS (not DFS) for top view so the topmost node at each horizontal distance wins.
Self-check
QWhen finding duplicate files across a 100k-file repo, why group by file size before hashing contents?
Coding with AI - the judgment test
After this you can use AI in-round the way they reward.
Cursor's loop has two coding modes. First technical screens allow only autocomplete - no AI assistance beyond that. The 8-hour onsite lets you use AI tools and even Slack questions. The same person should be strong in both.
When AI is allowed, the interviewer is not grading the model's output. They're grading your judgment about it. The whole point is that you can tell good suggestions from bad ones and act on the difference visibly.
Narrate intent before you prompt
Accept a good suggestion and say why
Reject a bad one out loud and fix it
Use AI for syntax and API lookups
Pasting model output you can't defend
Outsourcing the design to the model
Going silent and accepting blindly
Not noticing an obvious wrong suggestion
Pasting raw model output you can't explain is an explicit rejection trigger. The interviewer will ask "why does this line work?" - if you can't answer, the AI wrote it, not you. Read every suggestion before you accept it and be ready to defend each line as your own.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Same model, same prompt - the difference the panel grades is your judgment over it.
Use AI for lookup, not for designwhere the line sits
The healthy use is targeted: "what's the signature of Array.prototype.flatMap," or "generate the boilerplate for this test harness." The unhealthy use is handing the model the whole problem and steering by vibes. Keep the architecture, the data model and the tradeoffs in your own head; let AI handle the parts you already know but don't want to type.
- Reach for AI to recall an API, scaffold a test or convert a snippet - things you could do yourself, just faster.
- Keep the design decisions, complexity reasoning and edge-case handling explicitly yours, spoken aloud as you go.
- When a suggestion is wrong, say what's wrong and correct it; a visible rejection is worth more than a silent accept.
The authenticity testthey can tell within minutes
Cursor interviewers can tell within minutes whether you actually use Cursor daily for real work or skimmed the docs to prep. The way you drive AI in-round is part of that signal: fluent, opinionated use reads as authentic; hesitant or over-reliant use reads as someone performing familiarity. The fix isn't a script. Use the product on real work for weeks before the loop.
“I'll let autocomplete fill the loop body, but I want to design the dedup grouping myself first - here's my plan. If a suggestion doesn't match the invariant I just stated, I'll throw it out and explain why.”
Rehearse cold coding too. The first screens are autocomplete-only, so practice writing a clean text-buffer or duplicate-files solution start to finish with no chat assistant. If you can only solve with AI doing the thinking, the no-AI screen will expose it fast.
Strong driving, concretelythe moves the panel can see
"Use AI well" is too vague to rehearse. The people who actually use Cursor daily drive it with four habits that read as authentic on a shared screen. Practice each until it's reflex, because the interviewer is watching how you operate the tool, not just whether the tests pass.
State the plan before you let AI write a line
The flagship loop: plan with the strongest reasoning model, hand the to-dos to a cheaper, faster model to implement
Letting the expensive reasoning model write all your code burns the budget; you don't need it once the plan exists
Tag the precise file instead of asking vaguely and making the tool hunt the whole repo
@-mentioning guarantees the file lands in context rather than relying on semantic search
Night-and-day difference in both speed and accuracy of the output
"Make it better" on the most expensive model is the canonical anti-pattern
With no clear context the model makes blind edits, then burns more tokens reading and fixing them
Scope it: name the surface, the exact change and the constraints
Model output ships with generic names, verbose comments and needless abstraction
Cursor engineers run a de-slop pass at generation time, before code reaches a PR
Clean it to the standard you'd defend, then accept - never paste raw
The vague-vs-scoped contrast is the one to internalize, because it shows up verbatim in how engineers talk about driving the tool. A prompt like "make the app better" gives the model nothing to anchor on, so it edits at random and the cost compounds: blind edits, then reading and re-fixing the slop it just wrote. The scoped version names everything the model needs.
// WEAK: no surface, no change, no constraint - blind edits, compounding cost. make the app better // STRONG: surface + exact change + constraints, on a model matched to the task. Under the shop page, add an in-stock filter next to the existing tag and sort controls. Preserve the URL query params and update tests if needed.
“I'll plan this with the reasoning model first, then hand the to-dos to a faster model to implement - no point spending the expensive one on boilerplate. I'll @-mention the exact file so it doesn't have to guess, and I'll run a quick de-slop pass on whatever it writes before I accept it.”
Takeaway. When AI is allowed, judgment is the signal: narrate intent, defend every accepted line and visibly reject bad suggestions - and rehearse cold, because first screens are autocomplete-only. Strong driving is concrete: plan-then-execute, @-mention the exact file, scope the prompt (never "make it better") and de-slop the output before you accept.
Self-check
QIn an AI-allowed Cursor coding round, what behavior is an explicit rejection trigger?
TypeScript fluency under time pressure
After this you can remove language friction in the editor's primary language.
TypeScript is the language the editor surface is written in, so fluency is table stakes. Fumbling syntax burns the minutes you need for reasoning and it reads as someone who doesn't write TS daily.
Be reflexively fast with the few constructs editor code leans on. Not exotic type gymnastics - the everyday tools, used cleanly.
- Construct
- Map / Set
- Reach for it when
- Grouping, dedup, counting, adjacency
- One-liner you should know cold
- (m.get(k) ?? []).push(v) for grouped lists
- Construct
- Discriminated union
- Reach for it when
- Modeling a node or result with variants
- One-liner you should know cold
- switch (n.kind) with exhaustive checks
- Construct
- Generics
- Reach for it when
- A reusable helper over any element type
- One-liner you should know cold
- function groupBy<T, K>(xs: T[], key: (x: T) => K)
- Construct
- async / await
- Reach for it when
- File IO, hashing, streamed responses
- One-liner you should know cold
- await Promise.all(paths.map(readFile))
| Construct | Reach for it when | One-liner you should know cold |
|---|---|---|
| Map / Set | Grouping, dedup, counting, adjacency | (m.get(k) ?? []).push(v) for grouped lists |
| Discriminated union | Modeling a node or result with variants | switch (n.kind) with exhaustive checks |
| Generics | A reusable helper over any element type | function groupBy<T, K>(xs: T[], key: (x: T) => K) |
| async / await | File IO, hashing, streamed responses | await Promise.all(paths.map(readFile)) |
Small, well-typed helpersdon't any your way out
Reaching for any to escape a type error reads as a tell - the interviewer sees someone fighting the compiler instead of using it. Write a typed helper instead. A clean groupBy is the single most reused shape in these problems, so have it in muscle memory.
function groupBy<T, K>(items: T[], keyOf: (item: T) => K): Map<K, T[]> {
const out = new Map<K, T[]>();
for (const item of items) {
const k = keyOf(item);
const bucket = out.get(k);
if (bucket) bucket.push(item);
else out.set(k, [item]);
}
return out;
}
// findDuplicates becomes two groupBy calls: by size, then by hash.
const bySize = groupBy(files, (f) => f.bytes.length);Exhaustive unions catch your bugslet the compiler do the work
When you model a file-system node or a parse result as a discriminated union, a switch on the discriminant plus a never default makes the compiler error the moment you add a variant and forget to handle it. This is free correctness and showing it signals you write code the type system protects.
function size(node: FsNode): number {
switch (node.kind) {
case 'file': return node.bytes.length;
case 'dir': return node.children.reduce((n, c) => n + size(c), 0);
default: {
const _exhaustive: never = node; // new variant -> compile error here
return _exhaustive;
}
}
}Prove correctness fastlightweight tests beat claims
You won't wire up a full test runner in 45 minutes, but a handful of inline assertions proves your edge cases work and reads as a tester's instinct. Run the empty case, the single element and the one tricky case you're worried about.
function assertEq<T>(got: T, want: T, msg: string): void {
const a = JSON.stringify(got), b = JSON.stringify(want);
if (a !== b) throw new Error(`${msg}: got ${a}, want ${b}`);
}
assertEq(findDuplicates([]), [], 'empty input');
assertEq(applyEdits('abc', [{ offset: 1, length: 1, text: 'X' }]), 'aXc', 'single edit');A 45-minute mental template
- 1Contract (2 min). Write the typed signature and a one-line constraint, restate aloud.
- 2Brute force (5 min). Get a correct, obvious version working; name its complexity.
- 3Optimize (15 min). Apply the size-before-hash / right-to-left / BFS insight; keep it readable.
- 4Edge cases (8 min). Empty, single, overlapping, out-of-range - assert each.
- 5Walk it (rest). Trace one input end to end out loud, then state what you'd test next with more time.
Don't gold-plate the type system. Elaborate conditional types or branded generics burn time and rarely earn points in a 45-minute screen. Clean, obvious types that compile cleanly win; clever types that you spend ten minutes debugging lose.
Takeaway. Be reflexive with Map/Set, generics, discriminated unions and async; keep a typed groupBy and a 45-minute template (contract → brute force → optimize → edge cases → walk) in muscle memory.
Self-check
QWhy add a const _exhaustive: never = node; default case to a switch over a discriminated union?