diff --git a/lua/config/ignore_patterns.lua b/lua/config/ignore_patterns.lua index d427cda..e5259dc 100644 --- a/lua/config/ignore_patterns.lua +++ b/lua/config/ignore_patterns.lua @@ -86,6 +86,7 @@ M.patterns = { "Icon?", "iCloud~", "com~", + ".githooks", } --- Patterns that should never be shown in any file browser diff --git a/lua/config/markdown_edit.lua b/lua/config/markdown_edit.lua new file mode 100644 index 0000000..bcdddef --- /dev/null +++ b/lua/config/markdown_edit.lua @@ -0,0 +1,116 @@ +--- 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