Skip to content

Commit 434e348

Browse files
committed
feat(completion): handle symlinked directories in file completion
1 parent 760b404 commit 434e348

3 files changed

Lines changed: 49 additions & 1 deletion

File tree

lua/opencode/ui/completion/files.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ local function create_file_item(file, suffix, priority)
8585
local dir = vim.fn.fnamemodify(file, ':h')
8686
local file_path = dir == '.' and filename or dir .. '/' .. filename
8787
local detail = dir == '.' and filename or dir .. '/' .. filename
88-
local full_path = vim.fn.fnamemodify(file, ':p')
88+
-- Build absolute path without resolving symlinks so that files inside
89+
-- symlinked directories within cwd pass the is_path_in_cwd check.
90+
local full_path = file:sub(1, 1) == '/' and file or (vim.fn.getcwd() .. '/' .. file)
8991
local display_label = file_path
9092

9193
local file_config = config.ui.completion.file_sources

lua/opencode/util.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,14 @@ end
592592

593593
function M.is_path_in_cwd(path)
594594
local cwd = vim.fn.getcwd()
595+
-- For relative paths, build the logical absolute path without resolving symlinks
596+
-- so that files inside symlinked directories within cwd are accepted.
597+
if path:sub(1, 1) ~= '/' then
598+
local logical = vim.fn.simplify(cwd .. '/' .. path)
599+
if logical:sub(1, #cwd) == cwd then
600+
return true
601+
end
602+
end
595603
local abs_path = vim.fn.fnamemodify(path, ':p')
596604
return abs_path:sub(1, #cwd) == cwd
597605
end

tests/unit/util_spec.lua

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,41 @@ describe('util.parse_quick_context_args', function()
378378
end)
379379
end
380380
end)
381+
382+
describe('util.is_path_in_cwd', function()
383+
local original_getcwd
384+
local test_cwd = '/tmp/test_project'
385+
386+
before_each(function()
387+
original_getcwd = vim.fn.getcwd
388+
vim.fn.getcwd = function()
389+
return test_cwd
390+
end
391+
end)
392+
393+
after_each(function()
394+
vim.fn.getcwd = original_getcwd
395+
end)
396+
397+
it('accepts a relative path inside cwd', function()
398+
assert.is_true(util.is_path_in_cwd('src/foo.lua'))
399+
end)
400+
401+
it('accepts an absolute path inside cwd', function()
402+
assert.is_true(util.is_path_in_cwd(test_cwd .. '/src/foo.lua'))
403+
end)
404+
405+
it('rejects an absolute path outside cwd', function()
406+
assert.is_false(util.is_path_in_cwd('/tmp/outside/foo.lua'))
407+
end)
408+
409+
it('rejects a relative path that escapes cwd via ..', function()
410+
assert.is_false(util.is_path_in_cwd('../outside/foo.lua'))
411+
end)
412+
413+
it('accepts a relative path through a symlinked directory in cwd', function()
414+
-- Simulate the symlink case: relative path stays logical (not resolved)
415+
-- e.g. linked_folder -> /tmp/external, linked_folder/test.txt is valid
416+
assert.is_true(util.is_path_in_cwd('linked_folder/test.txt'))
417+
end)
418+
end)

0 commit comments

Comments
 (0)