From 434e348db29a06ca7f8a36010f28891bc41989e8 Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Sun, 22 Mar 2026 15:40:05 -0400 Subject: [PATCH 1/2] feat(completion): handle symlinked directories in file completion --- lua/opencode/ui/completion/files.lua | 4 ++- lua/opencode/util.lua | 8 ++++++ tests/unit/util_spec.lua | 38 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lua/opencode/ui/completion/files.lua b/lua/opencode/ui/completion/files.lua index 249d2868..2761da52 100644 --- a/lua/opencode/ui/completion/files.lua +++ b/lua/opencode/ui/completion/files.lua @@ -85,7 +85,9 @@ local function create_file_item(file, suffix, priority) local dir = vim.fn.fnamemodify(file, ':h') local file_path = dir == '.' and filename or dir .. '/' .. filename local detail = dir == '.' and filename or dir .. '/' .. filename - local full_path = vim.fn.fnamemodify(file, ':p') + -- Build absolute path without resolving symlinks so that files inside + -- symlinked directories within cwd pass the is_path_in_cwd check. + local full_path = file:sub(1, 1) == '/' and file or (vim.fn.getcwd() .. '/' .. file) local display_label = file_path local file_config = config.ui.completion.file_sources diff --git a/lua/opencode/util.lua b/lua/opencode/util.lua index 77c3ed44..c1a9215c 100644 --- a/lua/opencode/util.lua +++ b/lua/opencode/util.lua @@ -592,6 +592,14 @@ end function M.is_path_in_cwd(path) local cwd = vim.fn.getcwd() + -- For relative paths, build the logical absolute path without resolving symlinks + -- so that files inside symlinked directories within cwd are accepted. + if path:sub(1, 1) ~= '/' then + local logical = vim.fn.simplify(cwd .. '/' .. path) + if logical:sub(1, #cwd) == cwd then + return true + end + end local abs_path = vim.fn.fnamemodify(path, ':p') return abs_path:sub(1, #cwd) == cwd end diff --git a/tests/unit/util_spec.lua b/tests/unit/util_spec.lua index 42e83d22..64226558 100644 --- a/tests/unit/util_spec.lua +++ b/tests/unit/util_spec.lua @@ -378,3 +378,41 @@ describe('util.parse_quick_context_args', function() end) end end) + +describe('util.is_path_in_cwd', function() + local original_getcwd + local test_cwd = '/tmp/test_project' + + before_each(function() + original_getcwd = vim.fn.getcwd + vim.fn.getcwd = function() + return test_cwd + end + end) + + after_each(function() + vim.fn.getcwd = original_getcwd + end) + + it('accepts a relative path inside cwd', function() + assert.is_true(util.is_path_in_cwd('src/foo.lua')) + end) + + it('accepts an absolute path inside cwd', function() + assert.is_true(util.is_path_in_cwd(test_cwd .. '/src/foo.lua')) + end) + + it('rejects an absolute path outside cwd', function() + assert.is_false(util.is_path_in_cwd('/tmp/outside/foo.lua')) + end) + + it('rejects a relative path that escapes cwd via ..', function() + assert.is_false(util.is_path_in_cwd('../outside/foo.lua')) + end) + + it('accepts a relative path through a symlinked directory in cwd', function() + -- Simulate the symlink case: relative path stays logical (not resolved) + -- e.g. linked_folder -> /tmp/external, linked_folder/test.txt is valid + assert.is_true(util.is_path_in_cwd('linked_folder/test.txt')) + end) +end) From 16c9f006eea45d8436a49b91bac3482d970085ac Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Mon, 23 Mar 2026 07:05:52 -0400 Subject: [PATCH 2/2] feat(util): support symlinked directories in is_path_in_cwd --- lua/opencode/util.lua | 20 ++++++++++---------- tests/unit/util_spec.lua | 8 ++++++++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lua/opencode/util.lua b/lua/opencode/util.lua index c1a9215c..cde1b4ee 100644 --- a/lua/opencode/util.lua +++ b/lua/opencode/util.lua @@ -591,17 +591,17 @@ function M.pcall_trace(fn, ...) end function M.is_path_in_cwd(path) - local cwd = vim.fn.getcwd() - -- For relative paths, build the logical absolute path without resolving symlinks - -- so that files inside symlinked directories within cwd are accepted. - if path:sub(1, 1) ~= '/' then - local logical = vim.fn.simplify(cwd .. '/' .. path) - if logical:sub(1, #cwd) == cwd then - return true - end + local cwd = vim.fn.simplify(vim.fn.getcwd()) + local cwd_prefix = cwd == '/' and cwd or (cwd .. '/') + + local logical_path + if path:sub(1, 1) == '/' then + logical_path = vim.fn.simplify(path) + else + logical_path = vim.fn.simplify(cwd .. '/' .. path) end - local abs_path = vim.fn.fnamemodify(path, ':p') - return abs_path:sub(1, #cwd) == cwd + + return logical_path == cwd or logical_path:sub(1, #cwd_prefix) == cwd_prefix end --- Check if a given path is in the system temporary directory. diff --git a/tests/unit/util_spec.lua b/tests/unit/util_spec.lua index 64226558..778b5aa8 100644 --- a/tests/unit/util_spec.lua +++ b/tests/unit/util_spec.lua @@ -402,10 +402,18 @@ describe('util.is_path_in_cwd', function() assert.is_true(util.is_path_in_cwd(test_cwd .. '/src/foo.lua')) end) + it('accepts an absolute path through a symlinked directory in cwd', function() + assert.is_true(util.is_path_in_cwd(test_cwd .. '/linked_folder/test.txt')) + end) + it('rejects an absolute path outside cwd', function() assert.is_false(util.is_path_in_cwd('/tmp/outside/foo.lua')) end) + it('rejects a path that only shares the cwd prefix', function() + assert.is_false(util.is_path_in_cwd('/tmp/test_project2/src/foo.lua')) + end) + it('rejects a relative path that escapes cwd via ..', function() assert.is_false(util.is_path_in_cwd('../outside/foo.lua')) end)