Skip to content

Commit 5293738

Browse files
Merge pull request #272 from TravonteD/tree-sitter-actions
Use treesitter for buffer actions
2 parents b9de38e + b6aab53 commit 5293738

File tree

3 files changed

+195
-31
lines changed

3 files changed

+195
-31
lines changed

lua/orgmode/org/mappings.lua

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ local config = require('orgmode.config')
1010
local constants = require('orgmode.utils.constants')
1111
local ts_utils = require('nvim-treesitter.ts_utils')
1212
local utils = require('orgmode.utils')
13+
local tree_utils = require('orgmode.utils.treesitter')
14+
local Headline = require('orgmode.treesitter.headline')
1315

1416
---@class OrgMappings
1517
---@field capture Capture
@@ -307,21 +309,20 @@ function OrgMappings:priority_down()
307309
end
308310

309311
function OrgMappings:set_priority(direction)
310-
local item = Files.get_closest_headline()
311-
local priority = PriorityState:new(item.priority)
312+
local headline = Headline:new(tree_utils.closest_headline())
313+
local _, current_priority = headline:priority()
314+
local priority_state = PriorityState:new(current_priority)
312315

313316
local new_priority
314-
if direction then
315-
new_priority = direction == 'up' and priority:increase() or priority:decrease()
317+
if direction == 'up' then
318+
new_priority = priority_state:increase()
319+
elseif direction == 'down' then
320+
new_priority = priority_state:decrease()
316321
else
317-
new_priority = priority:prompt_user()
318-
end
319-
320-
if not new_priority then
321-
return
322+
new_priority = priority_state:prompt_user()
322323
end
323324

324-
item:set_priority(new_priority)
325+
headline:set_priority(new_priority)
325326
end
326327

327328
function OrgMappings:todo_next_state()
@@ -363,9 +364,8 @@ function OrgMappings:toggle_heading()
363364
end
364365

365366
function OrgMappings:_todo_change_state(direction)
366-
local item = Files.get_closest_headline()
367-
local was_done = item:is_done()
368-
local old_state = item.todo_keyword.value
367+
local headline = Headline:new(tree_utils.closest_headline())
368+
local _, old_state, was_done = headline:todo()
369369
local changed = self:_change_todo_state(direction, true)
370370
if not changed then
371371
return
@@ -379,10 +379,10 @@ function OrgMappings:_todo_change_state(direction)
379379
if #repeater_dates == 0 then
380380
local log_time = config.org_log_done == 'time'
381381
if log_time and item:is_done() and not was_done then
382-
item:add_closed_date()
382+
headline:add_closed_date()
383383
end
384384
if log_time and not item:is_done() and was_done then
385-
item:remove_closed_date()
385+
headline:remove_closed_date()
386386
end
387387
return item
388388
end
@@ -753,9 +753,9 @@ end
753753
---@param use_fast_access boolean
754754
---@return string
755755
function OrgMappings:_change_todo_state(direction, use_fast_access)
756-
local item = Files.get_closest_headline()
757-
local todo = item.todo_keyword
758-
local todo_state = TodoState:new({ current_state = todo.value })
756+
local headline = Headline:new(tree_utils.closest_headline())
757+
local todo, current_keyword = headline:todo()
758+
local todo_state = TodoState:new({ current_state = current_keyword })
759759
local next_state = nil
760760
if use_fast_access and todo_state:has_fast_access() then
761761
next_state = todo_state:open_fast_access()
@@ -773,25 +773,14 @@ function OrgMappings:_change_todo_state(direction, use_fast_access)
773773
return false
774774
end
775775

776-
if next_state.value == todo.value then
776+
if next_state.value == current_keyword then
777777
if todo.value ~= '' then
778778
utils.echo_info('TODO state was already ', { { next_state.value, next_state.hl } })
779779
end
780780
return false
781781
end
782782

783-
local linenr = item.range.start_line
784-
local stars = string.rep('%*', item.level)
785-
local old_state = vim.pesc(todo.value)
786-
if old_state ~= '' then
787-
old_state = old_state .. '%s+'
788-
end
789-
local new_state = next_state.value
790-
if new_state ~= '' then
791-
new_state = new_state .. ' '
792-
end
793-
local new_line = vim.fn.getline(linenr):gsub('^' .. stars .. '%s+' .. old_state, stars .. ' ' .. new_state)
794-
vim.fn.setline(linenr, new_line)
783+
headline:set_todo(next_state.value)
795784
return true
796785
end
797786

lua/orgmode/treesitter/headline.lua

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
local ts_utils = require('nvim-treesitter.ts_utils')
2+
local tree_utils = require('orgmode.utils.treesitter')
3+
local config = require('orgmode.config')
4+
local query = vim.treesitter.query
5+
6+
local Headline = {}
7+
8+
---@param headline userdata tree sitter headline node
9+
function Headline:new(headline_node)
10+
local data = { headline = headline_node }
11+
setmetatable(data, self)
12+
self.__index = self
13+
return data
14+
end
15+
16+
function Headline:stars()
17+
return self.headline:field('stars')[1]
18+
end
19+
20+
function Headline:priority()
21+
return self:parse('%[#(%w+)%]')
22+
end
23+
24+
function Headline:set_priority(priority)
25+
local current_priority = self:priority()
26+
if current_priority then
27+
local text = (vim.trim(priority) == '') and '' or ('[#%s]'):format(priority)
28+
tree_utils.set_node_text(current_priority, text)
29+
return
30+
end
31+
32+
local todo = self:todo()
33+
if todo then
34+
local text = query.get_node_text(todo, 0)
35+
tree_utils.set_node_text(todo, ('%s [#%s]'):format(text, priority))
36+
return
37+
end
38+
39+
local stars = self:stars()
40+
local text = query.get_node_text(stars, 0)
41+
tree_utils.set_node_text(stars, ('%s [#%s]'):format(text, priority))
42+
end
43+
44+
function Headline:set_todo(keyword)
45+
local current_todo = self:todo()
46+
if current_todo then
47+
tree_utils.set_node_text(current_todo, keyword)
48+
return
49+
end
50+
51+
local stars = self:stars()
52+
local text = query.get_node_text(stars, 0)
53+
tree_utils.set_node_text(stars, string.format('%s %s', text, keyword))
54+
end
55+
56+
function Headline:item()
57+
return self.headline:field('item')[1]
58+
end
59+
60+
-- Returns the headlines todo node, it's keyword,
61+
-- and if it's in done state
62+
-- @return Node, string, boolean
63+
function Headline:todo()
64+
local keywords = config.todo_keywords.ALL
65+
local done_keywords = config.todo_keywords.DONE
66+
for _, word in ipairs(keywords) do
67+
local todo = self:parse(word:gsub('-', '%%-'))
68+
if todo then
69+
return todo, word, vim.tbl_contains(done_keywords, word)
70+
end
71+
end
72+
end
73+
74+
function Headline:plan()
75+
local section = self.headline:parent()
76+
for _, node in ipairs(ts_utils.get_named_children(section)) do
77+
if node:type() == 'plan' then
78+
return node
79+
end
80+
end
81+
end
82+
83+
function Headline:dates()
84+
local plan = self:plan()
85+
local dates = {}
86+
for _, node in ipairs(ts_utils.get_named_children(plan)) do
87+
local name = vim.treesitter.query.get_node_text(node:named_child(0), 0)
88+
dates[name] = node
89+
end
90+
return dates
91+
end
92+
93+
function Headline:repeater_dates()
94+
return vim.tbl_filter(function(entry)
95+
local timestamp = entry:field('timestamp')[1]
96+
for _, node in ipairs(ts_utils.get_named_children(timestamp)) do
97+
if node:type() == 'repeat' then
98+
return true
99+
end
100+
end
101+
end, self:dates())
102+
end
103+
104+
function Headline:add_closed_date()
105+
local dates = self:dates()
106+
if vim.tbl_count(dates) == 0 or dates['CLOSED'] then
107+
return
108+
end
109+
local last_child = dates['DEADLINE'] or dates['SCHEDULED']
110+
local ptext = query.get_node_text(last_child, 0)
111+
local text = ptext .. ' CLOSED: [' .. vim.fn.strftime('%Y-%m-%d %a %H:%M') .. ']'
112+
tree_utils.set_node_text(last_child, text)
113+
end
114+
115+
function Headline:remove_closed_date()
116+
local dates = self:dates()
117+
if vim.tbl_count(dates) == 0 or not dates['CLOSED'] then
118+
return
119+
end
120+
tree_utils.set_node_text(dates['CLOSED'], '', true)
121+
end
122+
123+
-- @return tsnode, string
124+
function Headline:parse(pattern)
125+
local match = ''
126+
local matching_nodes = vim.tbl_filter(function(node)
127+
local text = query.get_node_text(node, 0) or ''
128+
local m = text:match(pattern)
129+
if m then
130+
match = text:match(pattern)
131+
return true
132+
end
133+
end, ts_utils.get_named_children(self:item()))
134+
return matching_nodes[1], match
135+
end
136+
137+
return Headline

lua/orgmode/utils/treesitter.lua

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
local ts_utils = require('nvim-treesitter.ts_utils')
2+
local config = require('orgmode.config')
3+
local M = {}
4+
5+
-- walks the tree to find a headline
6+
function M.find_headline(node)
7+
if node:type() == 'headline' then
8+
return node
9+
elseif node:type() == 'section' then
10+
-- The headline is always the first child of a section
11+
return ts_utils.get_named_children(node)[1]
12+
elseif node:parent() then
13+
return M.find_headline(node:parent())
14+
else
15+
return nil
16+
end
17+
end
18+
19+
-- returns the nearest headline
20+
function M.closest_headline()
21+
vim.treesitter.get_parser(0, 'org'):parse()
22+
return M.find_headline(ts_utils.get_node_at_cursor(vim.api.nvim_get_current_win()))
23+
end
24+
25+
-- @param front_trim boolean
26+
function M.set_node_text(node, text, front_trim)
27+
local sr, sc, er, ec = node:range()
28+
if string.len(text) == 0 then
29+
if front_trim then
30+
sc = sc - 1
31+
else
32+
ec = ec + 1
33+
end
34+
end
35+
vim.api.nvim_buf_set_text(0, sr, sc, er, ec, { text })
36+
end
37+
38+
return M

0 commit comments

Comments
 (0)