diff --git a/lua/orgmode/api/file.lua b/lua/orgmode/api/file.lua index 5488f04e6..57e22b5fa 100644 --- a/lua/orgmode/api/file.lua +++ b/lua/orgmode/api/file.lua @@ -1,5 +1,6 @@ ---@diagnostic disable: invisible local OrgHeadline = require('orgmode.api.headline') +local utils = require('orgmode.utils') local org = require('orgmode') ---@class OrgApiFile @@ -113,7 +114,7 @@ end --- @return string function OrgFile:get_link() local filename = self.filename - local bufnr = vim.fn.bufnr('^' .. filename .. '$') + local bufnr = utils.get_buffer_by_filename(filename) if bufnr == -1 or not vim.api.nvim_buf_is_loaded(bufnr) then -- do remote edit diff --git a/lua/orgmode/api/headline.lua b/lua/orgmode/api/headline.lua index f34cb8c5c..f5b12c291 100644 --- a/lua/orgmode/api/headline.lua +++ b/lua/orgmode/api/headline.lua @@ -4,6 +4,7 @@ local PriorityState = require('orgmode.objects.priority_state') local Date = require('orgmode.objects.date') local Calendar = require('orgmode.objects.calendar') local Promise = require('orgmode.utils.promise') +local utils = require('orgmode.utils') local org = require('orgmode') ---@class OrgApiHeadline @@ -276,7 +277,7 @@ end --- @return string function OrgHeadline:get_link() local filename = self.file.filename - local bufnr = vim.fn.bufnr('^' .. filename .. '$') + local bufnr = utils.get_buffer_by_filename(filename) if bufnr == -1 or not vim.api.nvim_buf_is_loaded(bufnr) then -- do remote edit diff --git a/lua/orgmode/api/init.lua b/lua/orgmode/api/init.lua index 95f7669d0..90e6c89ef 100644 --- a/lua/orgmode/api/init.lua +++ b/lua/orgmode/api/init.lua @@ -4,6 +4,7 @@ local OrgHeadline = require('orgmode.api.headline') local orgmode = require('orgmode') local validator = require('orgmode.utils.validator') local Promise = require('orgmode.utils.promise') +local utils = require('orgmode.utils') ---@class OrgApiRefileOpts ---@field source OrgApiHeadline @@ -81,7 +82,7 @@ function OrgApi.refile(opts) refile_opts.destination_headline = opts.destination._section end - local source_bufnr = vim.fn.bufnr('^' .. opts.source.file.filename .. '$') or -1 + local source_bufnr = utils.get_buffer_by_filename(opts.source.file.filename) local is_capture = source_bufnr > -1 and vim.b[source_bufnr].org_capture if is_capture and orgmode.capture._window then diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua index 0db8209aa..cd125dc82 100644 --- a/lua/orgmode/files/file.lua +++ b/lua/orgmode/files/file.lua @@ -60,7 +60,7 @@ end ---Load the file ---@return OrgPromise function OrgFile.load(filename) - local bufnr = vim.fn.bufnr('^' .. filename .. '$') or -1 + local bufnr = utils.get_buffer_by_filename(filename) if bufnr > -1 @@ -534,7 +534,7 @@ end ---@return number function OrgFile:bufnr() - local bufnr = vim.fn.bufnr('^' .. self.filename .. '$') or -1 + local bufnr = utils.get_buffer_by_filename(self.filename) -- Do not consider unloaded buffers as valid -- Treesitter is not working in them if bufnr > -1 and vim.api.nvim_buf_is_loaded(bufnr) then @@ -546,7 +546,7 @@ end ---Return valid buffer handle or throw an error if it's not valid ---@return number function OrgFile:get_valid_bufnr() - local bufnr = vim.fn.bufnr('^' .. self.filename .. '$') or -1 + local bufnr = utils.get_buffer_by_filename(self.filename) if bufnr < 0 then error('[orgmode] No valid buffer for file ' .. self.filename .. ' to edit', 0) end diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua index 1033cc3c0..40f1d6edc 100644 --- a/lua/orgmode/utils/init.lua +++ b/lua/orgmode/utils/init.lua @@ -622,4 +622,11 @@ function utils.if_nil(...) return nil end +---Get buffer number by filename with proper regex escaping +---@param filename string The filename to search for +---@return number Buffer number or -1 if not found/loaded +function utils.get_buffer_by_filename(filename) + return vim.fn.bufnr('^' .. vim.fn.escape(filename, '^$.*?/\\[]~') .. '$') +end + return utils diff --git a/tests/plenary/files/file_spec.lua b/tests/plenary/files/file_spec.lua index 7b17dc603..0f9e8977b 100644 --- a/tests/plenary/files/file_spec.lua +++ b/tests/plenary/files/file_spec.lua @@ -46,6 +46,19 @@ describe('OrgFile', function() file:reload_sync() assert.are.same(4, file.metadata.changedtick) end) + + it('should load files with special characters in filename from buffer', function() + local special_filename = vim.fn.tempname() .. '[test].org' + local file = load_file_sync({ '* Headline with bracket filename' }, special_filename) + vim.cmd('edit ' .. vim.fn.fnameescape(file.filename)) + + -- Test that OrgFile.load can find the buffer correctly + local loaded_file = OrgFile.load(special_filename):wait() + assert.are.same(special_filename, loaded_file.filename) + assert.are.same({ '* Headline with bracket filename' }, loaded_file.lines) + + vim.cmd('bdelete') + end) end) describe('reload', function() @@ -564,6 +577,42 @@ describe('OrgFile', function() assert.are.same(-1, file:bufnr()) assert.is.True(vim.fn.bufnr(file.filename) > 0) end) + + it('should work with filenames containing special characters', function() + -- Test various special characters that have regex meaning + local test_cases = { + '[test].org', + '(test).org', + 'test[1].org', + 'file.with.dots.org', + 'file+plus.org', + 'file*star.org', + 'file?question.org', + 'file$dollar.org', + 'file^caret.org', + } + + for _, special_filename in ipairs(test_cases) do + local full_filename = vim.fn.tempname() .. special_filename + local file = load_file_sync({ + '* Headline with special filename', + ' Content in special file', + }, full_filename) + + -- Test that bufnr() works correctly + assert.are.same(-1, file:bufnr()) + + -- Test that loading the buffer works + vim.cmd('edit ' .. vim.fn.fnameescape(file.filename)) + assert.is.True(file:bufnr() > 0, 'Failed for filename: ' .. special_filename) + + -- Test that get_valid_bufnr() works + local bufnr = file:get_valid_bufnr() + assert.is.True(bufnr > 0, 'get_valid_bufnr failed for filename: ' .. special_filename) + + vim.cmd('bdelete') + end + end) end) describe('get_filetags', function() diff --git a/tests/plenary/utils_spec.lua b/tests/plenary/utils_spec.lua index f2142b1ff..45c85abac 100644 --- a/tests/plenary/utils_spec.lua +++ b/tests/plenary/utils_spec.lua @@ -135,4 +135,59 @@ describe('Util', function() assert.are.equal(expected, err) end) end) + + describe('get_buffer_by_filename', function() + it('should return -1 for non-existent files', function() + local result = utils.get_buffer_by_filename('/this/file/does/not/exist.org') + assert.are.same(-1, result) + end) + + it('should return buffer number for loaded files', function() + local file = helpers.create_file({ '* Test headline' }) + local result = utils.get_buffer_by_filename(file.filename) + assert.is.True(result > 0) + end) + + it('should handle filenames with special regex characters', function() + -- Test various special characters that would break unescaped regex + local test_cases = { + '[test].org', + '(test).org', + 'test[1].org', + 'file.with.dots.org', + 'file+plus.org', + 'file*star.org', + 'file?question.org', + 'file$dollar.org', + 'file^caret.org', + } + + for _, special_filename in ipairs(test_cases) do + local full_filename = vim.fn.tempname() .. special_filename + vim.fn.writefile({ '* Test headline' }, full_filename) + vim.cmd('edit ' .. vim.fn.fnameescape(full_filename)) + + local result = utils.get_buffer_by_filename(full_filename) + assert.is.True(result > 0, 'Failed for filename: ' .. special_filename) + + vim.cmd('bdelete') + end + end) + + it('should return -1 for unloaded buffers', function() + local file = helpers.create_file({ '* Test headline' }) + local filename = file.filename + + -- First verify it works when loaded + local result_loaded = utils.get_buffer_by_filename(filename) + assert.is.True(result_loaded > 0) + + -- Wipe the buffer (this actually unloads it from memory) + vim.cmd('bwipeout') + + -- Should return -1 for wiped buffer + local result_unloaded = utils.get_buffer_by_filename(filename) + assert.are.same(-1, result_unloaded) + end) + end) end)