Skip to content

Commit d8ddcad

Browse files
authored
Merge pull request #50 from YousefHadder/refactor/phase2-split-list-module
refactor: split list module into focused sub-modules (Phase 2.2)
2 parents c8732c9 + 570440b commit d8ddcad

File tree

6 files changed

+886
-815
lines changed

6 files changed

+886
-815
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
-- Checkbox management module for markdown-plus.nvim
2+
local utils = require("markdown-plus.utils")
3+
local parser = require("markdown-plus.list.parser")
4+
local M = {}
5+
6+
---Toggle checkbox on a specific line
7+
---@param line_num number 1-indexed line number
8+
function M.toggle_checkbox_on_line(line_num)
9+
local line = utils.get_line(line_num)
10+
if line == "" then
11+
return
12+
end
13+
14+
local list_info = parser.parse_list_line(line)
15+
16+
if not list_info then
17+
return -- Not a list item, do nothing
18+
end
19+
20+
local new_line = M.toggle_checkbox_in_line(line, list_info)
21+
if new_line then
22+
utils.set_line(line_num, new_line)
23+
end
24+
end
25+
26+
---Toggle checkbox state in a line
27+
---@param line string The line content
28+
---@param list_info table The parsed list information
29+
---@return string|nil The modified line, or nil if no change
30+
function M.toggle_checkbox_in_line(line, list_info)
31+
if list_info.checkbox then
32+
-- Has checkbox - toggle between checked/unchecked
33+
return M.replace_checkbox_state(line, list_info)
34+
else
35+
-- No checkbox - add one
36+
return M.add_checkbox_to_line(line, list_info)
37+
end
38+
end
39+
40+
---Replace checkbox state in a line
41+
---@param line string The line content
42+
---@param list_info table The parsed list information
43+
---@return string The modified line
44+
function M.replace_checkbox_state(line, list_info)
45+
local indent = list_info.indent
46+
local marker = list_info.marker
47+
48+
-- Find the checkbox pattern and extract the content after it
49+
local checkbox_pattern = "^(" .. utils.escape_pattern(indent) .. utils.escape_pattern(marker) .. "%s*)%[.?%]%s*(.*)"
50+
51+
local prefix, content = line:match(checkbox_pattern)
52+
53+
if prefix and content ~= nil then
54+
local current_state = list_info.checkbox
55+
local new_state = (current_state == "x" or current_state == "X") and " " or "x"
56+
return prefix .. "[" .. new_state .. "] " .. content
57+
end
58+
59+
return line
60+
end
61+
62+
---Add checkbox to a line that doesn't have one
63+
---@param line string The line content
64+
---@param list_info table The parsed list information
65+
---@return string The modified line
66+
function M.add_checkbox_to_line(line, list_info)
67+
local indent = list_info.indent
68+
local marker = list_info.marker
69+
70+
-- Pattern to match list item and capture content
71+
local list_pattern = "^(" .. utils.escape_pattern(indent) .. utils.escape_pattern(marker) .. "%s*)(.*)"
72+
73+
local prefix, content = line:match(list_pattern)
74+
75+
if prefix and content ~= nil then
76+
return prefix .. "[ ] " .. content
77+
end
78+
79+
return line
80+
end
81+
82+
---Toggle checkbox on current line (normal mode)
83+
function M.toggle_checkbox_line()
84+
local cursor = utils.get_cursor()
85+
local row = cursor[1]
86+
M.toggle_checkbox_on_line(row)
87+
end
88+
89+
---Toggle checkbox in visual range
90+
function M.toggle_checkbox_range()
91+
local start_row = vim.fn.line("v")
92+
local end_row = vim.fn.line(".")
93+
94+
if start_row == 0 or end_row == 0 then
95+
return
96+
end
97+
98+
-- Ensure start is before end
99+
if start_row > end_row then
100+
start_row, end_row = end_row, start_row
101+
end
102+
103+
for row = start_row, end_row do
104+
M.toggle_checkbox_on_line(row)
105+
end
106+
end
107+
108+
---Toggle checkbox in insert mode (maintains cursor position)
109+
function M.toggle_checkbox_insert()
110+
local cursor = utils.get_cursor()
111+
local row = cursor[1]
112+
local col = cursor[2]
113+
114+
local old_line = utils.get_line(row)
115+
M.toggle_checkbox_on_line(row)
116+
117+
-- Restore cursor position (adjusting for potential line length changes)
118+
local new_line = utils.get_line(row)
119+
120+
-- Calculate the character delta to adjust cursor position
121+
local old_len = #old_line
122+
local new_len = #new_line
123+
local delta = new_len - old_len
124+
125+
local new_col
126+
-- Adjust cursor position by the delta to maintain visual position
127+
if delta > 0 then
128+
-- Characters were added (e.g., checkbox added), move cursor forward
129+
new_col = math.min(col + delta, #new_line)
130+
elseif delta < 0 then
131+
-- Characters were removed (e.g., checkbox removed), move cursor backward
132+
new_col = math.max(0, col + delta)
133+
new_col = math.min(new_col, #new_line)
134+
else
135+
-- No change in length (e.g., toggling checkbox state)
136+
new_col = math.min(col, #new_line)
137+
end
138+
139+
vim.api.nvim_win_set_cursor(0, { row, new_col })
140+
end
141+
142+
return M
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
-- List input handlers module for markdown-plus.nvim
2+
local utils = require("markdown-plus.utils")
3+
local parser = require("markdown-plus.list.parser")
4+
local M = {}
5+
6+
---Extract content from a list line after the marker
7+
---@param line string The line to extract from
8+
---@param list_info table List information
9+
---@return string Content after the marker
10+
local function extract_list_content(line, list_info)
11+
local marker_end = #list_info.indent + #list_info.full_marker
12+
return line:sub(marker_end + 1):match("^%s*(.*)") or ""
13+
end
14+
15+
---Break out of list (remove current empty item)
16+
---@param list_info table List information
17+
function M.break_out_of_list(list_info)
18+
local cursor = utils.get_cursor()
19+
local row = cursor[1]
20+
21+
-- Replace current line with just the indentation
22+
utils.set_line(row, list_info.indent)
23+
24+
-- Position cursor at end of line
25+
utils.set_cursor(row, #list_info.indent)
26+
end
27+
28+
---Create next list item
29+
---@param list_info table List information
30+
function M.create_next_list_item(list_info)
31+
local cursor = utils.get_cursor()
32+
local row = cursor[1]
33+
34+
local next_marker = parser.get_next_marker(list_info)
35+
36+
-- Build next line
37+
local next_line = list_info.indent .. next_marker .. " "
38+
if list_info.checkbox then
39+
next_line = next_line .. "[ ] "
40+
end
41+
42+
-- Insert new line
43+
utils.insert_line(row + 1, next_line)
44+
45+
-- Move cursor to new line at end
46+
utils.set_cursor(row + 1, #next_line)
47+
end
48+
49+
---Handle Enter key in lists
50+
function M.handle_enter()
51+
local current_line = utils.get_current_line()
52+
local cursor = utils.get_cursor()
53+
local row, col = cursor[1], cursor[2]
54+
55+
-- Check if we're in a list
56+
local list_info = parser.parse_list_line(current_line)
57+
58+
if not list_info then
59+
-- Not in a list, simulate default Enter behavior
60+
local line_before = current_line:sub(1, col)
61+
local line_after = current_line:sub(col + 1)
62+
63+
utils.set_line(row, line_before)
64+
utils.insert_line(row + 1, line_after)
65+
utils.set_cursor(row + 1, 0)
66+
return
67+
end
68+
69+
-- Check if current list item is empty
70+
if parser.is_empty_list_item(current_line, list_info) then
71+
-- Empty list item - break out of list
72+
M.break_out_of_list(list_info)
73+
return
74+
end
75+
76+
-- Create next list item
77+
M.create_next_list_item(list_info)
78+
end
79+
80+
---Handle Tab key for indentation
81+
function M.handle_tab()
82+
local current_line = utils.get_current_line()
83+
local list_info = parser.parse_list_line(current_line)
84+
85+
if not list_info then
86+
-- Not in a list, insert a tab character or spaces
87+
local cursor = utils.get_cursor()
88+
local row, col = cursor[1], cursor[2]
89+
local indent = string.rep(" ", vim.bo.shiftwidth or 2)
90+
local new_line = current_line:sub(1, col) .. indent .. current_line:sub(col + 1)
91+
utils.set_line(row, new_line)
92+
utils.set_cursor(row, col + #indent)
93+
return
94+
end
95+
96+
-- Increase indentation
97+
local cursor = utils.get_cursor()
98+
local row, col = cursor[1], cursor[2]
99+
local indent_size = vim.bo.shiftwidth
100+
101+
local new_indent = list_info.indent .. string.rep(" ", indent_size)
102+
local content = extract_list_content(current_line, list_info)
103+
local new_line = new_indent .. list_info.full_marker .. " " .. content
104+
105+
utils.set_line(row, new_line)
106+
utils.set_cursor(row, col + indent_size)
107+
end
108+
109+
---Handle Shift+Tab key for outdentation
110+
function M.handle_shift_tab()
111+
local current_line = utils.get_current_line()
112+
local list_info = parser.parse_list_line(current_line)
113+
114+
if not list_info then
115+
-- Not a list line: remove up to shiftwidth spaces from start
116+
local cursor = utils.get_cursor()
117+
local row, col = cursor[1], cursor[2]
118+
local indent_size = vim.bo.shiftwidth or 2
119+
local leading = current_line:match("^(%s*)")
120+
local to_remove = math.min(#leading, indent_size)
121+
if to_remove > 0 then
122+
local new_line = current_line:sub(to_remove + 1)
123+
utils.set_line(row, new_line)
124+
local new_col = math.max(0, col - to_remove)
125+
utils.set_cursor(row, new_col)
126+
end
127+
return
128+
end
129+
130+
-- Decrease indentation
131+
local cursor = utils.get_cursor()
132+
local row, col = cursor[1], cursor[2]
133+
local indent_size = vim.bo.shiftwidth
134+
135+
-- Can't outdent if already at root level
136+
if #list_info.indent < indent_size then
137+
return
138+
end
139+
140+
local new_indent = list_info.indent:sub(1, #list_info.indent - indent_size)
141+
local content = extract_list_content(current_line, list_info)
142+
local new_line = new_indent .. list_info.full_marker .. " " .. content
143+
144+
utils.set_line(row, new_line)
145+
146+
-- Adjust cursor position
147+
local new_col = math.max(0, col - indent_size)
148+
utils.set_cursor(row, new_col)
149+
end
150+
151+
---Handle Backspace key
152+
function M.handle_backspace()
153+
local current_line = utils.get_current_line()
154+
local cursor = utils.get_cursor()
155+
local row, col = cursor[1], cursor[2]
156+
157+
-- Check if we're in a list
158+
local list_info = parser.parse_list_line(current_line)
159+
160+
if not list_info then
161+
-- Not in a list, default backspace behavior
162+
if col > 0 then
163+
local new_line = current_line:sub(1, col - 1) .. current_line:sub(col + 1)
164+
utils.set_line(row, new_line)
165+
utils.set_cursor(row, col - 1)
166+
elseif row > 1 then
167+
-- At start of line, join with previous line
168+
local prev_line = utils.get_line(row - 1)
169+
local joined_line = prev_line .. current_line
170+
vim.api.nvim_buf_set_lines(0, row - 2, row, false, { joined_line })
171+
utils.set_cursor(row - 1, #prev_line)
172+
end
173+
return
174+
end
175+
176+
-- If at the start of list content, remove the list marker
177+
local marker_end_col = #list_info.indent + #list_info.full_marker + 1
178+
if col <= marker_end_col and col > #list_info.indent then
179+
-- Remove list marker, keep content
180+
local content = extract_list_content(current_line, list_info)
181+
utils.set_line(row, list_info.indent .. content)
182+
utils.set_cursor(row, #list_info.indent)
183+
return
184+
end
185+
186+
-- Default backspace in list content
187+
if col > 0 then
188+
local new_line = current_line:sub(1, col - 1) .. current_line:sub(col + 1)
189+
utils.set_line(row, new_line)
190+
utils.set_cursor(row, col - 1)
191+
end
192+
end
193+
194+
---Handle normal mode 'o' key
195+
function M.handle_normal_o()
196+
local current_line = utils.get_current_line()
197+
local cursor = utils.get_cursor()
198+
local row = cursor[1]
199+
200+
-- Check if current line is a list item
201+
local list_info = parser.parse_list_line(current_line)
202+
203+
if not list_info then
204+
-- Not in a list, insert blank line below and enter insert mode
205+
utils.insert_line(row + 1, "")
206+
utils.set_cursor(row + 1, 0)
207+
vim.cmd("startinsert")
208+
return
209+
end
210+
211+
-- Create next list item below
212+
local next_marker = parser.get_next_marker(list_info)
213+
local next_line = list_info.indent .. next_marker .. " "
214+
if list_info.checkbox then
215+
next_line = next_line .. "[ ] "
216+
end
217+
218+
utils.insert_line(row + 1, next_line)
219+
utils.set_cursor(row + 1, #next_line)
220+
vim.cmd("startinsert!")
221+
end
222+
223+
---Handle normal mode 'O' key
224+
function M.handle_normal_O()
225+
local current_line = utils.get_current_line()
226+
local cursor = utils.get_cursor()
227+
local row = cursor[1]
228+
229+
-- Check if current line is a list item
230+
local list_info = parser.parse_list_line(current_line)
231+
232+
if not list_info then
233+
-- Not in a list, insert blank line above and enter insert mode
234+
utils.insert_line(row, "")
235+
utils.set_cursor(row, 0)
236+
vim.cmd("startinsert")
237+
return
238+
end
239+
240+
-- Insert list item above with previous marker
241+
local prev_marker = parser.get_previous_marker(list_info, row)
242+
local prev_line = list_info.indent .. prev_marker .. " "
243+
if list_info.checkbox then
244+
prev_line = prev_line .. "[ ] "
245+
end
246+
247+
utils.insert_line(row, prev_line)
248+
utils.set_cursor(row, #prev_line)
249+
vim.cmd("startinsert!")
250+
end
251+
252+
return M

0 commit comments

Comments
 (0)