Skip to content

Commit 2e5fda4

Browse files
refactor: use treesitter to add properties to a headline
1 parent 4e38f49 commit 2e5fda4

File tree

8 files changed

+228
-69
lines changed

8 files changed

+228
-69
lines changed

lua/orgmode/clock/init.lua

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local Files = require('orgmode.parser.files')
2+
local tree_utils = require('orgmode.treesitter')
23
local Duration = require('orgmode.objects.duration')
34
local utils = require('orgmode.utils')
45
local Promise = require('orgmode.utils.promise')
@@ -75,18 +76,15 @@ function Clock:org_clock_goto()
7576
end
7677

7778
function Clock:org_set_effort()
78-
local item = Files.get_closest_headline()
79-
if not item then
80-
return
81-
end
79+
local item = tree_utils.closest_headline()
8280
-- TODO: Add Effort_ALL property as autocompletion
8381
local current_effort = item:get_property('Effort')
84-
local effort = vim.fn.OrgmodeInput('Effort: ', current_effort or '')
82+
local effort = vim.fn.OrgmodeInput('Effort: ', current_effort and current_effort.value or '')
8583
local duration = Duration.parse(effort)
8684
if duration == nil then
8785
return utils.echo_error('Invalid duration format: ' .. effort)
8886
end
89-
item:add_properties({ Effort = effort })
87+
item:set_property('Effort', effort)
9088
end
9189

9290
function Clock:get_statusline()

lua/orgmode/config/init.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,28 @@ function Config:respect_blank_before_new_entry(content, option, prepend_content)
353353
return content
354354
end
355355

356+
---@param amount number
357+
---@return string
356358
function Config:get_indent(amount)
357359
if self.opts.org_indent_mode == 'indent' then
358360
return string.rep(' ', amount)
359361
end
360362
return ''
361363
end
362364

365+
---@param content table|string
366+
---@param amount number
367+
function Config:apply_indent(content, amount)
368+
local indent = self:get_indent(amount)
369+
if type(content) ~= 'table' then
370+
return indent .. content
371+
end
372+
373+
for i, line in ipairs(content) do
374+
content[i] = indent .. line
375+
end
376+
return content
377+
end
378+
363379
instance = Config:new()
364380
return instance

lua/orgmode/org/mappings.lua

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,14 @@ function OrgMappings:archive()
4949
Files.reload(
5050
archive_location,
5151
vim.schedule_wrap(function()
52-
Files.update_file(archive_location, function(archive_file)
53-
-- Jump to last headline in archive file
54-
vim.cmd('normal! G')
55-
vim.fn.search([[^\*\+\s\+]], 'b')
56-
local last_item = archive_file:get_closest_headline()
57-
if not last_item then
58-
return
52+
Files.update_file(archive_location, function()
53+
local archived_headline = ts_org.find_headline(item.title, true)
54+
if archived_headline then
55+
archived_headline:set_property('ARCHIVE_TIME', Date.now():to_string())
56+
archived_headline:set_property('ARCHIVE_FILE', file.filename)
57+
archived_headline:set_property('ARCHIVE_CATEGORY', item.category)
58+
archived_headline:set_property('ARCHIVE_TODO', item.todo_keyword.value)
5959
end
60-
last_item:add_properties({
61-
ARCHIVE_TIME = Date.now():to_string(),
62-
ARCHIVE_FILE = file.filename,
63-
ARCHIVE_CATEGORY = item.category,
64-
ARCHIVE_TODO = item.todo_keyword.value,
65-
})
6660
end)
6761
end)
6862
)
@@ -444,8 +438,9 @@ function OrgMappings:_todo_change_state(direction)
444438
end
445439

446440
self:_change_todo_state('reset')
447-
local state_change =
448-
string.format('%s- State "%s" from "%s" [%s]', indent, item.todo_keyword.value, old_state, Date.now():to_string())
441+
local state_change = {
442+
string.format('%s- State "%s" from "%s" [%s]', indent, item.todo_keyword.value, old_state, Date.now():to_string()),
443+
}
449444

450445
dispatchEvent()
451446
return Promise.resolve()
@@ -459,20 +454,12 @@ function OrgMappings:_todo_change_state(direction)
459454
end)
460455
end)
461456
:next(function(note)
462-
local data = item:add_properties({ LAST_REPEAT = '[' .. Date.now():to_string() .. ']' })
457+
headline:set_property('LAST_REPEAT', Date.now():to_wrapped_string(false))
463458
if not note then
464459
return
465460
end
466-
467-
if data.is_new then
468-
vim.fn.append(data.end_line, note)
469-
return
470-
end
471-
item = Files.get_closest_headline()
472-
473-
if item.properties.valid then
474-
vim.fn.append(item.properties.range.end_line, note)
475-
end
461+
local properties_end_line = headline:properties():end_()
462+
vim.api.nvim_buf_set_lines(0, properties_end_line, properties_end_line, false, note)
476463
end)
477464
end
478465

lua/orgmode/parser/section.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ function Section:get_repeater_dates()
321321
end, self.dates)
322322
end
323323

324+
---@deprecated use treesitter.headline.set_property
324325
---@param properties table
325326
---@return table
326327
function Section:add_properties(properties)

lua/orgmode/treesitter/headline.lua

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,31 @@ function Headline:new(headline_node)
1717
return data
1818
end
1919

20+
---@param cursor? Table Cursor position tuple {row, col}
21+
---@return Headline|nil
22+
function Headline.from_cursor(cursor)
23+
local ts_headline = tree_utils.closest_headline(cursor)
24+
if not ts_headline then
25+
return nil
26+
end
27+
return Headline:new(ts_headline)
28+
end
29+
2030
---@return userdata stars node
2131
function Headline:stars()
2232
return self.headline:field('stars')[1]
2333
end
2434

35+
function Headline:refresh()
36+
tree_utils.parse()
37+
local start_row, start_col = self.headline:start()
38+
local updated_headline = Headline.from_cursor({ start_row + 1, start_col })
39+
if updated_headline then
40+
self.headline = updated_headline.headline
41+
end
42+
return self
43+
end
44+
2545
---@return number
2646
function Headline:level()
2747
local stars = self:stars()
@@ -150,6 +170,15 @@ function Headline:todo()
150170
end
151171
end
152172

173+
function Headline:title()
174+
local title = query.get_node_text(self:item(), 0) or ''
175+
local todo, word = self:todo()
176+
if todo then
177+
title = title:gsub('^' .. vim.pesc(word) .. '%s*', '')
178+
end
179+
return title
180+
end
181+
153182
---@return userdata
154183
function Headline:plan()
155184
local section = self.headline:parent()
@@ -160,6 +189,75 @@ function Headline:plan()
160189
end
161190
end
162191

192+
---@return userdata
193+
function Headline:properties()
194+
local section = self.headline:parent()
195+
for _, node in ipairs(ts_utils.get_named_children(section)) do
196+
if node:type() == 'property_drawer' then
197+
return node
198+
end
199+
end
200+
end
201+
202+
---@param name string
203+
---@param value string
204+
function Headline:set_property(name, value)
205+
local properties = self:properties()
206+
if not properties then
207+
local append_line = self:get_append_line()
208+
local property_drawer = self:_apply_indent({ ':PROPERTIES:', ':END:' })
209+
vim.api.nvim_buf_set_lines(0, append_line, append_line, false, property_drawer)
210+
tree_utils.parse()
211+
properties = self:refresh():properties()
212+
end
213+
214+
local property = (':%s: %s'):format(name, value)
215+
local existing_property = self:get_property(name)
216+
if existing_property then
217+
tree_utils.set_node_text(existing_property.node, property)
218+
return self:refresh()
219+
end
220+
local property_end = properties:end_()
221+
vim.api.nvim_buf_set_lines(0, property_end - 1, property_end - 1, false, { self:_apply_indent(property) })
222+
return self:refresh()
223+
end
224+
225+
---@param property_name string
226+
---@return table|nil
227+
function Headline:get_property(property_name)
228+
local properties = self:properties()
229+
if not properties then
230+
return nil
231+
end
232+
233+
for _, node in ipairs(ts_utils.get_named_children(properties)) do
234+
local name = node:field('name')[1]
235+
local value = node:field('value')[1]
236+
if name and query.get_node_text(name, 0):lower() == property_name:lower() then
237+
return {
238+
node = node,
239+
name = name,
240+
value = value and query.get_node_text(value, 0),
241+
}
242+
end
243+
end
244+
end
245+
246+
---Return the line number where content can be appended
247+
---
248+
---@return number
249+
function Headline:get_append_line()
250+
local properties = self:properties()
251+
if properties then
252+
return properties:end_()
253+
end
254+
local plan = self:plan()
255+
if plan then
256+
return plan:end_()
257+
end
258+
return self.headline:end_()
259+
end
260+
163261
---@return Table<string, userdata>
164262
function Headline:dates()
165263
local plan = self:plan()
@@ -341,4 +439,9 @@ function Headline:_remove_date(type)
341439
end
342440
end
343441

442+
---@param text table|string
443+
function Headline:_apply_indent(text)
444+
return config:apply_indent(text, self:level() + 1)
445+
end
446+
344447
return Headline

lua/orgmode/treesitter/init.lua

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ local Listitem = require('orgmode.treesitter.listitem')
55
---@param cursor? Table Cursor position tuple {row, col}
66
---@return Headline
77
local function closest_headline(cursor)
8-
local ts_headline = tree_utils.closest_headline(cursor)
8+
local ts_headline = Headline.from_cursor(cursor)
99
if not ts_headline then
1010
error('Unable to locate closest headline')
1111
end
12-
return Headline:new(ts_headline)
12+
return ts_headline
1313
end
1414

1515
local function listitem()
@@ -21,7 +21,33 @@ local function listitem()
2121
return nil
2222
end
2323

24+
---@param title string
25+
---@param exact? boolean
26+
local function find_headline(title, exact)
27+
local trees = vim.treesitter.get_parser(0, 'org', {}):parse()
28+
if #trees == 0 then
29+
return nil
30+
end
31+
local root = trees[1]:root()
32+
local ts_query = tree_utils.parse_query('(section (headline) @headline)')
33+
for _, match, _ in ts_query:iter_matches(root) do
34+
-- local items = {}
35+
for _, matched_node in pairs(match) do
36+
local headline = Headline:new(matched_node)
37+
local pattern = '^' .. vim.pesc(title:lower())
38+
if exact then
39+
pattern = pattern .. '$'
40+
end
41+
42+
if headline:title():lower():match(pattern) then
43+
return headline
44+
end
45+
end
46+
end
47+
end
48+
2449
return {
2550
closest_headline = closest_headline,
2651
listitem = listitem,
52+
find_headline = find_headline,
2753
}

lua/orgmode/utils/treesitter.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
local ts_utils = require('nvim-treesitter.ts_utils')
22
local parsers = require('nvim-treesitter.parsers')
33
local M = {}
4+
local query_cache = {}
45

56
function M.current_node()
67
local window = vim.api.nvim_get_current_win()
78
return ts_utils.get_node_at_cursor(window)
89
end
910

11+
function M.parse(bufnr)
12+
return vim.treesitter.get_parser(bufnr or 0, 'org', {}):parse()
13+
end
14+
15+
function M.parse_query(query)
16+
local ts_query = query_cache[query]
17+
if not ts_query then
18+
ts_query = vim.treesitter.query.parse_query('org', query)
19+
query_cache[query] = ts_query
20+
end
21+
return query_cache[query]
22+
end
23+
1024
---This is a full copy of nvim_treesiter get_node_at_cursor with support for custom cursor position
1125
---@param cursor? Table Cursor position tuple {row, col}
1226
---@param winnr? number

0 commit comments

Comments
 (0)