Skip to content

Commit dc9864f

Browse files
perf(files): Cache buffer numbers by filename (#1080)
* perf(files): Cache buffer numbers by filename * perf(agenda): Cache search query * perf(buffers): Resolve symlinks in buffers * fix(buffers): correctly resolve path to buffer
1 parent 3629625 commit dc9864f

11 files changed

Lines changed: 175 additions & 74 deletions

File tree

lua/orgmode/agenda/types/tags.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ function OrgAgendaTagsType:prepare()
5454
end
5555

5656
function OrgAgendaTagsType:get_file_headlines(file)
57-
local headlines = file:apply_search(Search:new(self.match_query), self.todo_only)
57+
-- Cache search object to avoid re-parsing same query for every file
58+
-- Re-create if query changed (e.g., user refreshed with new search)
59+
if not self._cached_search or self._cached_search.term ~= self.match_query then
60+
self._cached_search = Search:new(self.match_query)
61+
end
62+
local headlines = file:apply_search(self._cached_search, self.todo_only)
5863
if self.todo_ignore_deadlines then
5964
headlines = vim.tbl_filter(function(headline) ---@cast headline OrgHeadline
6065
local deadline_date = headline:get_deadline_date()

lua/orgmode/api/file.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---@diagnostic disable: invisible
22
local OrgHeadline = require('orgmode.api.headline')
3-
local utils = require('orgmode.utils')
43
local org = require('orgmode')
4+
local Buffers = require('orgmode.state.buffers')
55

66
---@class OrgApiFile
77
---@field category string current file category name. By default it's only filename without extension unless defined differently via #+CATEGORY directive
@@ -114,7 +114,7 @@ end
114114
--- @return string
115115
function OrgFile:get_link()
116116
local filename = self.filename
117-
local bufnr = utils.get_buffer_by_filename(filename)
117+
local bufnr = Buffers.get_buffer_by_filename(filename)
118118

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

lua/orgmode/api/headline.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ 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')
87
local org = require('orgmode')
8+
local Buffers = require('orgmode.state.buffers')
99

1010
---@class OrgApiHeadline
1111
---@field title string headline title without todo keyword, tags and priority. Ex. `* TODO I am a headline :SOMETAG:` returns `I am a headline`
@@ -277,7 +277,7 @@ end
277277
--- @return string
278278
function OrgHeadline:get_link()
279279
local filename = self.file.filename
280-
local bufnr = utils.get_buffer_by_filename(filename)
280+
local bufnr = Buffers.get_buffer_by_filename(filename)
281281

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

lua/orgmode/api/init.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ local OrgFile = require('orgmode.api.file')
33
local OrgHeadline = require('orgmode.api.headline')
44
local orgmode = require('orgmode')
55
local Promise = require('orgmode.utils.promise')
6-
local utils = require('orgmode.utils')
6+
local Buffers = require('orgmode.state.buffers')
77

88
---@class OrgApiRefileOpts
99
---@field source OrgApiHeadline
@@ -81,7 +81,7 @@ function OrgApi.refile(opts)
8181
refile_opts.destination_headline = opts.destination._section
8282
end
8383

84-
local source_bufnr = utils.get_buffer_by_filename(opts.source.file.filename)
84+
local source_bufnr = Buffers.get_buffer_by_filename(opts.source.file.filename)
8585
local is_capture = source_bufnr > -1 and vim.b[source_bufnr].org_capture
8686
if is_capture then
8787
local capture_window = orgmode.capture._windows[vim.b[source_bufnr].org_capture_window_id]

lua/orgmode/files/file.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ local Hyperlink = require('orgmode.org.links.hyperlink')
99
local Range = require('orgmode.files.elements.range')
1010
local Footnote = require('orgmode.objects.footnote')
1111
local Memoize = require('orgmode.utils.memoize')
12-
local is_nightly = vim.fn.has('nvim-0.12') > 0
12+
local Buffers = require('orgmode.state.buffers')
1313

1414
---@class OrgFileMetadata
1515
---@field mtime number File modified time in nanoseconds
@@ -66,7 +66,7 @@ end
6666
---Load the file
6767
---@return OrgPromise<OrgFile | false>
6868
function OrgFile.load(filename)
69-
local bufnr = utils.get_buffer_by_filename(filename)
69+
local bufnr = Buffers.get_buffer_by_filename(filename)
7070

7171
if bufnr > -1 and vim.api.nvim_buf_is_loaded(bufnr) and vim.bo[bufnr].filetype == 'org' then
7272
return Promise.resolve(OrgFile:new({
@@ -563,7 +563,7 @@ end
563563

564564
---@return number
565565
function OrgFile:bufnr()
566-
local bufnr = utils.get_buffer_by_filename(self.filename)
566+
local bufnr = Buffers.get_buffer_by_filename(self.filename)
567567
-- Do not consider unloaded buffers as valid
568568
-- Treesitter is not working in them
569569
if bufnr > -1 and vim.api.nvim_buf_is_loaded(bufnr) then

lua/orgmode/init.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ local auto_instance_keys = {
1919
---@field setup_called boolean
2020
---@field files OrgFiles
2121
---@field highlighter OrgHighlighter
22+
---@field buffers OrgBuffers
2223
---@field agenda OrgAgenda
2324
---@field capture OrgCapture
2425
---@field clock OrgClock
@@ -49,6 +50,7 @@ function Org:init()
4950
if self.initialized then
5051
return
5152
end
53+
self.buffers = require('orgmode.state.buffers').init()
5254
require('orgmode.events').init()
5355
self.highlighter = require('orgmode.colors.highlighter'):new()
5456
require('orgmode.colors.highlights').define_highlights()
@@ -123,6 +125,26 @@ function Org:setup_autocmds()
123125
end
124126
end,
125127
})
128+
129+
vim.api.nvim_create_autocmd({ 'BufNew' }, {
130+
pattern = { '*.org', '*.org_archive' },
131+
group = org_augroup,
132+
callback = function(event)
133+
if self.buffers then
134+
self.buffers.add(event.buf)
135+
end
136+
end,
137+
})
138+
139+
vim.api.nvim_create_autocmd('BufWipeout', {
140+
pattern = { '*.org', '*.org_archive' },
141+
group = org_augroup,
142+
callback = function(event)
143+
if self.buffers then
144+
self.buffers.remove(event.buf)
145+
end
146+
end,
147+
})
126148
end
127149

128150
---@param opts? OrgConfigOpts

lua/orgmode/state/buffers.lua

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---@class OrgBuffers
2+
---@field private _bufs table<string, number>
3+
local OrgBuffers = {
4+
_bufs = {},
5+
}
6+
7+
function OrgBuffers.init()
8+
local all_buffers = vim.api.nvim_list_bufs()
9+
local valid_buffers = {}
10+
for _, bufnr in ipairs(all_buffers) do
11+
local valid_buffer_name = OrgBuffers.get_valid_buffer_name(bufnr)
12+
if valid_buffer_name then
13+
valid_buffers[valid_buffer_name] = bufnr
14+
end
15+
end
16+
17+
OrgBuffers._bufs = valid_buffers
18+
return OrgBuffers
19+
end
20+
21+
---Return the buffer number for a given filename
22+
---If filename has org extension (.org or .org_archive), it will return the buffer number directly
23+
---If filename does not have org extension, it will try to find the buffer number and return
24+
---@param filename string absolute path to file
25+
function OrgBuffers.get_buffer_by_filename(filename)
26+
local resolved_filename = vim.fn.resolve(filename)
27+
28+
if OrgBuffers._bufs[resolved_filename] then
29+
return OrgBuffers._bufs[resolved_filename]
30+
end
31+
32+
-- If filename does not have an org extension, try to find the buf number and return it if filetype is org
33+
if not OrgBuffers._is_valid_file_name(resolved_filename) then
34+
local bufnr = vim.fn.bufnr(resolved_filename)
35+
if bufnr > -1 and vim.bo[bufnr].filetype == 'org' then
36+
return bufnr
37+
end
38+
end
39+
40+
return -1
41+
end
42+
43+
---Add the buffer to the list
44+
---@param bufnr number
45+
function OrgBuffers.add(bufnr)
46+
local name = OrgBuffers.get_valid_buffer_name(bufnr)
47+
48+
if name then
49+
OrgBuffers._bufs[name] = bufnr
50+
end
51+
end
52+
53+
---Remove the buffer from the list
54+
---@param bufnr number
55+
function OrgBuffers.remove(bufnr)
56+
local name = vim.fn.resolve(vim.api.nvim_buf_get_name(bufnr))
57+
58+
if OrgBuffers._bufs[name] then
59+
OrgBuffers._bufs[name] = nil
60+
end
61+
end
62+
63+
---Get valid buffer name if the buffer is an org file
64+
---@param bufnr number
65+
function OrgBuffers.get_valid_buffer_name(bufnr)
66+
local name = vim.fn.resolve(vim.fn.bufname(bufnr))
67+
68+
if OrgBuffers._is_valid_file_name(name) then
69+
return name
70+
end
71+
72+
return nil
73+
end
74+
75+
---Check if given filename has valid org extension
76+
---@private
77+
---@param filename string
78+
function OrgBuffers._is_valid_file_name(filename)
79+
filename = filename or ''
80+
return filename:sub(-4) == '.org' or filename:sub(-12) == '.org_archive'
81+
end
82+
83+
return OrgBuffers

lua/orgmode/utils/init.lua

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

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

tests/plenary/helpers.lua

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
local OrgFile = require('orgmode.files.file')
21
local orgmode = require('orgmode')
32

43
local M = {}
@@ -29,7 +28,7 @@ end
2928

3029
---@param path string
3130
function M.load_file(path)
32-
vim.cmd.edit(path)
31+
vim.cmd.edit(vim.fn.fnameescape(path))
3332
return orgmode.files:get(path)
3433
end
3534

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
local Buffers = require('orgmode.state.buffers')
2+
local helpers = require('tests.plenary.helpers')
3+
4+
describe('Buffers', function()
5+
it('should return -1 for non-existent files', function()
6+
local result = Buffers.get_buffer_by_filename('/this/file/does/not/exist.org')
7+
assert.are.same(-1, result)
8+
end)
9+
10+
it('should return buffer number for loaded files', function()
11+
local file = helpers.create_file({ '* Test headline' }, 'some_filename.org')
12+
local result = Buffers.get_buffer_by_filename(file.filename)
13+
assert.is.True(result > 0)
14+
end)
15+
16+
it('should handle filenames with special regex characters', function()
17+
-- Test various special characters that would break unescaped regex
18+
local test_cases = {
19+
'[test].org',
20+
'(test).org',
21+
'test[1].org',
22+
'file.with.dots.org',
23+
'file+plus.org',
24+
'file*star.org',
25+
'file?question.org',
26+
'file$dollar.org',
27+
'file^caret.org',
28+
}
29+
30+
for _, special_filename in ipairs(test_cases) do
31+
local file = helpers.create_file({ '* Test headline' }, special_filename)
32+
local result = Buffers.get_buffer_by_filename(file.filename)
33+
assert.is.True(result > 0, 'Failed for filename: ' .. special_filename)
34+
35+
vim.cmd('bdelete')
36+
end
37+
end)
38+
39+
it('should return -1 for unloaded buffers', function()
40+
local file = helpers.create_file({ '* Test headline' })
41+
local filename = file.filename
42+
43+
-- First verify it works when loaded
44+
local result_loaded = Buffers.get_buffer_by_filename(filename)
45+
assert.is.True(result_loaded > 0)
46+
47+
-- Wipe the buffer (this actually unloads it from memory)
48+
vim.cmd('bwipeout')
49+
50+
-- Should return -1 for wiped buffer
51+
local result_unloaded = Buffers.get_buffer_by_filename(filename)
52+
assert.are.same(-1, result_unloaded)
53+
end)
54+
end)

0 commit comments

Comments
 (0)