--- Visual-mode markdown formatting for the `e` group in markdown/mdx --- buffers: wrap the selection in inline markers (italic, bold, code, link) --- or rewrite its lines as a list / fenced code block. --- --- Inline ops work on the exact charwise selection, or on the whole lines of --- a linewise one. Line ops always act on whole lines, whichever way the --- selection was made. Blockwise selections are refused. --- --- Mapped buffer-locally from plugins/markdown-edit.lua. --- @module config.markdown_edit local M = {} local ESC = vim.keycode "" --- Leave visual mode and return the selection as a 0-based, end-exclusive --- `nvim_buf_set_text` rectangle. --- @return integer? srow, integer scol, integer erow, integer ecol local function selection() -- `'<`/`'>` are only set once visual mode ends. `:normal!` rather than -- feedkeys(..., "x"): the latter also flushes any typed-ahead keys, which -- would run before the insert mode some of the binds end in. vim.cmd.normal { ESC, bang = true } local mode = vim.fn.visualmode() local s, e = vim.fn.getpos "'<", vim.fn.getpos "'>" local srow, erow = s[2] - 1, e[2] - 1 if mode == "V" then local last = vim.api.nvim_buf_get_lines(0, erow, erow + 1, true)[1] return srow, 0, erow, #last elseif mode == "v" then -- getregionpos clamps a `$` end column (v:maxcol) to the line length and -- moves the inclusive end onto the last byte of a multibyte character local pos = vim.fn.getregionpos(s, e, { type = mode }) return srow, pos[1][1][3] - 1, erow, pos[#pos][2][3] end vim.notify("Blockwise selections are not supported", vim.log.levels.WARN) end --- Insert `before` at the start and `after` at the end of the selection. --- @param before string --- @param after string --- @return integer? row, integer? col 0-based position of `after` local function wrap(before, after) local srow, scol, erow, ecol = selection() if not srow then return end -- suffix first so the start position stays valid vim.api.nvim_buf_set_text(0, erow, ecol, erow, ecol, { after }) vim.api.nvim_buf_set_text(0, srow, scol, srow, scol, { before }) if srow == erow then ecol = ecol + #before end return erow, ecol end --- `[text](|)` with insert mode started at the cursor local function link() local row, col = wrap("[", "]()") if not row then return end vim.api.nvim_win_set_cursor(0, { row + 1, col + 2 }) vim.cmd "startinsert" end --- Prefix every non-blank selected line, after its indentation, with --- `marker(n)` for the n-th such line. Blank lines separate items and are --- left alone. --- @param marker fun(n: integer): string local function list(marker) local srow, _, erow = selection() if not srow then return end local lines = vim.api.nvim_buf_get_lines(0, srow, erow + 1, true) local n = 0 for i, line in ipairs(lines) do if line:find "%S" then n = n + 1 local indent = line:match "^%s*" lines[i] = indent .. marker(n) .. line:sub(#indent + 1) end end vim.api.nvim_buf_set_lines(0, srow, erow + 1, true, lines) end --- Fence the selected lines, indented like the first one, with insert mode --- started after the opening ``` for the language local function code_block() local srow, _, erow = selection() if not srow then return end local fence = vim.api.nvim_buf_get_lines(0, srow, srow + 1, true)[1]:match "^%s*" .. "```" vim.api.nvim_buf_set_lines(0, erow + 1, erow + 1, true, { fence }) vim.api.nvim_buf_set_lines(0, srow, srow, true, { fence }) vim.api.nvim_win_set_cursor(0, { srow + 1, 0 }) vim.cmd "startinsert!" end --- @type table> M.mappings = { x = { ["e"] = { desc = "Markdown" }, ["ei"] = { function() wrap("*", "*") end, desc = "Italic" }, ["eb"] = { function() wrap("**", "**") end, desc = "Bold" }, ["ec"] = { function() wrap("`", "`") end, desc = "Inline code" }, ["ex"] = { link, desc = "Link" }, ["el"] = { function() list(function() return "- " end) end, desc = "Unordered list", }, ["eL"] = { function() list(function(n) return n .. ". " end) end, desc = "Ordered list", }, ["eC"] = { code_block, desc = "Code block" }, }, } return M