Skip to content

Commit b525c77

Browse files
feat(files): Use TS string parser for non-nightly versions (#1060)
* feat(files): Use TS string parser for non-nightly versions * feat(files): Update changed metadata after updating lines
1 parent dc01c6f commit b525c77

File tree

4 files changed

+152
-33
lines changed

4 files changed

+152
-33
lines changed

lua/orgmode/colors/highlighter/markup/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ end
169169
---@return OrgMarkupPreparedHighlight[]
170170
function OrgMarkup:get_prepared_headline_highlights(headline)
171171
local highlights =
172-
self:get_node_highlights(headline:node(), headline.file:bufnr(), select(1, headline:node():range()))
172+
self:get_node_highlights(headline:node(), headline.file:get_source(), select(1, headline:node():range()))
173173

174174
local result = {}
175175

lua/orgmode/files/file.lua

Lines changed: 93 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +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
1213

1314
---@class OrgFileMetadata
1415
---@field mtime number File modified time in nanoseconds
@@ -17,13 +18,15 @@ local Memoize = require('orgmode.utils.memoize')
1718

1819
---@class OrgFileOpts
1920
---@field filename string
21+
---@field lines? string[]
2022
---@field buf? number
2123

2224
---@class OrgFile
2325
---@field filename string
2426
---@field buf number
2527
---@field index number
2628
---@field lines string[]
29+
---@field content string
2730
---@field metadata OrgFileMetadata
2831
---@field parser vim.treesitter.LanguageTree
2932
---@field root TSNode
@@ -45,18 +48,19 @@ function OrgFile:new(opts)
4548
filename = opts.filename,
4649
index = 0,
4750
buf = opts.buf or -1,
48-
lines = {},
51+
lines = opts.lines or {},
52+
content = table.concat(opts.lines or {}, '\n'),
4953
metadata = {
5054
mtime = stat and stat.mtime.nsec or 0,
5155
mtime_sec = stat and stat.mtime.sec or 0,
5256
changedtick = opts.buf and vim.api.nvim_buf_get_changedtick(opts.buf) or 0,
5357
},
5458
}
55-
if data.buf > 0 then
56-
data.lines = self:_get_lines(data.buf)
59+
local this = setmetatable(data, self)
60+
if this.buf > 0 then
61+
this:_update_lines(this:_get_lines(this.buf))
5762
end
58-
setmetatable(data, self)
59-
return data
63+
return this
6064
end
6165

6266
---Load the file
@@ -75,12 +79,23 @@ function OrgFile.load(filename)
7579
return Promise.resolve(false)
7680
end
7781

78-
bufnr = OrgFile._load_buffer(filename)
82+
-- TODO: Remove once Neovim adds string parser back
83+
-- See: https://github.com/nvim-orgmode/orgmode/issues/1049
84+
if is_nightly then
85+
bufnr = OrgFile._load_buffer(filename)
7986

80-
return Promise.resolve(OrgFile:new({
81-
filename = filename,
82-
buf = bufnr,
83-
}))
87+
return Promise.resolve(OrgFile:new({
88+
filename = filename,
89+
buf = bufnr,
90+
}))
91+
end
92+
93+
return utils.readfile(filename, { schedule = true }):next(function(lines)
94+
return OrgFile:new({
95+
filename = filename,
96+
lines = lines,
97+
})
98+
end)
8499
end
85100

86101
---Reload the file if it has been modified
@@ -94,12 +109,12 @@ function OrgFile:reload()
94109
local buf_changed = false
95110
local file_changed = false
96111

97-
if bufnr then
112+
if bufnr > -1 then
98113
local new_changedtick = vim.api.nvim_buf_get_changedtick(bufnr)
99114
buf_changed = self.metadata.changedtick ~= new_changedtick
100-
self.metadata.changedtick = new_changedtick
101115
if buf_changed then
102-
self.lines = self:_get_lines(bufnr)
116+
self:_update_lines(self:_get_lines(bufnr))
117+
self.metadata.changedtick = new_changedtick
103118
end
104119
end
105120
local stat = vim.uv.fs_stat(self.filename)
@@ -108,13 +123,15 @@ function OrgFile:reload()
108123
local new_mtime_sec = stat.mtime.sec
109124
file_changed = (new_mtime_nsec > 0 and self.metadata.mtime ~= new_mtime_nsec)
110125
or self.metadata.mtime_sec ~= new_mtime_sec
111-
self.metadata.mtime = new_mtime_nsec
112-
self.metadata.mtime_sec = new_mtime_sec
113126
end
114127

115128
if file_changed and not buf_changed then
116129
return utils.readfile(self.filename, { schedule = true }):next(function(lines)
117-
self.lines = lines
130+
self:_update_lines(lines)
131+
if stat then
132+
self.metadata.mtime = stat.mtime.nsec
133+
self.metadata.mtime_sec = stat.mtime.sec
134+
end
118135
return self
119136
end)
120137
end
@@ -184,7 +201,7 @@ function OrgFile:parse(skip_if_not_modified)
184201
if skip_if_not_modified and self.root and not self:is_modified() then
185202
return self.root
186203
end
187-
self.parser = ts.get_parser(self:bufnr(), 'org', {})
204+
self.parser = self:_get_parser()
188205
local trees = self.parser:parse()
189206
self.root = trees[1]:root()
190207
return self.root
@@ -203,7 +220,7 @@ function OrgFile:get_ts_matches(query, parent_node)
203220
local ts_query = ts_utils.get_query(query)
204221
local matches = {}
205222

206-
for _, match, _ in ts_query:iter_matches(parent_node, self:bufnr(), nil, nil, { all = true }) do
223+
for _, match, _ in ts_query:iter_matches(parent_node, self:get_source(), nil, nil, { all = true }) do
207224
local items = {}
208225
for id, nodes in pairs(match) do
209226
local name = ts_query.captures[id]
@@ -233,7 +250,7 @@ function OrgFile:get_ts_captures(query, node)
233250
local ts_query = ts_utils.get_query(query)
234251
local matches = {}
235252

236-
for _, match in ts_query:iter_captures(node, self:bufnr()) do
253+
for _, match in ts_query:iter_captures(node, self:get_source()) do
237254
table.insert(matches, match)
238255
end
239256
return matches
@@ -489,13 +506,13 @@ function OrgFile:get_node_text(node, range)
489506
return ''
490507
end
491508
if range then
492-
return ts.get_node_text(node, self:bufnr(), {
509+
return ts.get_node_text(node, self:get_source(), {
493510
metadata = {
494511
range = range,
495512
},
496513
})
497514
end
498-
return ts.get_node_text(node, self:bufnr())
515+
return ts.get_node_text(node, self:get_source())
499516
end
500517

501518
---@param node? TSNode
@@ -557,15 +574,27 @@ end
557574

558575
---@return number
559576
function OrgFile:bufnr()
560-
local bufnr = self.buf
577+
-- TODO: Remove once Neovim adds string parser back
578+
-- See: https://github.com/nvim-orgmode/orgmode/issues/1049
579+
if is_nightly then
580+
local bufnr = self.buf
581+
-- Do not consider unloaded buffers as valid
582+
-- Treesitter is not working in them
583+
if bufnr > -1 and vim.api.nvim_buf_is_loaded(bufnr) then
584+
return bufnr
585+
end
586+
local new_bufnr = self._load_buffer(self.filename)
587+
self.buf = new_bufnr
588+
return new_bufnr
589+
end
590+
591+
local bufnr = utils.get_buffer_by_filename(self.filename)
561592
-- Do not consider unloaded buffers as valid
562593
-- Treesitter is not working in them
563594
if bufnr > -1 and vim.api.nvim_buf_is_loaded(bufnr) then
564595
return bufnr
565596
end
566-
local new_bufnr = self._load_buffer(self.filename)
567-
self.buf = new_bufnr
568-
return new_bufnr
597+
return -1
569598
end
570599

571600
---@private
@@ -819,7 +848,7 @@ function OrgFile:get_links()
819848
(link_desc) @link
820849
]])
821850

822-
local source = self:bufnr()
851+
local source = self:get_source()
823852
for _, node in ipairs(matches) do
824853
table.insert(links, Hyperlink.from_node(node, source))
825854
end
@@ -840,7 +869,7 @@ function OrgFile:get_footnote_references()
840869

841870
local footnotes = {}
842871
local processed_lines = {}
843-
for _, match in ts_query:iter_captures(self.root, self:bufnr()) do
872+
for _, match in ts_query:iter_captures(self.root, self:get_source()) do
844873
local line_start, _, line_end = match:range()
845874
if not processed_lines[line_start] then
846875
if line_start == line_end then
@@ -947,6 +976,13 @@ function OrgFile:_get_directive(directive_name, all_matches)
947976
return nil
948977
end
949978

979+
function OrgFile:_update_lines(lines)
980+
self.lines = lines
981+
self.content = table.concat(lines, '\n')
982+
self:parse()
983+
return self
984+
end
985+
950986
---@private
951987
---Get all buffer lines, ensure empty buffer returns empty table
952988
---@return string[]
@@ -958,4 +994,34 @@ function OrgFile:_get_lines(bufnr)
958994
return lines
959995
end
960996

997+
---@private
998+
---@return vim.treesitter.LanguageTree
999+
function OrgFile:_get_parser()
1000+
local bufnr = self:bufnr()
1001+
1002+
if bufnr > -1 then
1003+
-- Always get the fresh parser for the buffer
1004+
return ts.get_parser(bufnr, 'org', {})
1005+
end
1006+
1007+
-- In case the buffer got unloaded, go back to string parser
1008+
if not self.parser or self:is_modified() or type(self.parser:source()) == 'number' then
1009+
return ts.get_string_parser(self.content, 'org', {})
1010+
end
1011+
1012+
return self.parser
1013+
end
1014+
1015+
--- Get the ts source for the file
1016+
--- If there is a buffer, return buffer number
1017+
--- Otherwise, return the string content
1018+
---@return integer | string
1019+
function OrgFile:get_source()
1020+
local bufnr = self:bufnr()
1021+
if bufnr > -1 then
1022+
return bufnr
1023+
end
1024+
return self.content
1025+
end
1026+
9611027
return OrgFile

lua/orgmode/files/headline.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ function Headline:get_plan_dates()
738738
if name ~= 'NONE' then
739739
has_plan_dates = true
740740
end
741-
dates[name:upper()] = Date.from_node(timestamp, self.file:bufnr(), {
741+
dates[name:upper()] = Date.from_node(timestamp, self.file:get_source(), {
742742
type = name:upper(),
743743
})
744744
dates_nodes[name:upper()] = node
@@ -792,7 +792,7 @@ function Headline:get_non_plan_dates()
792792
end
793793

794794
local all_dates = {}
795-
local source = self.file:bufnr()
795+
local source = self.file:get_source()
796796
for _, match in ipairs(matches) do
797797
local dates = Date.from_node(match, source)
798798
vim.list_extend(all_dates, dates)

tests/plenary/files/file_spec.lua

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
local OrgFile = require('orgmode.files.file')
22
local config = require('orgmode.config')
33
local Range = require('orgmode.files.elements.range')
4+
-- TODO: Remove once Neovim adds string parser back
5+
-- See: https://github.com/nvim-orgmode/orgmode/issues/1049
6+
local is_nightly = vim.fn.has('nvim-0.12') > 0
47

58
describe('OrgFile', function()
69
---@return OrgFile
@@ -20,7 +23,11 @@ describe('OrgFile', function()
2023
local stat = vim.uv.fs_stat(filename) or {}
2124
assert.are.same(stat.mtime.nsec, file.metadata.mtime)
2225
assert.are.same(stat.mtime.sec, file.metadata.mtime_sec)
23-
assert.are.same(2, file.metadata.changedtick)
26+
if is_nightly then
27+
assert.are.same(2, file.metadata.changedtick)
28+
else
29+
assert.are.same(0, file.metadata.changedtick)
30+
end
2431
end)
2532

2633
it('should not load a file that is not an org file', function()
@@ -39,10 +46,18 @@ describe('OrgFile', function()
3946
local stat = vim.uv.fs_stat(filename) or {}
4047
assert.are.same(stat.mtime.nsec, file.metadata.mtime)
4148
assert.are.same(stat.mtime.sec, file.metadata.mtime_sec)
42-
assert.are.same(2, file.metadata.changedtick)
49+
if is_nightly then
50+
assert.are.same(2, file.metadata.changedtick)
51+
else
52+
assert.are.same(0, file.metadata.changedtick)
53+
end
4354
vim.cmd('write!')
4455
file:reload_sync()
45-
assert.are.same(2, file.metadata.changedtick)
56+
if is_nightly then
57+
assert.are.same(2, file.metadata.changedtick)
58+
else
59+
assert.are.same(4, file.metadata.changedtick)
60+
end
4661
end)
4762

4863
it('should load files with special characters in filename from buffer', function()
@@ -477,6 +492,20 @@ describe('OrgFile', function()
477492
end)
478493

479494
describe('set_node_text', function()
495+
if not is_nightly then
496+
it('should throw an error if file is not loaded in buffer', function()
497+
local file = load_file_sync({
498+
'* Headline 1 :TAG:',
499+
' The content',
500+
' Multi line',
501+
})
502+
local paragraph_node = file:get_node_at_cursor():parent()
503+
assert.is.error_matches(function()
504+
return file:set_node_text(paragraph_node, 'New Text')
505+
end, '%[orgmode%] No valid buffer for file ' .. file.filename .. ' to edit')
506+
end)
507+
end
508+
480509
it('should set node text', function()
481510
local file = load_file_sync({
482511
'* Headline 1 :TAG:',
@@ -530,6 +559,30 @@ describe('OrgFile', function()
530559
end)
531560

532561
describe('bufnr', function()
562+
if not is_nightly then
563+
it('should return -1 if there is no buffer', function()
564+
local file = load_file_sync({
565+
'* Headline 1 :TAG:',
566+
' The content',
567+
' Multi line',
568+
})
569+
assert.are.same(-1, file:bufnr())
570+
end)
571+
572+
it('should return -1 if file is loaded in buffer but buffer is not loaded', function()
573+
local file = load_file_sync({
574+
'* Headline 1 :TAG:',
575+
' The content',
576+
' Multi line',
577+
})
578+
vim.cmd('edit ' .. file.filename)
579+
assert.is.True(file:bufnr() > 0)
580+
vim.cmd('bdelete')
581+
assert.are.same(-1, file:bufnr())
582+
assert.is.True(vim.fn.bufnr(file.filename) > 0)
583+
end)
584+
end
585+
533586
it('should return buffer number if file is loaded', function()
534587
local file = load_file_sync({
535588
'* Headline 1 :TAG:',

0 commit comments

Comments
 (0)