Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lua/markdown-plus/footnotes/insertion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ function M.insert_footnote()
-- Insert reference after the character the cursor is on
local line = vim.api.nvim_buf_get_lines(bufnr, row - 1, row, false)[1]
local reference = "[^" .. id .. "]"
local new_line = line:sub(1, col + 1) .. reference .. line:sub(col + 2)

-- Use UTF-8 safe split to handle multibyte characters correctly
local before, after = utils.split_after_cursor(line, col)
local new_line = before .. reference .. after
vim.api.nvim_buf_set_lines(bufnr, row - 1, row, false, { new_line })

-- If definition already exists, just insert the reference and notify
Expand Down
62 changes: 43 additions & 19 deletions lua/markdown-plus/format/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ end
function M.get_word_boundaries()
local cursor = utils.get_cursor()
local row = cursor[1]
local col = cursor[2]
local col = cursor[2] -- 0-indexed byte offset
local line = utils.get_current_line()

-- Define what characters are considered word boundaries (stop points)
Expand All @@ -305,38 +305,62 @@ function M.get_word_boundaries()
return false
end

-- Find word start
local word_start = col
while word_start > 0 do
local char = line:sub(word_start, word_start)
-- Convert byte offset to character index for iteration
local char_idx = vim.fn.charidx(line, col)
if char_idx < 0 then
char_idx = 0
end

-- Get total character count
local total_chars = vim.fn.strcharlen(line)

-- Find word start (iterate backwards by character)
local word_start_char = char_idx
while word_start_char > 0 do
local char = vim.fn.strcharpart(line, word_start_char, 1)
if is_word_boundary(char) then
word_start = word_start + 1
word_start_char = word_start_char + 1
break
end
word_start = word_start - 1
word_start_char = word_start_char - 1
end
if word_start == 0 then
word_start = 1
if word_start_char < 0 then
word_start_char = 0
end

-- Find word end
local word_end = col + 1
while word_end <= #line do
local char = line:sub(word_end, word_end)
-- Find word end (iterate forwards by character)
local word_end_char = char_idx + 1
while word_end_char < total_chars do
local char = vim.fn.strcharpart(line, word_end_char, 1)
if is_word_boundary(char) then
word_end = word_end - 1
word_end_char = word_end_char - 1
break
end
word_end = word_end + 1
word_end_char = word_end_char + 1
end
if word_end_char >= total_chars then
word_end_char = total_chars - 1
end
if word_end > #line then
word_end = #line
if word_end_char < 0 then
word_end_char = 0
end

-- Convert character indices back to byte positions (1-indexed for get_text_in_range)
local start_byte = vim.fn.byteidx(line, word_start_char)
if start_byte == -1 then
start_byte = 0
end
local end_byte = vim.fn.byteidx(line, word_end_char + 1)
if end_byte == -1 then
end_byte = #line
else
end_byte = end_byte - 1
end

return {
row = row,
start_col = word_start,
end_col = word_end,
start_col = start_byte + 1, -- Convert to 1-indexed
end_col = end_byte + 1, -- Convert to 1-indexed
}
end

Expand Down
6 changes: 4 additions & 2 deletions lua/markdown-plus/images/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,13 @@ function M.insert_image()
local line = utils.get_current_line()
local col = cursor[2]

local new_line = line:sub(1, col) .. image .. line:sub(col + 1)
-- Use UTF-8 safe split to handle multibyte characters correctly
local before, after = utils.split_after_cursor(line, col)
local new_line = before .. image .. after
utils.set_line(cursor[1], new_line)

-- Move cursor after the image
utils.set_cursor(cursor[1], col + #image)
utils.set_cursor(cursor[1], #before + #image)

utils.notify("Image inserted")
end
Expand Down
6 changes: 4 additions & 2 deletions lua/markdown-plus/links/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,13 @@ function M.insert_link()
local line = utils.get_current_line()
local col = cursor[2]

local new_line = line:sub(1, col) .. link .. line:sub(col + 1)
-- Use UTF-8 safe split to handle multibyte characters correctly
local before, after = utils.split_after_cursor(line, col)
local new_line = before .. link .. after
utils.set_line(cursor[1], new_line)

-- Move cursor after the link
utils.set_cursor(cursor[1], col + #link)
utils.set_cursor(cursor[1], #before + #link)

utils.notify("Link inserted")
end
Expand Down
22 changes: 12 additions & 10 deletions lua/markdown-plus/list/handlers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ function M.handle_enter()

if not list_info then
-- Not in a list at all, simulate default Enter behavior
local line_before = current_line:sub(1, col)
local line_after = current_line:sub(col + 1)
-- Use UTF-8 safe split to handle multibyte characters correctly
local line_before, line_after = utils.split_at_cursor(current_line, col)

utils.set_line(row, line_before)
utils.insert_line(row + 1, line_after)
Expand Down Expand Up @@ -102,8 +102,8 @@ function M.handle_enter()

if should_split then
-- Split content at cursor position
local content_before = current_line:sub(1, col)
local content_after = current_line:sub(col + 1)
-- Use UTF-8 safe split to handle multibyte characters correctly
local content_before, content_after = utils.split_at_cursor(current_line, col)

-- Update current line with content before cursor
utils.set_line(row, content_before)
Expand Down Expand Up @@ -142,8 +142,8 @@ function M.continue_list_content()

if not list_info then
-- Not in a list, simulate default Enter behavior
local line_before = current_line:sub(1, col)
local line_after = current_line:sub(col + 1)
-- Use UTF-8 safe split to handle multibyte characters correctly
local line_before, line_after = utils.split_at_cursor(current_line, col)

utils.set_line(row, line_before)
utils.insert_line(row + 1, line_after)
Expand All @@ -155,8 +155,8 @@ function M.continue_list_content()
local marker_end = shared.get_content_start_col(list_info)

-- Split line at cursor
local line_before = current_line:sub(1, col)
local line_after = current_line:sub(col + 1)
-- Use UTF-8 safe split to handle multibyte characters correctly
local line_before, line_after = utils.split_at_cursor(current_line, col)

-- Update current line
utils.set_line(row, line_before)
Expand All @@ -179,9 +179,11 @@ function M.handle_tab()
local cursor = utils.get_cursor()
local row, col = cursor[1], cursor[2]
local indent = string.rep(" ", vim.bo.shiftwidth or 2)
local new_line = current_line:sub(1, col) .. indent .. current_line:sub(col + 1)
-- Use UTF-8 safe split to handle multibyte characters correctly
local before, after = utils.split_after_cursor(current_line, col)
local new_line = before .. indent .. after
utils.set_line(row, new_line)
utils.set_cursor(row, col + #indent)
utils.set_cursor(row, #before + #indent)
return
end

Expand Down
68 changes: 68 additions & 0 deletions lua/markdown-plus/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,74 @@ function M.debug_print(...)
end
end

---Split a line at a byte column position, ensuring proper UTF-8 character boundaries.
---Splits BEFORE the character at the cursor position (character at cursor goes to 'after').
---Use this for line splitting operations (e.g., Enter key behavior).
---@param line string The line content
---@param byte_col number 0-indexed byte column (from nvim_win_get_cursor)
---@return string before Text before the cursor position
---@return string after Text from cursor position onwards (including character at cursor)
function M.split_at_cursor(line, byte_col)
if #line == 0 then
return "", ""
end

-- Handle cursor past end of line
if byte_col >= #line then
return line, ""
end

-- Convert 0-indexed byte offset to character index
local char_idx = vim.fn.charidx(line, byte_col)
if char_idx < 0 then
-- Should not happen if byte_col < #line, but be safe
return line, ""
end

-- Get byte position for start of current character
local curr_char_byte = vim.fn.byteidx(line, char_idx)
if curr_char_byte == -1 or curr_char_byte >= #line then
-- Past end of line
return line, ""
end

return line:sub(1, curr_char_byte), line:sub(curr_char_byte + 1)
end

---Split a line after the character at cursor position, ensuring proper UTF-8 character boundaries.
---Splits AFTER the character at the cursor position (character at cursor goes to 'before').
---Use this for insertion operations (e.g., inserting footnotes, links, images after current char).
---@param line string The line content
---@param byte_col number 0-indexed byte column (from nvim_win_get_cursor)
---@return string before Text up to and including the character at cursor
---@return string after Text after the character at cursor
function M.split_after_cursor(line, byte_col)
if #line == 0 then
return "", ""
end

-- Handle cursor past end of line
if byte_col >= #line then
return line, ""
end

-- Convert 0-indexed byte offset to character index
local char_idx = vim.fn.charidx(line, byte_col)
if char_idx < 0 then
-- Should not happen if byte_col < #line, but be safe
return line, ""
end

-- Get byte position for start of next character
local next_char_byte = vim.fn.byteidx(line, char_idx + 1)
if next_char_byte == -1 then
-- char_idx is at or past last character, split at end
return line, ""
end

return line:sub(1, next_char_byte), line:sub(next_char_byte + 1)
end

---Get the byte index of the last byte of a multi-byte character
---When vim.fn.getpos() returns a column position for a multi-byte character,
---it returns the byte index of the FIRST byte of that character.
Expand Down
Loading
Loading