aboutsummaryrefslogtreecommitdiff
path: root/lua/muwiki/todo.lua
blob: 6978c54e3580219d01b03a71765c6f8eebb95ba7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
local M = {}

---Toggle checkbox at cursor position and all children
-- Uses treesitter to find markdown list items with checkboxes
function M.toggle_checkbox()
  local bufnr = vim.api.nvim_get_current_buf()
  local cursor = vim.api.nvim_win_get_cursor(0)
  local cursor_row = cursor[1] - 1 -- 0-indexed
  local cursor_col = cursor[2]

  -- Get markdown parser
  local ok, parser = pcall(vim.treesitter.get_parser, bufnr, 'markdown')
  if not ok or not parser then
    vim.notify('Markdown parser not available', vim.log.levels.WARN)
    return
  end

  -- Parse and get tree
  local trees = parser:parse()
  if not trees or #trees == 0 then
    return
  end

  local tree = trees[1]
  local root = tree:root()

  -- Find list_item node at cursor position
  local target_node = nil
  for node in root:iter_children() do
    if node:range() then
      local start_row, start_col, end_row, end_col = node:range()
      -- end_row is exclusive, so use < not <=
      if cursor_row >= start_row and cursor_row < end_row then
        -- Check if this is a list_item or contains one
        target_node = M.find_list_item_at(node, cursor_row, cursor_col)
        if target_node then
          break
        end
      end
    end
  end

  if not target_node then
    return
  end

  -- Toggle the target and its children
  local new_state = M.toggle_list_item(bufnr, target_node)
  if new_state then
    M.toggle_children(bufnr, target_node, new_state)
  end
end

---Find list_item node containing cursor position
-- Searches children first to return the deepest matching node
function M.find_list_item_at(node, cursor_row, cursor_col)
  -- Check children first to find the deepest match
  for child in node:iter_children() do
    local result = M.find_list_item_at(child, cursor_row, cursor_col)
    if result then
      return result
    end
  end

  -- Then check this node
  if node:type() == 'list_item' then
    local start_row, start_col, end_row, end_col = node:range()
    -- end_row is exclusive, so use < not <=
    if cursor_row >= start_row and cursor_row < end_row then
      return node
    end
  end

  return nil
end

---Toggle a single list item's checkbox
-- Returns the new state ('x' or ' ') or nil if not a task
function M.toggle_list_item(bufnr, node)
  -- Find task_list_marker
  for child in node:iter_children() do
    local child_type = child:type()
    if child_type == 'task_list_marker_unchecked' then
      -- Replace [ ] with [x]
      local start_row, start_col, end_row, end_col = child:range()
      vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[x]' })
      return 'x'
    elseif child_type == 'task_list_marker_checked' then
      -- Replace [x] with [ ]
      local start_row, start_col, end_row, end_col = child:range()
      vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[ ]' })
      return ' '
    end
  end

  return nil
end

---Toggle all child list items to match parent state
function M.toggle_children(bufnr, parent_node, target_state)
  -- Find nested list
  for child in parent_node:iter_children() do
    if child:type() == 'list' then
      -- Iterate through list_items in the nested list
      for list_item in child:iter_children() do
        if list_item:type() == 'list_item' then
          -- Force this child to target state
          M.force_list_item_state(bufnr, list_item, target_state)
          -- Recursively handle grandchildren
          M.toggle_children(bufnr, list_item, target_state)
        end
      end
      break
    end
  end
end

---Force a list item to specific state without toggling
function M.force_list_item_state(bufnr, node, target_state)
  for child in node:iter_children() do
    local child_type = child:type()
    if child_type == 'task_list_marker_unchecked' and target_state == 'x' then
      -- Need to check it
      local start_row, start_col, end_row, end_col = child:range()
      vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[x]' })
      return
    elseif child_type == 'task_list_marker_checked' and target_state == ' ' then
      -- Need to uncheck it
      local start_row, start_col, end_row, end_col = child:range()
      vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, { '[ ]' })
      return
    end
  end
end

return M