Skip to content

Commit 129acf8

Browse files
committed
Add support for nested cookies
1 parent 2ea0d6b commit 129acf8

File tree

6 files changed

+261
-95
lines changed

6 files changed

+261
-95
lines changed

lua/orgmode/org/mappings.lua

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ local ts_utils = require('nvim-treesitter.ts_utils')
1212
local utils = require('orgmode.utils')
1313
local tree_utils = require('orgmode.utils.treesitter')
1414
local Headline = require('orgmode.treesitter.headline')
15-
local List = require('orgmode.treesitter.list')
15+
local Listitem = require('orgmode.treesitter.listitem')
1616

1717
---@class OrgMappings
1818
---@field capture Capture
@@ -38,8 +38,6 @@ function OrgMappings:archive()
3838
return utils.echo_warning('This file is already an archive file.')
3939
end
4040
local item = file:get_closest_headline()
41-
file = Files.get_current_file()
42-
item = file:get_closest_headline()
4341
local archive_location = file:get_archive_file_location()
4442
local archive_directory = vim.fn.fnamemodify(archive_location, ':p:h')
4543
if vim.fn.isdirectory(archive_directory) == 0 then
@@ -156,20 +154,18 @@ function OrgMappings:global_cycle()
156154
return vim.cmd([[silent! norm!zx]])
157155
end
158156

159-
-- TODO: Add hierarchy
160157
function OrgMappings:toggle_checkbox()
161-
local line = vim.fn.getline('.')
162-
local pattern = '^(%s*[%-%+]%s*%[([%sXx%-]?)%])'
163-
local checkbox, state = line:match(pattern)
164-
if not checkbox then
165-
return
158+
local win_view = vim.fn.winsaveview()
159+
-- move to the first non-blank character so the current treesitter node is the listitem
160+
vim.cmd([[normal! _]])
161+
162+
vim.treesitter.get_parser(0, 'org'):parse()
163+
local listitem = tree_utils.find_parent_type(tree_utils.current_node(), 'listitem')
164+
if listitem then
165+
Listitem:new(listitem):update_checkbox('toggle')
166166
end
167-
local new_val = vim.trim(state) == '' and '[X]' or '[ ]'
168-
checkbox = checkbox:gsub('%[[%sXx%-]?%]$', new_val)
169-
local new_line = line:gsub(pattern, checkbox)
170-
vim.fn.setline('.', new_line)
171-
local list = List:new(tree_utils.closest_list())
172-
list:update_parent_cookie()
167+
168+
vim.fn.winrestview(win_view)
173169
end
174170

175171
function OrgMappings:timestamp_up_day()
@@ -463,12 +459,12 @@ function OrgMappings:handle_return(suffix)
463459
end
464460

465461
if item.type == 'paragraph' or item.type == 'bullet' then
466-
local list_item = item.node:parent()
467-
if list_item:type() ~= 'listitem' then
462+
local listitem = item.node:parent()
463+
if listitem:type() ~= 'listitem' then
468464
return
469465
end
470-
local line = vim.fn.getline(list_item:start() + 1)
471-
local end_row, _ = list_item:end_()
466+
local line = vim.fn.getline(listitem:start() + 1)
467+
local end_row, _ = listitem:end_()
472468
local next_line_node = current_file:get_node_at_cursor({ end_row + 1, 0 })
473469
local second_line_node = current_file:get_node_at_cursor({ end_row + 2, 0 })
474470
local is_end_of_file = next_line_node
@@ -503,7 +499,7 @@ function OrgMappings:handle_return(suffix)
503499
newText = plain_list .. ' \n',
504500
})
505501
elseif number_in_list then
506-
local next_sibling = list_item
502+
local next_sibling = listitem
507503
local counter = 1
508504
while next_sibling do
509505
local bullet = next_sibling:child(0)
@@ -531,6 +527,13 @@ function OrgMappings:handle_return(suffix)
531527
vim.lsp.util.apply_text_edits(text_edits, 0, constants.default_offset_encoding)
532528

533529
vim.fn.cursor(end_row + 1 + (add_empty_line and 1 or 0), 0) -- +1 for next line
530+
531+
-- update all parents when we insert a new checkbox
532+
if checkbox then
533+
local new_listitem = tree_utils.find_parent_type(tree_utils.current_node(), 'listitem')
534+
Listitem:new(new_listitem):update_checkbox('off')
535+
end
536+
534537
vim.cmd([[startinsert!]])
535538
end
536539
end

lua/orgmode/treesitter/headline.lua

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,38 @@ function Headline:remove_closed_date()
191191
end
192192

193193
function Headline:cookie()
194-
local cookie = self:parse('%[%d?/%d?%]')
194+
local cookie = self:parse('%[%d*/%d*%]')
195195
if cookie then
196196
return cookie
197197
end
198198
return self:parse('%[%d?%d?%d?%%%]')
199199
end
200200

201+
function Headline:update_cookie(list_node)
202+
local total_boxes = self:child_checkboxes(list_node)
203+
local checked_boxes = vim.tbl_filter(function(box)
204+
return box:match('%[%w%]')
205+
end, total_boxes)
206+
207+
local cookie = self:cookie()
208+
if cookie then
209+
local new_cookie_val
210+
if query.get_node_text(cookie, 0):find('%%') then
211+
new_cookie_val = ('[%d%%]'):format((#checked_boxes / #total_boxes) * 100)
212+
else
213+
new_cookie_val = ('[%d/%d]'):format(#checked_boxes, #total_boxes)
214+
end
215+
tree_utils.set_node_text(cookie, new_cookie_val)
216+
end
217+
end
218+
219+
function Headline:child_checkboxes(list_node)
220+
return vim.tbl_map(function(node)
221+
local text = query.get_node_text(node, 0)
222+
return text:match('%[.%]')
223+
end, ts_utils.get_named_children(list_node))
224+
end
225+
201226
-- @return tsnode, string
202227
function Headline:parse(pattern)
203228
local match = ''

lua/orgmode/treesitter/list.lua

Lines changed: 0 additions & 63 deletions
This file was deleted.

lua/orgmode/treesitter/listitem.lua

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
local ts_utils = require('nvim-treesitter.ts_utils')
2+
local tree_utils = require('orgmode.utils.treesitter')
3+
local query = vim.treesitter.query
4+
local Headline = require('orgmode.treesitter.headline')
5+
6+
local Listitem = {}
7+
8+
function Listitem:new(listitem_node)
9+
local data = { listitem = listitem_node }
10+
setmetatable(data, self)
11+
self.__index = self
12+
return data
13+
end
14+
15+
function Listitem:get_new_checkbox_value(action, current_value, total_child_checkboxes, checked_child_checkboxes)
16+
if action == 'on' then
17+
return '[X]'
18+
elseif action == 'off' then
19+
return '[ ]'
20+
elseif action == 'toggle' then
21+
return (current_value == '[X]' or current_value == '[x]') and '[ ]' or '[X]'
22+
elseif action == 'children' then
23+
if #checked_child_checkboxes == 0 then
24+
return '[ ]'
25+
elseif #checked_child_checkboxes == #total_child_checkboxes then
26+
return '[X]'
27+
end
28+
end
29+
return '[-]'
30+
end
31+
32+
function Listitem:checkbox()
33+
local contents = self.listitem:field('contents')
34+
for _, content in ipairs(contents) do
35+
if content:type() == 'paragraph' then
36+
for child in content:iter_children() do
37+
local text = query.get_node_text(child, 0)
38+
if text:match('%[[Xx-]%]') then
39+
return { text = text, range = { child:range() } }
40+
end
41+
42+
-- empty checkboxes are split into two nodes, so we need to hack this together
43+
local next_sibling = child:next_sibling()
44+
if next_sibling and text == '[' and query.get_node_text(next_sibling, 0) == ']' then
45+
local sr, sc, er, ec = child:range()
46+
return { text = '[ ]', range = { sr, sc, er, ec + 2 } }
47+
end
48+
end
49+
end
50+
end
51+
end
52+
53+
function Listitem:update_checkbox(action)
54+
action = action or 'toggle'
55+
56+
local checkbox = self:checkbox()
57+
local total_child_checkboxes = self:child_checkboxes() or {}
58+
local checked_child_checkboxes = vim.tbl_filter(function(box)
59+
return box:match('%[%w%]')
60+
end, total_child_checkboxes)
61+
62+
if checkbox then
63+
vim.api.nvim_buf_set_text(
64+
0,
65+
checkbox.range[1],
66+
checkbox.range[2],
67+
checkbox.range[3],
68+
checkbox.range[4],
69+
{ self:get_new_checkbox_value(action, checkbox.text, total_child_checkboxes, checked_child_checkboxes) }
70+
)
71+
end
72+
73+
self:update_cookie(total_child_checkboxes, checked_child_checkboxes)
74+
75+
local parent_list = tree_utils.find_parent_type(self.listitem, 'list')
76+
local parent_listitem = tree_utils.find_parent_type(parent_list, 'listitem')
77+
if parent_listitem then
78+
Listitem:new(parent_listitem):update_checkbox('children')
79+
else
80+
local parent_headline = tree_utils.closest_headline()
81+
if parent_headline then
82+
Headline:new(parent_headline):update_cookie(parent_list)
83+
end
84+
end
85+
end
86+
87+
function Listitem:child_checkboxes()
88+
local contents = self.listitem:field('contents')
89+
for _, content in ipairs(contents) do
90+
if content:type() == 'list' then
91+
return vim.tbl_map(function(node)
92+
local text = query.get_node_text(node, 0)
93+
return text:match('%[.%]')
94+
end, ts_utils.get_named_children(content))
95+
end
96+
end
97+
end
98+
99+
function Listitem:cookie()
100+
local content = self.listitem:field('contents')[1]
101+
-- The cookie should be the last thing on the line
102+
local cookie_node = content:named_child(content:named_child_count() - 1)
103+
104+
local text = query.get_node_text(cookie_node, 0)
105+
if text:match('%[%d*/%d*%]') or text:match('%[%d?%d?%d?%%%]') then
106+
return cookie_node
107+
end
108+
end
109+
110+
function Listitem:update_cookie(total_child_checkboxes, checked_child_checkboxes)
111+
local cookie = self:cookie()
112+
if cookie then
113+
local new_cookie_val
114+
if query.get_node_text(cookie, 0):find('%%') then
115+
new_cookie_val = ('[%d%%]'):format((#checked_child_checkboxes / #total_child_checkboxes) * 100)
116+
else
117+
new_cookie_val = ('[%d/%d]'):format(#checked_child_checkboxes, #total_child_checkboxes)
118+
end
119+
tree_utils.set_node_text(cookie, new_cookie_val)
120+
end
121+
end
122+
123+
return Listitem

lua/orgmode/utils/treesitter.lua

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
local ts_utils = require('nvim-treesitter.ts_utils')
2-
local config = require('orgmode.config')
3-
local query = vim.treesitter.query
42
local M = {}
53

64
function M.current_node()
@@ -28,23 +26,19 @@ function M.closest_headline()
2826
return M.find_headline(M.current_node())
2927
end
3028

31-
function M.find_list(node)
32-
if node:type() == 'list' then
29+
function M.find_parent_type(node, type)
30+
if node:type() == type then
3331
return node
3432
end
3533
if node:type() == 'body' then
3634
return nil
3735
end
38-
return M.find_list(node:parent())
39-
end
40-
41-
function M.closest_list()
42-
vim.treesitter.get_parser(0, 'org'):parse()
43-
return M.find_list(M.current_node())
36+
return M.find_parent_type(node:parent(), type)
4437
end
4538

4639
-- @param front_trim boolean
4740
function M.set_node_text(node, text, front_trim)
41+
local lines = vim.split(text, '\n', true)
4842
local sr, sc, er, ec = node:range()
4943
if string.len(text) == 0 then
5044
if front_trim then
@@ -53,7 +47,7 @@ function M.set_node_text(node, text, front_trim)
5347
ec = ec + 1
5448
end
5549
end
56-
vim.api.nvim_buf_set_text(0, sr, sc, er, ec, { text })
50+
pcall(vim.api.nvim_buf_set_text, 0, sr, sc, er, ec, lines)
5751
end
5852

5953
return M

0 commit comments

Comments
 (0)