Skip to content

Commit e3b0bb6

Browse files
zenibakoibhagwan
andauthored
feat: Jujutsu (jj) support for files and grep, auto-detecting vcs_search function (#2610)
* fix: exclude .jj directory from file search defaults Jujutsu (jj) stores internal data in a .jj directory. Exclude it from the default fd_opts, rg_opts, and find_opts so .jj contents don't appear in file picker results. * feat: add jj_files provider for Jujutsu workspaces Add a FzfLua jj_files command that lists tracked files using `jj file list`, mirroring git_files but for Jujutsu repos. This works in secondary jj workspaces that lack a .git directory. - path.jj_root(): detects jj workspace root with fast .jj walk-up - path.is_jj_repo(): boolean wrapper - providers/jj.lua: jj_files provider following the git_files pattern - defaults.lua: jj.files config with jj file list command * feat: add vcs_files provider with jj -> git -> files fallback Adds FzfLua vcs_files as a drop-in replacement for git_files that auto-detects the VCS: uses jj_files in jj workspaces, git_files in git repos, and falls back to the regular files picker otherwise. * fix: make vcs_files self-contained to avoid E565 window error vcs_files was delegating to git.files()/jj.files() which each call normalize_opts and core.fzf_exec independently. This caused E565 errors when the function was invoked from a context where opening windows isn't immediately allowed. Instead, detect the VCS type, normalize opts with the right defaults key, set the VCS root as cwd, and call core.fzf_exec directly. * refactor: consolidate jj_root cmd construction into single expression * refactor: simplify vcs_files back to delegation pattern The E565 error was caused by hardtime.nvim + function-reference keymaps, not by the delegation pattern. Revert to the simpler form that delegates to jj.files()/git.files()/files() directly, avoiding duplicated logic. * test: add unit tests for jj_root and is_jj_repo * test: add vcs_files delegation unit tests * fix: resolve CI lint failures and add jj documentation - Add @param/@return annotations to is_jj_repo and jj_root to fix redundant-parameter lint warnings - Add diagnostic disable for duplicate-set-field in vcs_files tests (intentional monkey-patching) - Update find_opts to use -prune for .jj directories instead of -path filter - Add opts = opts or {} in vcs_files to prevent nil indexing - Add user-visible message in jj_root when .jj walk-up fails - Add jj_files and vcs_files to README.md and doc/fzf-lua.txt - Update example configs with current find_opts/rg_opts/fd_opts defaults * fix: use discovered workspace root for jj -R flag, add docs TOC and prereqs - Use walk-up discovered root_dir instead of opts.cwd for jj -R flag - Add *fzf-lua-jj* TOC entry and section tag in vimdoc - Add jj to optional dependencies in README - Derive jj defaults from git.files via tbl_deep_extend - Add nil-opts regression test for vcs_files() * fix: detect GNU getopt on Apple Silicon Macs Add /opt/homebrew/opt/gnu-getopt/bin/getopt path check for Homebrew on Apple Silicon, before the existing /usr/local/bin/getopt Intel path. * fix: remove explicit -print from find_opts, normalize jj_root cwd, add jj optional dep - Remove -print from default find_opts so the hidden toggle filter and nullglob -print0 are correctly positioned in the predicate chain - Append -print0 after predicates (instead of prepending) in render_crlf - Insert hidden filter before -print0 in find commands so it is evaluated - Normalize opts.cwd in jj_root with tilde expansion and relative path resolution, matching how git_cwd handles paths - List jj as an optional dependency in the help docs * docs: sync help file defaults with code and clarify vcs_files fallback - Update find_opts, rg_opts, fd_opts in customization example to match the actual runtime defaults (remove -print and --hidden flags) - Clarify that only jj_files is a no-op without jj; vcs_files falls back to git_files or the generic files picker * fix: alignment issues * fix: revert find_opts to `! -path` pattern and drop unrelated headless_fd.sh change Address ibhagwan's review feedback: - Revert find_opts from -prune to `! -path` pattern to avoid .git entries leaking into output and macOS -print0 incompatibilities - Revert -print0 placement in config.lua (prepend, not append) - Revert hidden toggle refactor in providers/files.lua (unnecessary with ! -path pattern) - Revert unrelated getopt path change in scripts/headless_fd.sh * feat: add VCS-specific header to vcs_files popup and test coverage vcs_files now sets cwd_header_txt to indicate which VCS backend (jj/git) is being used. Both git.files and jj.files already have "cwd" in their _headers config, ensuring a header displays in the popup. Tests added for: vcs_files header text per VCS type, git_files quiet failure when not in a git repo, and _headers configuration for both git.files and jj.files. * fix: capitalize JJ in default-title profile ("JJ Files" not "Jj Files") * Restore `--hidden` * feat: simplify jj_root to use jj root command directly * fix: handle missing jj command gracefully in utils.io_systemlist * fix: handle missing commands gracefully in utils.io_systemlist and io_system * docs: incorrect default --hidden ci: fix lint --------- Co-authored-by: bhagwan <bhagwan@disroot.org>
1 parent 8a79ee5 commit e3b0bb6

10 files changed

Lines changed: 393 additions & 15 deletions

File tree

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim)
7474
using fzf's native previewer
7575
- [delta](https://github.com/dandavison/delta) - syntax highlighted git pager
7676
for git status previews
77+
- [jj](https://github.com/jj-vcs/jj) - for Jujutsu commands (`jj_files`, `vcs_files`)
7778
- [nvim-dap](https://github.com/mfussenegger/nvim-dap) - for Debug Adapter
7879
Protocol (DAP) support
7980
- [nvim-treesitter-context](https://github.com/nvim-treesitter/nvim-treesitter-context) - for
@@ -211,6 +212,7 @@ Fzf-Lua conveniently comes with a VS-Code like picker by default
211212
| `treesitter` | current buffer treesitter symbols |
212213
| `tabs` | open tabs |
213214
| `args` | argument list |
215+
| `vcs_files` | `jj`/`git` files or `find`/`fd` |
214216

215217
</details>
216218
<details>
@@ -273,6 +275,16 @@ Fzf-Lua conveniently comes with a VS-Code like picker by default
273275
| `git_tags` | git tags |
274276
| `git_stash` | git stash |
275277

278+
</details>
279+
<details>
280+
<summary>Jujutsu</summary>
281+
282+
### Jujutsu
283+
284+
| Command | List |
285+
| ---------- | ---------------------- |
286+
| `jj_files` | `jj file list` tracked files |
287+
276288
</details>
277289
<details>
278290
<summary>LSP / Diagnostics</summary>
@@ -823,9 +835,9 @@ previewers = {
823835
-- otherwise auto-detect prioritizes `fd`:`rg`:`find`
824836
-- default options are controlled by 'fd|rg|find|_opts'
825837
-- cmd = "rg --files",
826-
find_opts = [[-type f \! -path '*/.git/*']],
827-
rg_opts = [[--color=never --hidden --files -g "!.git"]],
828-
fd_opts = [[--color=never --hidden --type f --type l --exclude .git]],
838+
find_opts = [[-type f \! -path '*/.git/*' \! -path '*/.jj/*']],
839+
rg_opts = [[--color=never --files -g "!.git" -g "!.jj"]],
840+
fd_opts = [[--color=never --type f --type l --exclude .git --exclude .jj]],
829841
dir_opts = [[/s/b/a:-d]],
830842
-- by default, cwd appears in the header only if {opts} contain a cwd
831843
-- parameter to a different folder than the current working directory

doc/fzf-lua.txt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Buffers and Files ................................ |fzf-lua-buffers-and-files|
1717
Search ...................................................... |fzf-lua-search|
1818
Tags .......................................................... |fzf-lua-tags|
1919
Git ............................................................ |fzf-lua-git|
20+
Jujutsu ......................................................... |fzf-lua-jj|
2021
LSP/Diagnostics .................................... |fzf-lua-lsp/diagnostics|
2122
Misc .......................................................... |fzf-lua-misc|
2223
Neovim API .............................................. |fzf-lua-neovim-api|
@@ -129,6 +130,11 @@ OPTIONAL DEPENDENCIES *fzf-lua-optional-dependencies*
129130
<https://github.com/MeanderingProgrammer/render-markdown.nvim> or
130131
markview.nvim <https://github.com/OXY2DEV/markview.nvim> - for rendering
131132
markdown files in the previewer
133+
- jj <https://github.com/jj-vcs/jj> - for Jujutsu repository support;
134+
`jj_files` is a no-op when the `jj` executable is not found on PATH,
135+
while `vcs_files` falls back to `git_files` or the generic `files`
136+
picker (install via `cargo install jj-cli` or see
137+
https://jj-vcs.github.io/jj/latest/install-and-setup)
132138
Below are a few optional dependencies for viewing media files (which you need
133139
to configure in `previewer.builtin.extensions`):
134140

@@ -259,6 +265,7 @@ BUFFERS AND FILES *fzf-lua-buffers-and-files*
259265
| `treesitter` | current buffer treesitter symbols |
260266
| `tabs` | open tabs |
261267
| `args` | argument list |
268+
| `vcs_files` | `jj`/`git` files or `find`/`fd` |
262269

263270

264271

@@ -317,6 +324,14 @@ GIT *fzf-lua-git*
317324

318325

319326

327+
JUJUTSU *fzf-lua-jj*
328+
329+
| Command | List |
330+
| ---------- | ---------------------------- |
331+
| `jj_files` | `jj file list` tracked files |
332+
333+
334+
320335
LSP/DIAGNOSTICS *fzf-lua-lsp/diagnostics*
321336

322337
| Command | List |
@@ -798,9 +813,9 @@ CUSTOMIZATION *fzf-lua-customization*
798813
-- otherwise auto-detect prioritizes `fd`:`rg`:`find`
799814
-- default options are controlled by 'fd|rg|find|_opts'
800815
-- cmd = "rg --files",
801-
find_opts = [[-type f \! -path '*/.git/*']],
802-
rg_opts = [[--color=never --hidden --files -g "!.git"]],
803-
fd_opts = [[--color=never --hidden --type f --type l --exclude .git]],
816+
find_opts = [[-type f \! -path '*/.git/*' \! -path '*/.jj/*']],
817+
rg_opts = [[--color=never --hidden --files -g "!.git" -g "!.jj"]],
818+
fd_opts = [[--color=never --hidden --type f --type l --exclude .git --exclude .jj]],
804819
dir_opts = [[/s/b/a:-d]],
805820
-- by default, cwd appears in the header only if {opts} contain a cwd
806821
-- parameter to a different folder than the current working directory
@@ -1678,4 +1693,4 @@ I missed your name feel free to contact me and I'll add it below:
16781693
previewer code while using nvim-bqf
16791694
<https://github.com/kevinhwang91/nvim-bqf>
16801695

1681-
vim:tw=78:ts=8:ft=help:norl:
1696+
vim:tw=78:ts=8:ft=help:norl:

lua/fzf-lua/defaults.lua

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,9 @@ M.defaults.files = {
522522
_fzf_nth_devicons = true,
523523
git_status_cmd = {
524524
"git", "-c", "color.status=false", "--no-optional-locks", "status", "--porcelain=v1" },
525-
find_opts = [[-type f \! -path '*/.git/*']],
526-
rg_opts = [[--color=never --files -g "!.git"]],
527-
fd_opts = [[--color=never --type f --type l --exclude .git]],
525+
find_opts = [[-type f \! -path '*/.git/*' \! -path '*/.jj/*']],
526+
rg_opts = [[--color=never --files -g "!.git" -g "!.jj"]],
527+
fd_opts = [[--color=never --type f --type l --exclude .git --exclude .jj]],
528528
dir_opts = [[/s/b/a:-d]],
529529
hidden = true,
530530
toggle_ignore_flag = "--no-ignore",
@@ -894,6 +894,14 @@ M.defaults.git = {
894894
},
895895
}
896896

897+
M.defaults.jj = {
898+
---Jujutsu tracked files.
899+
files = vim.tbl_deep_extend("force", M.defaults.git.files, {
900+
cmd = "jj file list --ignore-working-copy",
901+
git_icons = false,
902+
}),
903+
}
904+
897905
---Grep using `rg`, `grep` or other grep commands.
898906
---@class fzf-lua.config.Grep: fzf-lua.config.Base
899907
---Shell command used to execute grep, default: auto detect `rg|grep`.

lua/fzf-lua/init.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ local lazyloaded_modules = {
263263
git_branches = { "fzf-lua.providers.git", "branches" },
264264
git_worktrees = { "fzf-lua.providers.git", "worktrees" },
265265
git_tags = { "fzf-lua.providers.git", "tags" },
266+
jj_files = { "fzf-lua.providers.jj", "files" },
267+
vcs_files = { "fzf-lua.providers.files", "vcs_files" },
266268
oldfiles = { "fzf-lua.providers.oldfiles", "oldfiles" },
267269
history = { "fzf-lua.providers.oldfiles", "history" },
268270
undotree = { "fzf-lua.providers.undotree", "undotree" },

lua/fzf-lua/path.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,28 @@ function M.git_root(opts, noerr)
617617
return output[1]
618618
end
619619

620+
---@param opts? table
621+
---@param noerr? boolean
622+
---@return boolean
623+
function M.is_jj_repo(opts, noerr)
624+
return not not M.jj_root(opts, noerr)
625+
end
626+
627+
---@param opts? table
628+
---@param noerr? boolean
629+
---@return string?
630+
function M.jj_root(opts, noerr)
631+
local cmd = (opts and opts.cwd)
632+
and { "jj", "-R", opts.cwd, "root", "--ignore-working-copy" }
633+
or { "jj", "root", "--ignore-working-copy" }
634+
local output, err = utils.io_systemlist(cmd)
635+
if err ~= 0 then
636+
if not noerr then utils.info(table.concat(output, "\n")) end
637+
return nil
638+
end
639+
return output[1]
640+
end
641+
620642
---@param str string
621643
---@param opts fzf-lua.config.Resolved
622644
---@return fzf-lua.path.Entry|fzf-lua.keymap.Entry

lua/fzf-lua/profiles/default-title.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ return {
2828
worktrees = title("Git Worktrees"),
2929
stash = title("Git Stash"),
3030
},
31+
jj = { files = title("JJ Files") },
3132
args = title("Args"),
3233
oldfiles = title("Oldfiles"),
3334
history = title("History"),

lua/fzf-lua/providers/files.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,21 @@ M.zoxide = function(opts)
172172
return core.fzf_exec(opts.cmd, opts)
173173
end
174174

175+
---VCS-aware file picker: uses jj_files in jj repos, git_files in git repos,
176+
---falls back to the regular files picker otherwise.
177+
---@param opts table|{}?
178+
---@return thread?, string?, table?
179+
M.vcs_files = function(opts)
180+
opts = opts or {}
181+
if path.is_jj_repo(opts, true) then
182+
opts.winopts = vim.tbl_deep_extend("keep", opts.winopts or {}, { title = " VCS Files (jj) " })
183+
return require("fzf-lua.providers.jj").files(opts)
184+
elseif path.is_git_repo(opts, true) then
185+
opts.winopts = vim.tbl_deep_extend("keep", opts.winopts or {}, { title = " VCS Files (git) " })
186+
return require("fzf-lua.providers.git").files(opts)
187+
else
188+
return M.files(opts)
189+
end
190+
end
191+
175192
return M

lua/fzf-lua/providers/jj.lua

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
local core = require "fzf-lua.core"
2+
local path = require "fzf-lua.path"
3+
local utils = require "fzf-lua.utils"
4+
local config = require "fzf-lua.config"
5+
6+
local M = {}
7+
8+
local function set_jj_cwd_args(opts)
9+
-- verify cwd is a jj repo, override user supplied
10+
-- cwd if cwd isn't a jj repo, error was already
11+
-- printed to `:messages` by 'path.jj_root'
12+
local jj_root = path.jj_root(opts)
13+
if not opts.cwd or not jj_root then
14+
opts.cwd = jj_root
15+
end
16+
return opts
17+
end
18+
19+
---@param opts table|{}?
20+
---@return thread?, string?, table?
21+
M.files = function(opts)
22+
opts = config.normalize_opts(opts, "jj.files")
23+
if not opts then return end
24+
opts = set_jj_cwd_args(opts)
25+
if not opts.cwd then return end
26+
return core.fzf_exec(opts.cmd, opts)
27+
end
28+
29+
return M

lua/fzf-lua/utils.lua

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,13 @@ end
12791279
---@return integer exit_code (0: success)
12801280
function M.io_systemlist(cmd)
12811281
if vim.system ~= nil then -- nvim 0.10+
1282-
local proc = vim.system(cmd):wait()
1282+
local ok, proc_or_err = pcall(vim.system, cmd)
1283+
if not ok then
1284+
-- Command doesn't exist or other error occurred
1285+
-- Return the error message in the output so callers can display it
1286+
return { tostring(proc_or_err) }, -1
1287+
end
1288+
local proc = proc_or_err:wait()
12831289
local output = (type(proc.stderr) == "string" and proc.stderr or "")
12841290
.. (type(proc.stdout) == "string" and proc.stdout or "")
12851291
return vim.split(output, "\n", { trimempty = true }), proc.code
@@ -1293,10 +1299,15 @@ end
12931299
---@return integer exit_code (0: success)
12941300
function M.io_system(cmd)
12951301
if vim.system ~= nil then -- nvim 0.10+
1296-
local proc = vim.system(cmd):wait()
1297-
local output = (type(proc.stderr) == "string" and proc.stderr or "")
1298-
.. (type(proc.stdout) == "string" and proc.stdout or "")
1299-
return output, proc.code
1302+
local ok, proc_or_err = pcall(function() return vim.system(cmd):wait() end)
1303+
if not ok then
1304+
-- Command doesn't exist or other error occurred
1305+
-- Return the error message so callers can display it
1306+
return tostring(proc_or_err), -1
1307+
end
1308+
local output = (type(proc_or_err.stderr) == "string" and proc_or_err.stderr or "")
1309+
.. (type(proc_or_err.stdout) == "string" and proc_or_err.stdout or "")
1310+
return output, proc_or_err.code
13001311
else
13011312
return vim.fn.system(cmd), vim.v.shell_error
13021313
end

0 commit comments

Comments
 (0)