Skip to content

Search: ranked, along the path, one typo, answered at once - #61

Open
thisisgm wants to merge 38 commits into
omacom:mainfrom
thisisgm:search
Open

thisisgm wants to merge 38 commits into
omacom:mainfrom
thisisgm:search

Conversation

@thisisgm

@thisisgm thisisgm commented Nov 11, 2026 •

Copy link
Copy Markdown

Stacked on #26: its eighteen commits come first here, the last twenty are this pull request's, and I will rebase it as soon as #26 lands.

Ten changes to search (/), one commit each with its own tests: a search no longer asks the disk about every file, later keys are answered from memory and still find a file made since, the best matches come first, a query can follow the path, one typo is forgiven, a very large folder shows matches while it is still read, Select All and a refresh reach the matches past the first window, a search goes on answering while it is read again, and the status in the search field leaves room for the query in a narrow pane. An eleventh commit answers the review: a path typed out exactly is found when folding its letters makes it longer than it is stored. Four more answer a second one: a search reads at most 2,000,000 names, the nearest folders first, and the field then says how many it read; no link is followed on the way to a folder; a capital starts a word in a name that has letters past ASCII; and the index's numbers have names. The last one is tests only: the cases that hold a walk pass on a machine with one processor, none rests on the folder watcher's timer, and one more lands an answer that the end of a walk overtook. The newest commit takes out a pause: the field waited for 150 ms without a key before it asked, and it now asks at every key. The last two take out a flicker while typing: the selection and the cursor change in the frame an answer is drawn and not before, the cursor stays on the best match, the end of a search puts it back on the item it left, and a folder's item count stays on screen from one answer to the next. One more answers the review of those two: a selection made again after a key, with nothing changed (Select All pressed a second time), is kept when the answer lands. The suite goes from 236 to 276 tests. No library, depends or optdepends entry is added, and nothing runs when no search is open: no index is kept on disk and nothing is read ahead of the first key.

What you'd notice

  • Each key answers at once. Every key walked the whole tree again and asked the disk about every file on the way. The first key now walks the tree once, reading names and types from the directory alone, and later keys are answered from those names in memory. The names are let go when the search closes, the folder changes or it is read again. The field kept a 150 ms wait from the days when every key walked the tree, and asked only once the typing paused. That wait is gone: the field asks at each key, so the results follow the typing.
  • No flicker while typing. A key cleared the selection at once, a frame or two before its answer was drawn, so the highlighted row blinked. Each answer left the cursor on the item or the row it had been on, which scrolled the list away from the best match, and Escape showed the folder a frame before its cursor. Folder counts (3 items) went out for a frame at every key and came back. Now the selection changes in the frame the new rows are drawn, the cursor is on the first row of each answer until it is moved to another item, Escape shows the folder with the cursor back on the item it left in the same frame, and a folder that was already listed keeps its count on screen while it is counted again. A folder that was not on screen before still gets its count after its row, as it does when any folder opens. Showing or hiding hidden files clears the counts at once, since a count made the other way is wrong, and the folders are counted again.
  • The best matches come first. Results came in name order and stopped at 1,000. They are now ranked: the exact name, then a name or a word that starts with the query, then its letters in order. Rows arrive 350 at a time as the view scrolls, and the field says how many matched.
  • A query can follow the path. editor/src/render found nothing, since only names were compared. The folders above a row now take as much of the query as they can and the name takes the rest. A name that holds the query still comes first.
  • One typo is forgiven. redner for render found nothing. When no name spells the query, names one edit away are listed (two for long words), ahead of names that only hold its letters scattered in order. Digits are never edited, and a query that some name spells out never pays for this.
  • A very large folder answers while it is read. On a very large tree the first key waited for the whole walk. After a short hold the best matches read so far are shown and refreshed until the walk lands, and the field says it is still reading.
  • Select All takes every match, and a refresh keeps them. With rows arriving 350 at a time, Select All takes the matches that are not rows yet by path, at once, from the answer in memory: a copy or a delete pressed next acts on all of them, and the rows still arrive as the view scrolls. A search that is read again (a file changed, or F5) shows as many rows as it did and keeps what is selected while it still matches, and the partial answers shown while a large tree is read again drop nothing from the selection: a copy or a delete pressed then still names every selected file, found again or not yet, and the rows come back as many as they were. A row selected while the new answer is on its way stays selected too, among the rows shown or past them. The names are read under the search's lock and the disk is asked with the lock let go, so Select All never waits on the disk.
  • A later key finds what was made since. Later keys were answered from the names read at the first key alone, and only the searched folder itself is watched, so a file made in a subfolder afterwards stayed missing until the search was closed. A later key is still answered from memory at once; the folders are then read again behind the answer, off the UI thread, and the new answer takes over when it is whole.
  • A search read again keeps answering. A change on disk or F5 walked the folders again from nothing, so a search that had landed fell back to partial answers and a partial count. It now answers from the walk it has until the new one has answered, then takes the new one. One walk runs at a time: a change that comes during a walk is read once that walk is whole, so a folder that keeps changing never starves the answer. Showing or hiding hidden files starts the search over. Select All takes every match while a large tree is still being read, and under any sort, not only best first. Sorted another way, a list cut to the best 1,000 matches says so in the path bar, with how many matched.
  • The query stays readable in a narrow pane. The status at the end of the search field says where it searches and how many matched. It was drawn at its full width, and the field's padding was that width, so in a split window or a narrow one it ran out of the field and over the query. It is now cut short with an ellipsis before the query has less than 160 pixels, and not drawn in a field with no room for it at all. In a field with room for both nothing changes.

Numbers

Measured on a Linux server with a small driver that links the same sources and types into the same model the window uses, on a folder tree of 100,000 files, warm cache, two runs of ten timings a query after two warm-ups. Times are from the key to the rows being ready.

Query main this branch
The walk inside every query below 41.1 to 42.1 ms, once, at the first key
An exact name, render_frame.rs 69.8 to 74.3 ms under 0.1 ms
One typo, redner_frame.rs 69.0 to 75.0 ms, nothing found 11.6 to 13.0 ms, the file found
Along the path, editor/src/components/render 66.0 to 71.6 ms, nothing found under 0.1 ms, the file found
A deep path, archive/2024/client/reports/quarterly 63.8 to 68.5 ms, nothing found under 0.1 ms, the file found
An extension, .svg, 6,458 matches 20.6 to 23.9 ms, stops at the first 1,000 3.3 to 3.5 ms, all counted, the best 350 shown

On main every key pays for the walk again, so its times are a walk and a compare. On the branch the walk is paid once, and a key is a pass over the names in memory. The typo pass is the slow one, since it runs only after that pass found no name that spells the query. The tree takes 7.0 MB while the search is open.

Screenshots

Each picture is main above and this branch below, in the list view, on the same small source tree.

render: main lists by name, the branch ranks the folder and the exact names first and counts the matches.

Search by name before and after

editor/src/render: a path query.

Search along the path before and after

redner: one typo.

Search with a typo before and after

A window split in two panes, render typed in the right one: this branch before its last commit above, with it below.

The search status in a narrow pane before and after

Tests

Forty new cases, each failing without its commit: a search asks the disk only about the folders it enters and the rows it shows; a later key is answered before any folder is read again; a file made since the folders were read is found at the next key; the exact name, a name or a word that starts with the query, and scattered letters come in that order; rows arrive a window at a time while the count is whole; a query follows the folders above a name; a typo is forgiven only when no name spells the query, and ranks above scattered letters; a tree still being read shows the matches found so far; Select All selects all 500 of 500 matches at once when 350 are shown, and names them all; a search read again keeps the 500 rows it showed and the selection at row 420; it keeps what Select All took past the rows, less a file that is gone; and through the partial answers of a tree being read again all 500 selected files stay selected and named, and the 500 rows come back; Select All while a tree is still read takes every match found so far; a search that landed keeps its rows and its count while it is read again, also while a download keeps changing the folder; Select All under another sort takes every match; what Select All names is whole by the time the selection says it changed; a search that is cleared forgets how many matched; a match gone from the disk since the walk still has its row; a change during the first walk is read once that walk is whole; a walk made again takes over only when it has answered; Select All after a key taken back takes every match; a row selected while a search is read again stays selected when the new answer leaves it past the rows shown; hiding hidden files starts the search over; a search that ends stops its second walk; the path bar says how many matched and how many are shown, whatever the folder is called; and the status in a narrow field is cut short so the query keeps its room, mirrored too, changes the field's padding and never its width, and is not drawn in a field with no room for it; a path whose letter grows when it is folded is found typed out, as a folder's name and as a file's, while a path in ASCII too short for the query is still ruled out before a folder is read. A tree of more names than a search keeps is read up to that many, the nearest, and the field says how many were read, beside how many rows are shown when the list is cut too; a folder swapped for a link while its own folders wait is not read through; a folder searched through a link that is turned elsewhere mid-read stays the tree it was; and büro finds MeinBüro.txt ahead of xbüro.txt. A key typed in the search field is the query at once, with no pause waited out; and keys typed while the first walk still reads start no walk beside it, are read once more together when it ends, and what lands is the answer to the last of them, with no earlier key's answer shown on the way; a key leaves the selection alone until its answer lands, and what was selected since is kept, also when it is the same selection asked for again, and is still cut to what the answer matches; the end of a search shows the folder and says so in one turn; in the window, the cursor is on the first row of each answer, follows a better match found later, stays with an item it was moved to, and is back on the item it left when the search ends; and a folder two answers list shows its count in the turn the second lands, is counted again all the same, and never shows a count made with hidden files the other way. Every commit builds and passes the suite on its own, also with TMPDIR set to a folder whose name has a space, where only extractRefusesWhatWouldLandOutside fails, as it does on main.

Limits

  • The names of a tree are held in memory while its search is open: about 70 bytes a file on the tree above.
  • A search reads at most 2,000,000 names (about 140 MB at that rate), the nearest folders first. Of a bigger tree the field says N matches in X and below, only the nearest 2,000,000 items read.
  • Folders are opened with openat2, so the walk needs Linux 5.6 or later.
  • Each later key walks the tree again behind its answer, on the thread pool: one walk at a time, and keys typed during a walk ask for one more between them.
  • A typo is looked for in names only, not along the path, and only when no name spells the query.

@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The latest changes appear safe to merge; the previous selection-loss issue is addressed.

Summary

The PR adds ranked search, path matching, typo support, and answers from names held in memory.

  • Search finds and ranks matches from names and paths.
  • Search keeps selected matches and places the cursor with the results.
  • The search field shows useful status without hiding the query.
  • Folder views read file details as the visible rows need them.
  • Sidebar lists notify the view only when their own contents change.
  • Theme rounding follows Hyprland changes while the app is open.

Reviews (20) · Last reviewed commit: "Keep a selection made again after a sear..." · Reviewed by Greptile

Comment thread src/foldermodel.cpp Resolved
Comment thread src/foldermodel.cpp Resolved
Comment thread src/foldermodel.cpp Resolved
@thisisgm
thisisgm force-pushed the search branch 2 times, most recently from ecce9ca to b78cc11 Compare November 11, 2026 23:33
Comment thread src/foldermodel.cpp Resolved
@thisisgm
thisisgm force-pushed the search branch 2 times, most recently from 541e13a to 283ae11 Compare November 12, 2026 02:06
Comment thread src/foldermodel.cpp Resolved
Comment thread src/theme.cpp Resolved
@thisisgm
thisisgm force-pushed the search branch 2 times, most recently from 705ce17 to ffec451 Compare November 12, 2026 06:04
Comment thread src/PathBar.qml Resolved
Comment thread src/searchindex.cpp Resolved
Building a listing asked the disk about every file a second time,
because Qt stats a file again before it reads the name. A regular file
is now typed from the name the listing already read; a link, a pipe or
a device keeps the full lookup.
A selection, a folder count or a second view of the same folder each
made the model copy its whole listing. They now read it in place, so a
large folder is held once however many views show it.
The preview's folder summary sorted every name to count them and to
pick two pictures. It now keeps the first two pictures in one pass, so
summarizing a large folder no longer holds every name.
Every entry kept its own copy of its type, kind and extension, and the
listing kept spare room for entries that never came. The entries of a
listing now share those strings, and the spare room is given back.
Opening a folder typed every file before its first row showed. The
listing now leaves a regular file's type, kind and extension unread
until a role asks, which touches no disk; a sort by kind still types
every entry in the worker.
Every refresh of Places, the 15 second poll included, told all four
sidebar lists they had changed. Each list now has its own signal and
is announced only when its contents differ.
While active, Folio ran hyprctl once a second to read the corner
rounding. It now reads it at start, on a palette change, on return to
the window and when Hyprland's event socket reports a reloaded config.
Sorting 100,000 names collated both strings on every compare. A
listing that arrives in a few ordered runs is now merged, and a name
or kind sort makes one sort key an entry first. The comparator is the
same, so every order and tie stays; the C locale compares as before.
A name or kind listing read every entry's size and date before its
first row showed. It now reads the first 320 rows, and a later row
when a view asks, one batch a turn, off the UI thread. A row not read
yet shows no size and no date, never 0 B.
A re-read that found the same names kept each folder's item count,
even when the folder's own date had moved. Such a folder's count is
now dropped and counted again, as it was before the listing learned
to keep its rows through a re-read.
The event socket was opened once. Hyprland hangs up on a reader that
fell behind, and after that a config reload no longer reached the
corners. The watch is now taken up again when Folio is back in use.
A text tile past the rows read at first asked for its page with no
date, then again when the date arrived, and a page has no cache on
disk, so the file was read and drawn twice. Like a thumbnail, a page
is now asked for only once its date is known.
A folder past the rows read at first is counted without its date
being read, so a re-read had nothing to compare and kept its old
count. Its date is now read just before it is counted and kept
with the count, and the re-read sees the folder changed since.
A burst of events was read whole into memory before any of it was
looked at, on the UI thread. One turn of the event loop now reads
64 KiB at most and looks at each piece as it comes, and a line too
long to keep is dropped to its end, its last piece included.
Hung up on by Hyprland, the theme took its watch up again only when
Folio was left and come back to, so a config reload in between left
the corners as they were. While in use it now connects again after
a pause, and reads the rounding for a reload that went by untold.
Hung up on while Folio stayed in use, the theme tried Hyprland's
event socket once more after a pause. A try that found no socket
was the last, and reloads went untold until Folio was left and
come back to. Each failed try now starts the pause again.
A re-read drops the answers on their way and reads the rows asked for itself.
A row it could not read either stayed marked as being read, so no view could
ask for it again. Such a row is now owed: a view may ask again, the folder's
next re-read tries it again, and a view is told when another view read it.
A view asks for a row's kind, MIME type and extension each time it
makes the row, and each ask read them from the name again: 48 us a
row in /usr/bin. The model now keeps up to 4,096 names' types, until
a folder is found changed.
A search asked the disk about every file it passed. The walk now reads
names and types from the directory alone and asks only about folders
it enters and rows it shows. The matches and their order are as before.
Every key walked the whole tree again. The first key now walks it once
and later keys are answered from the names in memory, until the search
closes, the folder changes or it is read again. Escape frees them.
Results came by name and stopped at 1,000. They are now ranked: the
exact name, then a name or a word that starts with the query, then its
letters in order. Rows arrive 350 at a time as the view scrolls, and
the field says how many matched.
A query such as editor/src/render found nothing, since only names were
compared. The folders above a row now take as much of the query as
they can and the name takes the rest; a name that holds the query
still comes first.
A slip such as redner for render found nothing. When no name holds
the query whole or in runs that each start a word, names one edit
away are listed, two for long words, ahead of names that only hold
its letters scattered in order. Digits are never edited, and a query
some name spells out never pays.
On a very large tree the first key waited for the whole walk. After a
short hold the best matches read so far are shown and refreshed until
the walk lands, and the field says it is still reading.
With matches shown a window at a time, Select All took only the rows
scrolled to, and F5 or a change in the folder went back to the first
window, dropping a selection past it. Select All now takes the matches
past the rows by path, at once, from the answer in memory; a search
read again shows as many rows as it did and keeps what is selected
while it still matches, and a partial answer drops nothing.
A change on disk or F5 walked the folders again from nothing, so
a landed search fell back to partial answers and its count. It
now answers on until the new walk has answered, one walk at a
time. Select All takes every match while still reading and under
any sort, and a list cut to the best matches says so.
Only the open folder is watched, and a later key was answered from
the first read alone, so a file made below it since stayed missing
until a refresh. Each later key is still answered at once from what
was read, and the folders are read again behind that answer.
The status at the end of the search field says where it searches and how
many match, at its full width, and the field's padding is that width. In a
narrow pane it took the room the query is typed in. It is now cut short
with an ellipsis before the query has less than 160 pixels.
A search along the path ruled a row out when the folded query was
longer than the path as stored. Folding can lengthen a letter, so a
path typed out exactly went missing. Only ASCII paths take that test.
The bits of digits and marks, the first size of the lookup table and
the classes of a pending score were bare numbers: they have names now,
and a static_assert ties the classes to the tiers. Each comment is one
line. Nothing a search does changes.
A folder swapped for a link after its own folders were listed was
followed when they were opened: O_NOFOLLOW guards the last name only.
Every folder is opened from the root's descriptor now, with
RESOLVE_NO_SYMLINKS, and the root is set under the walk's lock.
A name with one letter past ASCII was folded whole, so MeinBüro lost
the capital that starts its second word and ranked with names that
only hold the letters. Only what is past ASCII is folded now.
A walk kept every name below the folder, with no limit on the sum. It
ends at 2,000,000 names now, the nearest first, and the search field
says that only those were read.
A held walk keeps the pool at two threads, so the cases pass on one
processor. The held-pool cases wait until the walk's end has asked for
its answer, the walk counts rest on no watcher timer, and a new case
lands an answer the walk's end overtook. Each comment is one line.
The field waited for 150 ms without a key before it asked, a wait kept
from when every key walked the tree. The model answers a key from the
names it holds, so the field asks as each is typed.
A key cleared the selection before its answer was drawn, each answer
left the cursor on its old row, and the end of a search showed the
folder a frame before its cursor. The selection now goes with its rows,
the cursor stays on the best match, and the end restores it at once.
Rows that replace others came with no folder counted, so at every key
of a search the counts on screen went out for a frame and came back.
A folder the rows before had now shows the count it had until its new
one is in, and is counted again whatever overtakes that counting.
Comment thread src/foldermodel.cpp Resolved
What is selected after a key is kept by that key's answer, but asking
for the selection already there returned early and left the let-go
standing: Select All pressed again while an answer was on its way was
cleared by it. The let-go now ends at any ask, changed or not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant