|
| 1 | +---@diagnostic disable: undefined-field, missing-fields |
| 2 | +-- Regression test for https://github.com/dmtrKovalenko/fff.nvim/issues/389 |
| 3 | +-- When find_files_in_dir(dir) runs with dir != neovim's cwd, the Rust indexer |
| 4 | +-- reports paths relative to `dir`, but the Lua side used to resolve them |
| 5 | +-- against neovim's cwd when calling :edit / preview / quickfix — so files |
| 6 | +-- opened as phantom buffers and previews showed "No preview available". |
| 7 | + |
| 8 | +local fff_rust = require('fff.rust') |
| 9 | +local picker_ui = require('fff.picker_ui') |
| 10 | +local file_picker = require('fff.file_picker') |
| 11 | + |
| 12 | +--- Normalise a path so that comparisons work on every OS. |
| 13 | +--- Windows complicates things: Rust may return forward-slash paths while |
| 14 | +--- vim.fn.resolve uses backslashes, temp paths may contain 8.3 short names |
| 15 | +--- (RUNNER~1), and the filesystem is case-insensitive. |
| 16 | +--- vim.uv.fs_realpath expands 8.3 names on Windows (unlike vim.fn.resolve). |
| 17 | +--- @param p string |
| 18 | +--- @return string |
| 19 | +local function norm(p) |
| 20 | + -- fs_realpath is the closest Lua equivalent of Rust's std::fs::canonicalize |
| 21 | + -- and expands 8.3 short names on Windows. |
| 22 | + local rp = vim.uv.fs_realpath(p) or vim.fn.fnamemodify(vim.fn.resolve(p), ':p') |
| 23 | + local n = vim.fs.normalize(rp) |
| 24 | + -- Strip trailing slash for consistent comparison |
| 25 | + n = n:gsub('/$', '') |
| 26 | + -- Case-fold on Windows (drive letters, 8.3 short names, etc.) |
| 27 | + if vim.fn.has('win32') == 1 then n = n:lower() end |
| 28 | + return n |
| 29 | +end |
| 30 | + |
| 31 | +--- `change_indexing_directory` swaps the picker on a background thread, so the |
| 32 | +--- `FILE_PICKER` global may still point at the *old* picker for a moment — |
| 33 | +--- `wait_for_initial_scan` on the old picker returns immediately and the |
| 34 | +--- search then runs against the new, still-empty index. Poll `health_check` |
| 35 | +--- until `base_path` matches the expected dir before waiting on the scan. |
| 36 | +local function wait_for_reindex(expected_dir, timeout_ms) |
| 37 | + local expected = norm(expected_dir) |
| 38 | + local deadline = vim.uv.hrtime() + timeout_ms * 1e6 |
| 39 | + while vim.uv.hrtime() < deadline do |
| 40 | + local ok, health = pcall(fff_rust.health_check, expected) |
| 41 | + if ok and health and health.file_picker and health.file_picker.base_path then |
| 42 | + if norm(health.file_picker.base_path) == expected then return true end |
| 43 | + end |
| 44 | + vim.wait(20, function() return false end) |
| 45 | + end |
| 46 | + return false |
| 47 | +end |
| 48 | + |
| 49 | +local function wait_for_scan(expected_dir, timeout_ms) |
| 50 | + assert.is_true(wait_for_reindex(expected_dir, timeout_ms), 'reindex to ' .. expected_dir .. ' did not complete') |
| 51 | + fff_rust.wait_for_initial_scan(timeout_ms) |
| 52 | +end |
| 53 | + |
| 54 | +describe('picker find_files_in_dir path resolution (issue #389)', function() |
| 55 | + local sandbox_root, target_dir, other_cwd, target_filename |
| 56 | + |
| 57 | + before_each(function() |
| 58 | + sandbox_root = vim.fn.tempname() |
| 59 | + target_dir = sandbox_root .. '/target-dir' |
| 60 | + other_cwd = sandbox_root .. '/other-cwd' |
| 61 | + vim.fn.mkdir(target_dir, 'p') |
| 62 | + vim.fn.mkdir(other_cwd, 'p') |
| 63 | + |
| 64 | + target_filename = 'issue389_target.lua' |
| 65 | + local fd = assert(io.open(target_dir .. '/' .. target_filename, 'w')) |
| 66 | + fd:write('-- issue #389 regression fixture\nreturn true\n') |
| 67 | + fd:close() |
| 68 | + |
| 69 | + -- Clear the DirChanged autocmd that a previous test run (e.g. fff_core_spec) |
| 70 | + -- may have installed. Without this, the :cd below triggers a scheduled |
| 71 | + -- change_indexing_directory(other_cwd) that races with our explicit |
| 72 | + -- change_indexing_directory(target_dir) and overwrites the FILE_PICKER. |
| 73 | + pcall(vim.api.nvim_del_augroup_by_name, 'fff_file_tracking') |
| 74 | + |
| 75 | + vim.cmd('cd ' .. vim.fn.fnameescape(other_cwd)) |
| 76 | + -- Equivalent to require('fff').setup({}) — just seeds vim.g.fff — but |
| 77 | + -- avoids the top-level fff module lookup which plenary's sandboxed |
| 78 | + -- require can miss depending on package.path. |
| 79 | + vim.g.fff = {} |
| 80 | + file_picker.setup() |
| 81 | + end) |
| 82 | + |
| 83 | + after_each(function() |
| 84 | + pcall(picker_ui.close) |
| 85 | + pcall(fff_rust.stop_background_monitor) |
| 86 | + pcall(fff_rust.cleanup_file_picker) |
| 87 | + if sandbox_root then vim.fn.delete(sandbox_root, 'rf') end |
| 88 | + end) |
| 89 | + |
| 90 | + it(':edit opens the file inside base_path even when neovim cwd differs', function() |
| 91 | + assert.are_not.equal(norm(target_dir), norm(vim.fn.getcwd())) |
| 92 | + |
| 93 | + assert.is_true(picker_ui.change_indexing_directory(target_dir)) |
| 94 | + wait_for_scan(target_dir, 10000) |
| 95 | + |
| 96 | + local items = file_picker.search_files('', nil, nil, nil, nil) |
| 97 | + assert.is_true(#items > 0, 'indexer returned no items for target_dir (norm=' .. norm(target_dir) .. ')') |
| 98 | + |
| 99 | + local target_item |
| 100 | + for _, item in ipairs(items) do |
| 101 | + if item.name == target_filename then |
| 102 | + target_item = item |
| 103 | + break |
| 104 | + end |
| 105 | + end |
| 106 | + assert.is_not_nil(target_item, 'target fixture file missing from results') |
| 107 | + -- On Windows Rust may use backslash separators; compare just the filename. |
| 108 | + local rel = target_item.relative_path |
| 109 | + assert.are.equal(target_filename, rel:match('[^/\\]+$') or rel) |
| 110 | + assert.is_nil( |
| 111 | + vim.uv.fs_stat(target_item.relative_path), |
| 112 | + 'relative_path should not resolve against cwd — if it does, the test fixture is wrong' |
| 113 | + ) |
| 114 | + |
| 115 | + -- Drive the same code path that user hits when pressing <CR>: populate |
| 116 | + -- UI state as open_ui_with_state would and invoke select('edit'). |
| 117 | + picker_ui.state.active = true |
| 118 | + picker_ui.state.filtered_items = items |
| 119 | + picker_ui.state.cursor = (function() |
| 120 | + for i, item in ipairs(items) do |
| 121 | + if item.name == target_filename then return i end |
| 122 | + end |
| 123 | + return 1 |
| 124 | + end)() |
| 125 | + picker_ui.state.query = '' |
| 126 | + picker_ui.state.mode = nil |
| 127 | + picker_ui.state.location = nil |
| 128 | + picker_ui.state.suggestion_source = nil |
| 129 | + picker_ui.state.selected_files = {} |
| 130 | + picker_ui.state.selected_items = {} |
| 131 | + |
| 132 | + picker_ui.select('edit') |
| 133 | + |
| 134 | + local bufname = vim.api.nvim_buf_get_name(0) |
| 135 | + assert.is_true(bufname ~= '', 'expected :edit to open a buffer with a non-empty name') |
| 136 | + |
| 137 | + local stat = vim.uv.fs_stat(bufname) |
| 138 | + assert.is_not_nil(stat, 'opened buffer points at a non-existent file: ' .. bufname) |
| 139 | + |
| 140 | + -- The opened file must be the fixture inside target_dir, not a phantom |
| 141 | + -- file under cwd. |
| 142 | + local expected = norm(target_dir .. '/' .. target_filename) |
| 143 | + local actual = norm(bufname) |
| 144 | + assert.are.equal(expected, actual) |
| 145 | + end) |
| 146 | +end) |
0 commit comments