Skip to content

fix: Escape special regex chars in buffer lookup #1022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lua/orgmode/api/file.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---@diagnostic disable: invisible
local OrgHeadline = require('orgmode.api.headline')
local utils = require('orgmode.utils')
local org = require('orgmode')

---@class OrgApiFile
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lua/orgmode/api/headline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lua/orgmode/api/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lua/orgmode/files/file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ end
---Load the file
---@return OrgPromise<OrgFile | false>
function OrgFile.load(filename)
local bufnr = vim.fn.bufnr('^' .. filename .. '$') or -1
local bufnr = utils.get_buffer_by_filename(filename)

if
bufnr > -1
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lua/orgmode/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 49 additions & 0 deletions tests/plenary/files/file_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
55 changes: 55 additions & 0 deletions tests/plenary/utils_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading