Skip to content

ADR 0065: Quick open (Ctrl+P)

Status

Accepted

Context

Multiple buffers landed in ADR 0054, but the only way to get a buffer was ADR 0023's file browser: one directory at a time, navigating with ... That is fine for the file next to the one you have open and tedious for anything else, which in practice means the editor is pleasant to edit in and awkward to move around in.

Decision

Ctrl+P, as a third mode of the panel that already exists

Not a new widget. Quick open is the same badge, the same input field, the same list, the same sliding row highlight, the same theming and the same Enter/Escape handling as Open and Save-As — it differs only in where the entries come from and how they are filtered. A separate class would have been roughly two hundred duplicated lines that then drift apart visually, which is how editors end up with two file pickers that look almost the same.

So FileBrowserPanel::Mode gains QuickOpen, the badge shows P, and three methods branch on it.

"The project" is the git checkout, or the folder you are in

project::rootFor() walks up from the current file's directory looking for .git and stops at the first one; with no repository anywhere above, the file's own directory is the root.

Deliberately not git ls-files. It would be faster on a huge repository and would respect .gitignore for free — and it would also mean Ctrl+P silently doing nothing in a directory that isn't a checkout, which is a perfectly normal way to use a text editor.

The walk prunes rather than filters: an explicit directory stack, not QDirIterator::Subdirectories, because that descends into everything and only lets you drop the results — on this repository that means walking all of .git to throw it away. Hidden entries and a fixed list of build/vendor directory names (build*, node_modules, .venv*, target, dist, …) are skipped. It is not a .gitignore parser and does not pretend to be one; it is a fixed list, which never surprises you by hiding a file you can see in your own directory listing.

The score is the feature

Matching is subsequence — edvim finds gui/src/editor_viewport_vim.cpp — but with a few hundred files, half of them match any short query, so ranking is the entire job. Four things, in descending weight: consecutive runs, characters matched at a boundary (/, _, -, ., or a camelCase hump), matches in the basename rather than a directory name, and brevity as a tiebreak.

Matching is greedy left-to-right — each needle character takes the next occurrence, not the best one. That is a real limitation, stated in fuzzy_match.h rather than hidden: a query whose letters appear early in a bad spot and again later in a good one is scored on the early one. The alternative is a dynamic-programming pass over every candidate on every keystroke, and this editor's whole claim is that it doesn't do that sort of thing.

Unlike applyFilter(), which hides rows, quick open rebuilds the list in score order — hiding rows in a fixed alphabetical order cannot put the file you meant first, which is the only thing a fuzzy finder is for. The sort is stable, so equal scores keep the walk's alphabetical order instead of shuffling under you as you type, and only the top 200 rows are built: you never look past the first handful, and constructing thousands of list items per keystroke is exactly the per-frame work ADR 0053 was about.

The field is a query, never a path

In Open and Save-As, typing something that looks like a path resolves it (ADR 0055). In quick open it does not: src/ed is something to match against, not a file to create. Enter opens whatever is highlighted, and nothing else.

Consequences

Verified live in this repository: Ctrl+P lists 298 files with the root shown as ~/Lab/area56/textEditor — 298 files; typing edvim puts gui/src/editor_viewport_vim.cpp first, ahead of editor_viewport_commands.cpp and the vim-related ADRs; Enter opens it as a new buffer; Down moves the highlight; Escape closes. Opened on a file outside any repository, the root falls back to that file's own directory.

The listing header says how many files are in play, and says first N files when the walk hit its cap — a listing that silently stops is a listing that lies about what you can open.

The cost, measured rather than assumed. The walk runs on the UI thread, once per Ctrl+P. Pruned, this repository is ~8ms — instant. A home directory with no repository above it reaches the 20,000-file cap in ~0.5s, which is a perceptible hitch. That is the honest ceiling of this design, and the fixes when it matters are a background thread or caching the listing between invocations; neither is worth doing before the hitch is actually in someone's way.

Deferred, and cheap on top of this:

  • Ranking recently-opened files first. The single biggest quality jump a quick-open gets, and it needs a buffer-history list the editor doesn't keep yet.
  • Highlighting the matched characters in each row. The scorer already knows the positions; the list renders plain strings.
  • Project-wide search reuses all of this — the walk, the panel, the results list — which is why it is the next thing on the list.