"finders" use "scanners" to generate a list of haystacks to search (usually but not always files).
There are two generic classes of "finders", that are paired with corresponding generic classes of scanners:
- "List"-based finders and scanners. These take a list of candidate haystacks, like a list of buffers, a list of help tags, or a list of search patterns. These are accessed by commands like
:CommandTBuffer,:CommandTHelp, and:CommandTSearch. - "Exec"-based finders and scanners. These run a command to generate the list of candidate haystacks. These are accessed by commands like
:CommandTGitand:CommandTRipgrep.
Each instance of a list or exec-based finder is defined using a Lua table. Finder definitions are found under finders, and specify either candidates (a list of candidates, or a function that returns such a list) or command (a command string, or a function that returns such a string).
There are also two specialised "finder" and "scanner" pairs:
- The built-in "file" finder. This uses POSIX APIs to traverse the filesystem and generate a list of candidates.
- The watchman finder. This communicates with watchman using its binary protocol to obtain a list of candidates.
These finders are hard-coded in the Command-T source code as functions.
In the following discussion, note that there are a few sets of confusing, overlapping, or overloading terms that would make a good target for refactoring:
- "open", "on_open": used at various layers of abstraction for configuration key names, callbacks, parameters, and so on.
- "config" vs "options": used to describe user-supplied settings, defaults, and also function-level parameters.
-
(Optional) User configures a mapping in their personal config (eg.
vim.keymap.set('n', '<Leader>b', '<Plug>(CommandTBuffer)')) -
(Optional) User uses the mapping, which passes through the built-in
<Plug>mapping to a command (eg.vim.keymap.set('n', '<Plug>(CommandTBuffer)', ':CommandTBuffer<CR>', { silent = true })) -
The command invokes the
commandt.finderfunction defined ininit.lua, passing'buffer'as a parameter:vim.api.nvim_create_user_command(CommandTBuffer, function() require('wincent.commandt').finder('buffer') end, { nargs = 0, }) -
commandt.finder()takes two params, a findername, and an optionaldirectory(not passed in the case of:CommandTBuffer,:CommandTHelp,:CommandTHistory,:CommandTJump,:CommandTLine,:CommandTSearch, or:CommandTTag, but passed in the case of:CommandTFd,:CommandTFind,:CommandTGit, and:CommandTRipgrep).- It looks up the finder config under the
namekey ('buffer', for example) of the user's settings (obtained via a call tocommandt.options(), which returns a copy of the user's settings, or falls back to the defaults if there are no explicit user settings). - If the config defines an
optionscallback, it calls the callback with the existing options so that it has an opportunity to transform those options, and then sanitizes them. This is used by several finders to force the display of dotfiles (ie. by unconditionally settingalways_show_dot_filestotrue, andnever_show_dot_filestofalse); specifically, the'buffer','help','history','jump','line','search', and'tag'finders. - If the config defines an
on_directorycallback, it calls the callback with the directory to give it the opportunity to transform the directory. This is used by finders which change their root directory based on thecommandt.traversesetting; if no directory is provided, and the user settings require it, the callback will attempt to infer an appropriate directory, using the current working directory as a fallback. Finders which useon_directoryinclude'fd','find','git', and'rg'. - It prepares an
opencallback with signatureopen(item, ex_command)and assigns it back to theoptionsobject that will be passed into thefinders.listorfinders.execimplementation, and also intoui.show(). Thisopencallback will in turn forward to theopenimplementation specified in the config, if provided, otherwise falling back tosbuffer(item, ex_command)(which tries to intelligently pick the right place to open the requested buffer). Finders which specify a custom'open'in their config include'fd','find','git', and'rg'; which all pass in anon_open(item, ex_command, directory, _options, _context)implementation which usessbuffer()to open a relativized path (ie.sbuffer(relativize(directory, item), ex_command)). In contrast, the'help','history','line','search', and'tag'finders define totally custom open callbacks. Finally, the'buffer'and'jump'finders define nothing, which means they use the fallback. - If the finder config provides a
candidatesfield, it obtains the actual list finder and context by passing in thedirectory,config.candidates, andoptions. Otherwise, it must contain acommandfield and it obtains a command finder passing indirectory,config.command,options, andname(thenameis used to look up finder-specific settings in theoptions). - If the finder config provides a
fallbackfield set totrue, it adds the fallback finder to thefinderobject, passing in the originalfinder,directory, andoptions(this fallback finder is a lazy wrapper around the built-in file finder, which gets invoked if the primary finder fails. - It invokes
ui.show(), passing in thefinderandoptions, merging in three additional settings:name,mode(modeis'virtual','file'ornil), and anon_closecallback set toconfig.on_close. Finders which actually specify anon_closeare'fd','find','rg', and the built-in file finders; they all usepopdfor this purpose, to reset to the original working directory in case the user specified a temporary directory as an argument to those finders (the watchman finder doesn't need this as it does not change into a temporary directory even when the user specifies a directory argument).
- It looks up the finder config under the
-
ui.show()sets some module-level state based on the passed infinder,options.on_close, andoptions.on_open. -
It calls
MatchListing.new(), passing in specific options, and thenmatch_listing:show(). -
It calls
Prompt.new(), again passing in specific options, including passingui.openvia theon_openproperty, thenprompt:show().Prompt.new()captures various settings, includingon_openandmappings. It creates a window withWindow.new()andself._window:show(), and sets up mappings inside that window withself._window:map(). When the user chooses to open a selection via a mapping, the prompt will call theon_opencallback with either'edit','split','tabedit', or'vsplit'as an argument.
-
The
on_opencallback, which is actuallyui.open(ex_command)(ex_commandis'edit','split'etc), callsclose(), which closes the windows and invokes anyon_closecallback. -
If the UI has an
on_opencallback, it calls it so that it can transform the result. Only the built-in file finder and the watchman finder use this, and they both use it to relativize the result in relation to thedirectory. -
The thing that actually opens the result is called with
vim.defer_fn(), to give autocommands a chance to run. The called function iscurrent_finder.open(result, ex_command). Recall from the above that finders like'fd'pass in anoptions.openthat relativizes the path and usessbuffer()to do a "smart open". Finders like the'help'finder define a custom callback. Finders like'buffer'and'jump'define noon_open, so use the default.
In most ways, the exec finders follow the same life-cycle described above (and in fact we mention some of them in the descriptions of the steps) so I won't repeat the details here. The main differences are in terms of how they interact with the scanners, but they don't have any implications for how opening works. See the "Memory model" section further down for important differences that exist between the different types in relation to memory ownership.
Here I'll document only the differences from the other finders:
- Instead of
commandt.finder(name, directory), we callcommandt.file_finder(directory). - We don't look at
candidates,list(etc) config because there is no specific config required to build this finder. We justrequirethe finder directly and forward all theoptionsto it. - We use
pushd()to optionally move to another directory, preserving a record of the previous working directory so that we can go back to it. - In the call to
ui.show(), we pass anon_open(result)that usesrelativize(directory, result)to transform the result.on_closeispopd, as mentioned previously.
Again, only documenting the differences:
- Like
commandt.file_finder(directory), the function we call just takes one param:commandt.watchman_finder(directory). - Like
commandt.file_finder(), this function doesn't do anything special with config; it just forwardsoptions. - Unlike
commandt.file_finder(), we don'tpushd, and we don't pass in anon_closethat doespopdeither, because watchman doesn't need to change directory to do its job. However, it does require us to pass anon_open()that does therelativize()trick.
| Command | Argument? | Finder function | Variant | Mode | Context? | on_directory? |
pushd? |
on_close/popd? |
|---|---|---|---|---|---|---|---|---|
:CommandTBuffer |
none | finder |
list | 'file' | no | no | no | no |
:CommandTCommand |
none | finder |
list | 'virtual' | no | no | no | no |
:CommandTFd |
directory | finder |
exec | 'file' | no | yes | yes | yes |
:CommandTFind |
directory | finder |
exec | 'file' | no | yes | yes | yes |
:CommandTGit |
directory | finder |
exec | 'file' | no | yes | no | no |
:CommandTHelp |
none | finder |
list | 'virtual' | no | no | no | no |
:CommandTHistory |
none | finder |
list | 'virtual' | no | no | no | no |
:CommandTJump |
none | finder |
list | 'virtual' | no | no | no | no |
:CommandTLine |
none | finder |
list | 'virtual' | no | no | no | no |
:CommandTRipgrep |
directory | finder |
exec | 'file' | no | yes | yes | yes |
:CommandTSearch |
none | finder |
list | 'virtual' | no | no | no | no |
:CommandTTag |
none | finder |
list | 'virtual' | yes | no | no | no |
:CommandTWatchman |
directory | watchman_finder |
n/a | 'file' | no | no1 | no | no |
:CommandT |
directory | file_finder |
n/a | 'file' | no | no1 | yes | yes |
Legend:
- Argument?: Does the commmand accept an argument?
- Finder function: What is the
commandtfunction that serves as entry point? - Variant: Does the finder use a list scanner (ie.
candidates) or an exec scanner (ie.command)? - Mode: Is the finder a "file" finder (shows icons) or a "virtual" one?
- Context?: Does the finder make use of the
contextparameter to pass state? on_directory?: Does the finder use anon_directorycallback to infer a starting directory if appropriate?pushd?: Does the finder usepushd()before scanning?on_close/popd?: Does the finder use anon_closecallback andpopdto go back to the previous directory when closing its UI (and optionally opening a selection)?
For maximum performance, Command-T takes great pains to avoid unnecessary copying. This, combined with the fact that memory is passing across Lua's FFI boundary to and from C code, means that there are some subtleties to the question of which code owns any particular piece of memory, and who is responsible for freeing it (either manually from the C code, or automatically via garbage collection initiated on the Lua side).
Ideally this would all be self-documenting and fool-proof, but for now it relies on extremely careful construction.
- C strings are wrapped inside a
str_tstruct with three fields (contents,length, andcapacity). These strings are (generally) mutable, and functions are provided for truncating and appending; if additionalcapacityis required in order to accommodate the desiredlength, then the backing allocation of thecontentsstorage is automatically grown (and potentially moved). When a string is no longer needed, the struct and associatedcontentsare released with a call tostr_free(), which in turn callsfree().- As a special case, strings can be part of a "slab allocation", which means that the
str_tpoints into a large block of memory containing manystr_tstructs, and theircontentpointers also point into a large slab containing string data. These strings are mostly immutable (producing an error if you try to callstr_append()orstr_append_char()), although you can callstr_truncate()on such strings (ie. effectively reducing thelengthvalue and writing a terminatingNULbyte at the required location). Callingstr_free()on a slab-allocated string does nothing; both the structs and theircontentswill remain in memory until the slab allocations themselves get deallocated. Slab-allocated strings are created by callingstr_init(). As an implementation detail, slab-allocated strings are marked as such by having theircapacityfield set to-1. At the time of writing, there are three places in Command-T that create slab-allocated strings:- The built-in
:CommandTfinder (seefind.c). - In the "exec" scanner (see
scanner.c). watchman_read_string_no_copy()in the watchman scanner, which is used to read the"files"property of the Watchman response (seewatchman.c).
- The built-in
- As a special case, strings can be part of a "slab allocation", which means that the
- Scanners manage access to a list of haystacks to be searched. The come in four varieties:
- Scanners created by
scanner_new_exec(), called from the "exec" scanner (scanners/exec.lua). As the name suggests, this scanner obtains its candidates by running commands likegit-ls-files,fd,find,rg, and friends. - Scanners created by
scanner_new(), called fromfind.c. This scanner does not copy the candidate strings (which are slab-allocated), but it does take ownership of the slabs. - Scanners created by
scanner_new_copy(), called fromtest/matcher.lua,benchmarks/matcher.luaand the "list" scanner (scanners/list.lua). As the name indicates, this scanner copies the passed in candidates, creating newstr_tobjects. This scanner is suitable and is used for smaller lists of candidates, like help tags, buffers and so on. - Scanners created by
scanner_new_str(), called fromwatchman.lua.
- Scanners created by
So, at the risk of producing documentation that is very prone to becoming out-of-date as things get refactored, these are the four patterns of memory ownership as manifested in the four different varieties of scanner. In summary:
| Scanner pattern | Has candidates? |
candidates owner |
Has buffer? |
buffer owner |
str_t are slab-allocated? |
|---|---|---|---|---|---|
scanner_new_exec() |
Yes | scanner_t (created) |
Yes | scanner_t (created) |
Yes |
scanner_new() |
Yes | scanner_t (assigned) |
Yes | scanner_t (assigned) |
Yes |
scanner_new_copy() |
Yes | scanner_t (created) |
No | n/a | No |
scanner_new_str() |
Yes | watchman_query_t (files) |
Yes | wathcman_reponse_t (payload) |
Yes |
Details follow.
This is the most common form of scanning in Command-T, used by anything that wraps an external command (eg. :CommandTGit, wrapping git-ls-files, :CommandTRipgrep, wrapping rg, and so on).
- The main controller (defined in
init.lua) callsfinders.exec()to obtain afinder:finders.exec()(defined infinders/exec.lua) callsscanner()(defined inscanners/exec.lua) to obtain ascannerinstance:scanner()callslib.scanner_new_exec()to obtain and return ascanner:lib.scanner_new_exec()calls thescanner_new_exec()via FFI:scanner_new_exec()creates acandidatesslab and abufferslab withxmap(). The former is used to store zero-copystr_trecords (created withstr_init()), while the latter holds the actual command output, into which thestr_trecords index via theircontentspointers.
lib.scanner_new_exec()usesffi.gc()to mark the returnedscannerobject such that when it is garbage-collected, thecommandt_scanner_free()function will be called:commandt_scanner_free()usesxmunmap()to release thecandidatesandbufferslabs, andfree()to deallocate thescanner_tstruct itself.- Note that it also contains a
forloop that would callfreeon all of thestr_trecords incandidates, but theforloop is a no-op because all of those strings are slab-allocated and there is anifthat checks this condition. (It does thisifcheck rather than callingstr_free()in order to save an unnecessary function call;str_free()on a slab-allocatedstr_tis a no-op.)
finders.exec()passes thescannerintolib.matcher_new(), and returns afinderobject that exposes arun()function (callinglib.matcher_run()); thefinderobject has a reference to thescanner, which keeps it alive until thefinderitself falls out of scope.
- The returned
finderis passed intoui.show(), which stores a reference in the module-localcurrent_findervariable, keeping thefinderalive until the next timeui.show()is called and a differentfinderis passed in.
The overall ownership chain, then, is: the finder owns the scanner, the scanner owns its candidates slab and buffer slab, and the str_t structs in the candidates slab do not own their individual contents because those are all slab-allocated.
- The main controller (defined in
init.lua) callsfinders.file()to obtain afinder:finders.file()(defined infinders/file.lua) callsscanner()(defined inscanners/file.lua) to obtain ascannerinstance:scanner()callslib.file_scanner()to obtain and return ascanner:lib.file_scanner()(defined inprivate/lib.lua) callscommandt_file_scanner()via FFI:commandt_file_scanner()(defined infind.c), callscommandt_find()(also infind.c).commandt_find()allocates two slabs withxmap(): thefilesslab for holdingstr_trecords, and thebufferslab for holding stringcontents.- As it walks the directory tree, it copies file paths into the
bufferslab (withmemcpy()), and createsstr_trecords in thefilesslab, usingstr_init()so as to avoid a redundant copy operation. - Once traversal is finished,
commandt_file_scanner()passes the two slabs intoscanner_new(), which takes ownership of them rather than copying them. commandt_file_scanner()then frees (withfree()) the left-over book-keeping data structures used bycommandt_find(), taking care to ensure that it does not free the slabs.
lib.file_scanner()usesffi.gc()to mark the returnedscannersuch that when it is garbage-collected, thecommandt_scanner_free()function will be called:commandt_scanner_free()will free itscandidatesslab (containingstr_tobjects) (withxmunmap()).- It will also free (with
xmunmap()) itsbuffer(string storage) and thescanner_tstruct itself. - Note that it also contains a
forloop that would callfreeon all of thestr_trecords incandidates, but theforloop is a no-op because all of those strings are slab-allocated and there is anifthat checks this condition. (It does thisifcheck rather than callingstr_free()in order to save an unnecessary function call;str_free()on a slab-allocatedstr_tis a no-op.)
finders.file()passes thescannerintolib.matcher_new(), and returns afinderobject that exposes arun()function (callinglib.matcher_run()); thefinderobject has a reference to thescanner, which keeps it alive until thefinderitself falls out of scope.
- The returned
finderis passed intoui.show(), which stores a reference in the module-localcurrent_findervariable, keeping thefinderalive until the next timeui.show()is called and a differentfinderis passed in.
The overall ownership chain, then, is: the finder owns the scanner, the scanner assumes ownership of the candidates and buffer slabs passed into it, and the str_t structs in candidates do not own their individual contents because those are all slab-allocated.
- The main controller (defined in
init.lua) callsfinders.list()to obtain afinder:finders.list()(defined infinders/list.lua) callsscanner()(defined inscanners/list.lua) to obtain ascannerinstance:scanner()callslib.scanner_new_copy()to obtain and return ascanner:lib.scanner_new_copy()(defined inprivate/lib.lua) callscommandt_scanner_new_copy()via FFI:commandt_scanner_new_copy()usesxmap()to createcandidatesstorage (forstr_tstructs).- It uses
str_init_copy()to createstr_tobjects in the slab which themselves reference storage obtained viaxmalloc()and populated withmemcpy(). This is why, as mentioned above, "list" scanners are only suitable for smaller sets of candidates.
lib.scanner_new_copy()usesffi.gc()to mark the returnedscannersuch that when it is garbage-collected, thecommandt_scanner_free()function will be called:commandt_scanner_free()will free itscandidates(withxmunmap()) after callingfree()on thecontentsstorage of eachstr_tin thecandidatesslab.- It will not free its
bufferbecause it does not use one. - It will free the
scanner_tstruct itself.
finders.list()passes thescannerintolib.matcher_new(), and returns afinderobject that exposes arun()function (callinglib.matcher_run()); thefinderobject has a reference to thescanner, which keeps it alive until thefinderitself falls out of scope.
- The returned
finderis passed intoui.show(), which stores a reference in the module-localcurrent_findervariable, keeping thefinderalive until the next timeui.show()is called and a differentfinderis passed in.
The overall ownership chain, then, is: the finder owns the scanner, the scanner owns its candidates slab (but has no buffer slab), and the str_t structs in the candidates slab own individual contents allocations.
- The main controller (defined in
init.lua) callsfinders.watchman()to obtain afinder:finders.watchman()(defined infinders/watchman.lua) callsscanner()(defined inscanners/watchman.lua) to obtain ascannerinstance:scanner()callslib.watchman_watch_project()scanner()then callslib.watchman_query()lib.watchman_query()callscommandt_watchman_query()via FF:commandt_watchman_query()lib.watchman_query()usesffi.gc()to mark the returnedresult.rawobject such that when it is garbage-collected,commandt_watchman_query_free()function will be called.commandt_watchman_query_free()usesxmunmap()to free thefilesslab.- It calls
watchman_response_free()to free theresponse:watchman_response_free()callsfree()on thepayloadand on thewatchman_response_tstruct itself.
- It also calls
free()on theerrorand on theresultstruct itself.
- In the happy path,
commandt_watchman_query()useswatchman_send()to send the query:watchman_send()creates awatchman_response_twithpayloadbuffer of size 4096, callingxrealloc()if necessary to grow the buffer once it has sniffed the initial part of the response to see how much storage is needed overall.
- Parsing the response,
commandt_watchman_query()usesxmap()to prepare an appropriately sizedfilesslab. - It then creates strings using
watchman_read_string_no_copy()directly into thefilesslab:watchman_read_string_no_copy()usesstr_init()to create zero-copystr_tstructs in thefilesslab that point at addresses within thepayloadbuffer inside thewatchman_reponse_t.
scanner()it passes a pointer (result.raw.files) intolib.scanner_new_str():lib.scanner_new_str()callsscanner_new_str()via FF:scanner_new_str()marks bothcandidatesandbufferas unowned (and in fact,bufferisNUL) by using the special value of-1forcandidates_sizeandbuffer_size. This is similar to howstr_tuses acapacityof-1to label something as belonging to a slab allocation.
lib.scanner_new_str()usesffi.gc()to mark the returnedscannersuch that when it is garbage-collected, thecommandt_scanner_free()function will be called.commandt_scanner_free()will not free thecandidatesandbufferslabs because thescannerdoes not own those; it only callsfreeon thescanner_tstruct itself.
scanner()stores a reference to theresultobject in a weak table, using thescanneras a key. Theresultobject has a reference to theresult.rawproperty, preventing it from being prematurely garbage collected.
finders.watchman()passes thescannerintolib.matcher_new(), and returns afinderobject that exposes arun()function (callinglib.matcher_run()); thefinderobject has a reference to thescanner, which keeps it alive until thefinderitself falls out of scope.
- The returned
finderis passed intoui.show(), which stores a reference in the module-localcurrent_findervariable, keeping thefinderalive until the next timeui.show()is called and a differentfinderis passed in.
This last one has the most complicated ownership chain: the finder owns the scanner, the scanner references the result only via the weak table, and the result references the raw return value from watchman_query() (ie. the watchman_query_t) which owns the files slab, which in turn contains pointers to string contents in the watchman_response_t. When the scanner is garbage collected, the last reference to result goes away, which in turn means the last reference to result.raw goes away, which causes commandt_watchman_query_free() to run, freeing the files slab, and calling watchman_response_free() which frees the payload. This could probably be improved.