Skip to content

Commit bd88635

Browse files
author
Sebastian Flügge
committed
fix: Escape special regex chars in buffer lookup
Buffer lookup failed for filenames containing [], (), . and other regex metacharacters. - Add utils.get_buffer_by_filename() with proper Vim regex escaping - Replace duplicate buffer lookup patterns across files
1 parent 2b91d9a commit bd88635

File tree

7 files changed

+120
-6
lines changed

7 files changed

+120
-6
lines changed

lua/orgmode/api/file.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---@diagnostic disable: invisible
22
local OrgHeadline = require('orgmode.api.headline')
3+
local utils = require('orgmode.utils')
34
local org = require('orgmode')
45

56
---@class OrgApiFile
@@ -113,7 +114,7 @@ end
113114
--- @return string
114115
function OrgFile:get_link()
115116
local filename = self.filename
116-
local bufnr = vim.fn.bufnr('^' .. filename .. '$')
117+
local bufnr = utils.get_buffer_by_filename(filename)
117118

118119
if bufnr == -1 or not vim.api.nvim_buf_is_loaded(bufnr) then
119120
-- do remote edit

lua/orgmode/api/headline.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local PriorityState = require('orgmode.objects.priority_state')
44
local Date = require('orgmode.objects.date')
55
local Calendar = require('orgmode.objects.calendar')
66
local Promise = require('orgmode.utils.promise')
7+
local utils = require('orgmode.utils')
78
local org = require('orgmode')
89

910
---@class OrgApiHeadline
@@ -276,7 +277,7 @@ end
276277
--- @return string
277278
function OrgHeadline:get_link()
278279
local filename = self.file.filename
279-
local bufnr = vim.fn.bufnr('^' .. filename .. '$')
280+
local bufnr = utils.get_buffer_by_filename(filename)
280281

281282
if bufnr == -1 or not vim.api.nvim_buf_is_loaded(bufnr) then
282283
-- do remote edit

lua/orgmode/api/init.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local OrgHeadline = require('orgmode.api.headline')
44
local orgmode = require('orgmode')
55
local validator = require('orgmode.utils.validator')
66
local Promise = require('orgmode.utils.promise')
7+
local utils = require('orgmode.utils')
78

89
---@class OrgApiRefileOpts
910
---@field source OrgApiHeadline
@@ -81,7 +82,7 @@ function OrgApi.refile(opts)
8182
refile_opts.destination_headline = opts.destination._section
8283
end
8384

84-
local source_bufnr = vim.fn.bufnr('^' .. opts.source.file.filename .. '$') or -1
85+
local source_bufnr = utils.get_buffer_by_filename(opts.source.file.filename)
8586
local is_capture = source_bufnr > -1 and vim.b[source_bufnr].org_capture
8687

8788
if is_capture and orgmode.capture._window then

lua/orgmode/files/file.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ end
6060
---Load the file
6161
---@return OrgPromise<OrgFile | false>
6262
function OrgFile.load(filename)
63-
local bufnr = vim.fn.bufnr('^' .. filename .. '$') or -1
63+
local bufnr = utils.get_buffer_by_filename(filename)
6464

6565
if
6666
bufnr > -1
@@ -534,7 +534,7 @@ end
534534

535535
---@return number
536536
function OrgFile:bufnr()
537-
local bufnr = vim.fn.bufnr('^' .. self.filename .. '$') or -1
537+
local bufnr = utils.get_buffer_by_filename(self.filename)
538538
-- Do not consider unloaded buffers as valid
539539
-- Treesitter is not working in them
540540
if bufnr > -1 and vim.api.nvim_buf_is_loaded(bufnr) then
@@ -546,7 +546,7 @@ end
546546
---Return valid buffer handle or throw an error if it's not valid
547547
---@return number
548548
function OrgFile:get_valid_bufnr()
549-
local bufnr = vim.fn.bufnr('^' .. self.filename .. '$') or -1
549+
local bufnr = utils.get_buffer_by_filename(self.filename)
550550
if bufnr < 0 then
551551
error('[orgmode] No valid buffer for file ' .. self.filename .. ' to edit', 0)
552552
end

lua/orgmode/utils/init.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,4 +622,11 @@ function utils.if_nil(...)
622622
return nil
623623
end
624624

625+
---Get buffer number by filename with proper regex escaping
626+
---@param filename string The filename to search for
627+
---@return number Buffer number or -1 if not found/loaded
628+
function utils.get_buffer_by_filename(filename)
629+
return vim.fn.bufnr('^' .. vim.fn.escape(filename, '^$.*?/\\[]~') .. '$')
630+
end
631+
625632
return utils

tests/plenary/files/file_spec.lua

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ describe('OrgFile', function()
4646
file:reload_sync()
4747
assert.are.same(4, file.metadata.changedtick)
4848
end)
49+
50+
it('should load files with special characters in filename from buffer', function()
51+
local special_filename = vim.fn.tempname() .. '[test].org'
52+
local file = load_file_sync({ '* Headline with bracket filename' }, special_filename)
53+
vim.cmd('edit ' .. vim.fn.fnameescape(file.filename))
54+
55+
-- Test that OrgFile.load can find the buffer correctly
56+
local loaded_file = OrgFile.load(special_filename):wait()
57+
assert.are.same(special_filename, loaded_file.filename)
58+
assert.are.same({ '* Headline with bracket filename' }, loaded_file.lines)
59+
60+
vim.cmd('bdelete')
61+
end)
4962
end)
5063

5164
describe('reload', function()
@@ -564,6 +577,42 @@ describe('OrgFile', function()
564577
assert.are.same(-1, file:bufnr())
565578
assert.is.True(vim.fn.bufnr(file.filename) > 0)
566579
end)
580+
581+
it('should work with filenames containing special characters', function()
582+
-- Test various special characters that have regex meaning
583+
local test_cases = {
584+
'[test].org',
585+
'(test).org',
586+
'test[1].org',
587+
'file.with.dots.org',
588+
'file+plus.org',
589+
'file*star.org',
590+
'file?question.org',
591+
'file$dollar.org',
592+
'file^caret.org',
593+
}
594+
595+
for _, special_filename in ipairs(test_cases) do
596+
local full_filename = vim.fn.tempname() .. special_filename
597+
local file = load_file_sync({
598+
'* Headline with special filename',
599+
' Content in special file',
600+
}, full_filename)
601+
602+
-- Test that bufnr() works correctly
603+
assert.are.same(-1, file:bufnr())
604+
605+
-- Test that loading the buffer works
606+
vim.cmd('edit ' .. vim.fn.fnameescape(file.filename))
607+
assert.is.True(file:bufnr() > 0, 'Failed for filename: ' .. special_filename)
608+
609+
-- Test that get_valid_bufnr() works
610+
local bufnr = file:get_valid_bufnr()
611+
assert.is.True(bufnr > 0, 'get_valid_bufnr failed for filename: ' .. special_filename)
612+
613+
vim.cmd('bdelete')
614+
end
615+
end)
567616
end)
568617

569618
describe('get_filetags', function()

tests/plenary/utils_spec.lua

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,59 @@ describe('Util', function()
135135
assert.are.equal(expected, err)
136136
end)
137137
end)
138+
139+
describe('get_buffer_by_filename', function()
140+
it('should return -1 for non-existent files', function()
141+
local result = utils.get_buffer_by_filename('/this/file/does/not/exist.org')
142+
assert.are.same(-1, result)
143+
end)
144+
145+
it('should return buffer number for loaded files', function()
146+
local file = helpers.create_file({ '* Test headline' })
147+
local result = utils.get_buffer_by_filename(file.filename)
148+
assert.is.True(result > 0)
149+
end)
150+
151+
it('should handle filenames with special regex characters', function()
152+
-- Test various special characters that would break unescaped regex
153+
local test_cases = {
154+
'[test].org',
155+
'(test).org',
156+
'test[1].org',
157+
'file.with.dots.org',
158+
'file+plus.org',
159+
'file*star.org',
160+
'file?question.org',
161+
'file$dollar.org',
162+
'file^caret.org',
163+
}
164+
165+
for _, special_filename in ipairs(test_cases) do
166+
local full_filename = vim.fn.tempname() .. special_filename
167+
vim.fn.writefile({ '* Test headline' }, full_filename)
168+
vim.cmd('edit ' .. vim.fn.fnameescape(full_filename))
169+
170+
local result = utils.get_buffer_by_filename(full_filename)
171+
assert.is.True(result > 0, 'Failed for filename: ' .. special_filename)
172+
173+
vim.cmd('bdelete')
174+
end
175+
end)
176+
177+
it('should return -1 for unloaded buffers', function()
178+
local file = helpers.create_file({ '* Test headline' })
179+
local filename = file.filename
180+
181+
-- First verify it works when loaded
182+
local result_loaded = utils.get_buffer_by_filename(filename)
183+
assert.is.True(result_loaded > 0)
184+
185+
-- Wipe the buffer (this actually unloads it from memory)
186+
vim.cmd('bwipeout')
187+
188+
-- Should return -1 for wiped buffer
189+
local result_unloaded = utils.get_buffer_by_filename(filename)
190+
assert.are.same(-1, result_unloaded)
191+
end)
192+
end)
138193
end)

0 commit comments

Comments
 (0)