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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ See [Configuration](#configuration) for all available options.
- **Clear all formatting**: `<leader>mC` to remove all markdown formatting from selection or word
- **Smart word detection**: Works with words containing hyphens (`test-word`), dots (`file.name`), and underscores (`snake_case`)
- **Visual and normal mode**: All formatting commands work in both visual selection and normal mode (on current word)
- **Dot-repeat support**: Normal mode formatting actions are dot-repeatable - press `.` to apply the same format to another word
- Example: `<leader>mb` on "word1" to make it bold, then move to "word2" and press `.` to make it bold too
- Note: Visual mode formatting does not support dot-repeat

</details>

Expand Down
22 changes: 20 additions & 2 deletions doc/markdown-plus.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ FEATURES *markdown-plus-introduction-feature
- Smart formatting that respects word boundaries
- Clear all formatting from selected text
- Works in both normal and visual mode
- Dot-repeat support in normal mode (press `.` to reapply to next word)

- Links & References:
- Insert and edit markdown links
Expand Down Expand Up @@ -382,16 +383,33 @@ FORMATTING *markdown-plus-usage-formatting*
All formatting commands work in both normal and visual mode. Use `<leader>m`
prefix for formatting operations.

In normal mode, formatting actions are dot-repeatable - after running a
formatter once (e.g. `<leader>mb` to make a word bold), press `.` to apply
the same format to the next word without reselecting.


BOLD ~

Toggle bold with `<leader>mb`:

>markdown
This is text -> This is *bold text*
This is *bold text* -> This is text
This is text -> This is **text**
This is **text** -> This is text
<

Dot-repeat example (normal mode only):
>markdown
word1 word2 word3

1. Position cursor on "word1"
2. Press <leader>mb -> **word1** word2 word3
3. Move cursor to "word2"
4. Press . -> **word1** **word2** word3
5. Move to "word3", press . -> **word1** **word2** **word3**
<

Note: Visual mode formatting does not support dot-repeat.


ITALIC ~

Expand Down
103 changes: 97 additions & 6 deletions lua/markdown-plus/format/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,85 @@ local M = {}
---@type markdown-plus.InternalConfig
M.config = {}

---State for dot-repeat operations
M._repeat_state = {
format_type = nil,
}

---Register a mapping for dot-repeat support (for use with repeat.vim if available)
---@param plug string The plug mapping to register (e.g., "<Plug>(MarkdownPlusBold)")
---@return nil
function M.register_repeat(plug)
if not plug then
return
end

-- Check if repeat.vim is available
local has_repeat = vim.fn.exists("*repeat#set") == 1
if not has_repeat then
return
end

-- Schedule the repeat registration to happen after current operation completes
vim.schedule(function()
local termcodes = vim.api.nvim_replace_termcodes(plug, true, true, true)
vim.fn["repeat#set"](termcodes)
end)
end

---Operatorfunc callback for dot-repeat support
---@return nil
function M._format_operatorfunc()
if not M._repeat_state.format_type then
return
end

-- Apply the formatting operation on the range
M.toggle_format_word(M._repeat_state.format_type)
end

---Operatorfunc callback for clear formatting
---@return nil
function M._clear_operatorfunc()
M.clear_formatting_word()
end

---Wrapper to make formatting dot-repeatable using operatorfunc
---@param format_type string The type of formatting to apply
---@param plug string? Optional plug mapping for repeat.vim support
---@return string The operator sequence for expr mapping
function M._toggle_format_with_repeat(format_type, plug)
-- Save state for repeat
M._repeat_state.format_type = format_type

-- Set operatorfunc (buffer-local to avoid conflicts)
vim.bo.operatorfunc = "v:lua.require'markdown-plus.format'._format_operatorfunc"

-- Register with repeat.vim if available
if plug then
M.register_repeat(plug)
end

-- Return g@l for linewise operation (operatorfunc will handle word detection)
return "g@l"
end

---Wrapper to make clear formatting dot-repeatable
---@param plug string? Optional plug mapping for repeat.vim support
---@return string The operator sequence for expr mapping
function M._clear_with_repeat(plug)
-- Set operatorfunc (buffer-local to avoid conflicts)
vim.bo.operatorfunc = "v:lua.require'markdown-plus.format'._clear_operatorfunc"

-- Register with repeat.vim if available
if plug then
M.register_repeat(plug)
end

-- Return g@l for linewise operation (operatorfunc will handle word detection)
return "g@l"
end

---Formatting pattern definition
---@class markdown-plus.format.Pattern
---@field start string Start pattern (Lua pattern)
Expand Down Expand Up @@ -47,7 +126,7 @@ function M.setup_keymaps()
plug = keymap_helper.plug_name("Bold"),
fn = {
function()
M.toggle_format_word("bold")
return M._toggle_format_with_repeat("bold", string.format("<Plug>(%s)", keymap_helper.plug_name("Bold")))
end,
function()
M.toggle_format("bold")
Expand All @@ -56,12 +135,13 @@ function M.setup_keymaps()
modes = { "n", "x" },
default_key = { "<leader>mb", "<leader>mb" },
desc = "Toggle bold formatting",
expr = { true, false },
},
{
plug = keymap_helper.plug_name("Italic"),
fn = {
function()
M.toggle_format_word("italic")
return M._toggle_format_with_repeat("italic", string.format("<Plug>(%s)", keymap_helper.plug_name("Italic")))
end,
function()
M.toggle_format("italic")
Expand All @@ -70,12 +150,16 @@ function M.setup_keymaps()
modes = { "n", "x" },
default_key = { "<leader>mi", "<leader>mi" },
desc = "Toggle italic formatting",
expr = { true, false },
},
{
plug = keymap_helper.plug_name("Strikethrough"),
fn = {
function()
M.toggle_format_word("strikethrough")
return M._toggle_format_with_repeat(
"strikethrough",
string.format("<Plug>(%s)", keymap_helper.plug_name("Strikethrough"))
)
end,
function()
M.toggle_format("strikethrough")
Expand All @@ -84,12 +168,13 @@ function M.setup_keymaps()
modes = { "n", "x" },
default_key = { "<leader>ms", "<leader>ms" },
desc = "Toggle strikethrough formatting",
expr = { true, false },
},
{
plug = keymap_helper.plug_name("Code"),
fn = {
function()
M.toggle_format_word("code")
return M._toggle_format_with_repeat("code", string.format("<Plug>(%s)", keymap_helper.plug_name("Code")))
end,
function()
M.toggle_format("code")
Expand All @@ -98,6 +183,7 @@ function M.setup_keymaps()
modes = { "n", "x" },
default_key = { "<leader>mc", "<leader>mc" },
desc = "Toggle inline code formatting",
expr = { true, false },
},
{
plug = keymap_helper.plug_name("CodeBlock"),
Expand All @@ -109,12 +195,17 @@ function M.setup_keymaps()
{
plug = keymap_helper.plug_name("ClearFormatting"),
fn = {
M.clear_formatting_word,
M.clear_formatting,
function()
return M._clear_with_repeat(string.format("<Plug>(%s)", keymap_helper.plug_name("ClearFormatting")))
end,
function()
M.clear_formatting()
end,
},
modes = { "n", "x" },
default_key = { "<leader>mC", "<leader>mC" },
desc = "Clear all formatting",
expr = { true, false },
},
})
end
Expand Down
9 changes: 9 additions & 0 deletions lua/markdown-plus/keymap_helper.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local M = {}
---@field modes string|string[] Mode(s) for the keymap ('n', 'v', 'x', 'i')
---@field default_key? string|string[] Default key binding (optional). If both `modes` and `default_key` are arrays, they are indexed correspondingly (i.e., `modes[1]` gets `default_key[1]`, etc.).
---@field desc string Description for the keymap
---@field expr? boolean|boolean[] Whether the mapping is an expression mapping (optional). Can be a single boolean or array per mode.

---Setup keymaps for a module
---@param config markdown-plus.InternalConfig Plugin configuration
Expand All @@ -21,6 +22,10 @@ function M.setup_keymaps(config, keymaps)
if default_keys and type(default_keys) ~= "table" then
default_keys = { default_keys }
end
local exprs = keymap.expr
if exprs and type(exprs) ~= "table" then
exprs = { exprs }
end

for idx, mode in ipairs(modes) do
-- Always create <Plug> mapping (regardless of keymaps.enabled)
Expand All @@ -32,9 +37,13 @@ function M.setup_keymaps(config, keymaps)
fn = fn[idx]
end

-- Determine if this mode uses expr mapping
local is_expr = exprs and exprs[idx] or false

vim.keymap.set(mode, plug_name, fn, {
silent = true,
desc = keymap.desc,
expr = is_expr,
})

-- Set default keymap only if keymaps are enabled and default is specified
Expand Down
Loading