From 5b860e79aabdf28fe2cba1f9b14c6b0a410a3e6b Mon Sep 17 00:00:00 2001 From: Dorian Karter Date: Wed, 3 Jun 2026 11:30:21 -0500 Subject: [PATCH 1/2] feat: support wrapped and document bullets --- lua/bullets/actions.lua | 82 +++++++++++++++++++++++++++++++++- test/asciidoc_spec.lua | 11 +++-- test/bullets_spec.lua | 6 +-- test/helpers.lua | 1 + test/wrapping_bullets_spec.lua | 26 ++++++++++- 5 files changed, 116 insertions(+), 10 deletions(-) diff --git a/lua/bullets/actions.lua b/lua/bullets/actions.lua index 3ce3524..ba2ae63 100644 --- a/lua/bullets/actions.lua +++ b/lua/bullets/actions.lua @@ -6,11 +6,52 @@ local function feed(keys) vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), "n", false) end +local function style_enabled(marker) + for _, style in ipairs(config.options.list_item_styles) do + if style == marker then + return true + end + + if style:sub(-1) == "+" and marker:match("^" .. vim.pesc(style:sub(1, -2)) .. "+$") then + return true + end + end + + return false +end + +local function parse_static(line) + local indent, marker, spacing, text = line:match("^(%s*)(\\item)(%s+)(.*)$") + if not marker then + indent, marker, spacing, text = line:match("^(%s*)(#%.)(%s+)(.*)$") + end + if not marker then + indent, marker, spacing, text = line:match("^(%s*)([%*.]+)(%s+)(.*)$") + if marker and not (marker:match("^%*+$") or marker:match("^%.+$")) then + marker = nil + end + end + if not marker or not style_enabled(marker) then + return nil + end + + return { + type = "static", + indent = indent, + marker = marker, + spacing = spacing, + text = text, + } +end + local function parse_standard(line) - local indent, marker, spacing, text = line:match("^(%s*)([-*+])(%s+)(.*)$") + local indent, marker, spacing, text = line:match("^(%s*)([-+])(%s+)(.*)$") if not marker then return nil end + if not style_enabled(marker) then + return nil + end return { indent = indent, @@ -78,6 +119,11 @@ local function parse_roman(line) end local function parse_line(line) + local static = parse_static(line) + if static then + return { static } + end + local standard = parse_standard(line) if standard then standard.type = "std" @@ -177,6 +223,14 @@ local function next_marker(bullet) return bullet.marker end +local function prefix_width(bullet) + if bullet.type == "num" or bullet.type == "abc" or bullet.type == "rom" then + return #bullet.indent + #bullet.marker + #bullet.closure + #bullet.spacing + end + + return #bullet.indent + #bullet.marker + #bullet.spacing +end + local function next_prefix(bullet) local marker = next_marker(bullet) if not marker then @@ -186,16 +240,40 @@ local function next_prefix(bullet) if bullet.type == "std" then return bullet.indent .. marker .. bullet.spacing end + if bullet.type == "static" then + return bullet.indent .. marker .. bullet.spacing + end local prefix = marker .. bullet.closure .. " " return bullet.indent .. pad_right(prefix, #bullet.marker + #bullet.closure + #bullet.spacing) end +local function wrapped_owner(lnum, line) + if not config.options.enable_wrapped_lines or line:match("^%s*$") then + return nil + end + + local current_indent = #(line:match("^%s*") or "") + for row = lnum - 1, 1, -1 do + local previous_line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1] + if previous_line == "" then + return nil + end + + local previous_bullet = resolve_bullet(parse_line(previous_line), row) + if previous_bullet and current_indent >= prefix_width(previous_bullet) then + return previous_bullet + end + end + + return nil +end + function M.insert_new_bullet() local mode = vim.fn.mode() local lnum = vim.api.nvim_win_get_cursor(0)[1] local line = vim.api.nvim_get_current_line() - local bullet = resolve_bullet(parse_line(line), lnum) + local bullet = resolve_bullet(parse_line(line), lnum) or wrapped_owner(lnum, line) if mode ~= "n" and not at_eol(line) then feed("") diff --git a/test/asciidoc_spec.lua b/test/asciidoc_spec.lua index e155697..f442157 100644 --- a/test/asciidoc_spec.lua +++ b/test/asciidoc_spec.lua @@ -1,8 +1,13 @@ local helpers = require("test.helpers") +local active_it = it local it = pending describe("AsciiDoc", function() - it("maintains indentation in ascii doc bullets", function() + before_each(function() + helpers.reset_config() + end) + + active_it("maintains indentation in ascii doc bullets", function() helpers.test_bullet_inserted( "rats", { "= Pets!", "* dogs", "** cats" }, @@ -10,11 +15,11 @@ describe("AsciiDoc", function() ) end) - it("supports dot bullets", function() + active_it("supports dot bullets", function() helpers.test_bullet_inserted("cats", { "= Pets!", ". dogs" }, { "= Pets!", ". dogs", ". cats" }) end) - it("supports nested dot bullets", function() + active_it("supports nested dot bullets", function() helpers.test_bullet_inserted( "rats", { "= Pets!", ". dogs", ".. cats" }, diff --git a/test/bullets_spec.lua b/test/bullets_spec.lua index 3d93521..ba1be35 100644 --- a/test/bullets_spec.lua +++ b/test/bullets_spec.lua @@ -45,7 +45,7 @@ describe("Bullets.vim", function() ) end) - it("adds a new latex bullet", function() + active_it("adds a new latex bullet", function() helpers.test_bullet_inserted("Second item", { "\\documentclass{article}", " \\begin{document}", @@ -62,7 +62,7 @@ describe("Bullets.vim", function() }) end) - it("adds a pandoc bullet if the prev line had one", function() + active_it("adds a pandoc bullet if the prev line had one", function() helpers.test_bullet_inserted( "second bullet", { "Hello there", "#. this is the first bullet" }, @@ -70,7 +70,7 @@ describe("Bullets.vim", function() ) end) - it("adds an Org mode bullet if the prev line had one", function() + active_it("adds an Org mode bullet if the prev line had one", function() helpers.test_bullet_inserted( "second bullet", { "Hello there", "**** this is the first bullet" }, diff --git a/test/helpers.lua b/test/helpers.lua index cf5d61d..6b5ee8b 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -47,6 +47,7 @@ function M.reset_config() checkbox_partials_toggle = 1, outline_levels = { "ROM", "ABC", "num", "abc", "rom", "std-", "std*", "std+" }, enable_roman_list = true, + enable_wrapped_lines = true, pad_right = true, delete_last_bullet_if_empty = 1, }) diff --git a/test/wrapping_bullets_spec.lua b/test/wrapping_bullets_spec.lua index 8330cb2..2e4b65b 100644 --- a/test/wrapping_bullets_spec.lua +++ b/test/wrapping_bullets_spec.lua @@ -1,8 +1,13 @@ local helpers = require("test.helpers") +local active_it = it local it = pending describe("wrapped bullets", function() - it("inserts a new bullet after a wrapped bullet", function() + before_each(function() + helpers.reset_config() + end) + + active_it("inserts a new bullet after a wrapped bullet", function() helpers.test_bullet_inserted("do that", { "# Hello there", "- do this", @@ -15,7 +20,24 @@ describe("wrapped bullets", function() }) end) - it("does not insert wrapped bullets unnecessarily", function() + active_it("does not insert wrapped bullets when disabled", function() + require("bullets").setup({ enable_wrapped_lines = false }) + helpers.new_buffer({ + "# Hello there", + "- do this", + " this is the second line of the first bullet", + }) + helpers.feedkeys("A") + helpers.feedkeys("ido that") + assert.are.same({ + "# Hello there", + "- do this", + " this is the second line of the first bullet", + "do that", + }, helpers.get_lines()) + end) + + active_it("does not insert wrapped bullets unnecessarily", function() -- When is pressed on a non-bullet line the plugin defers the actual -- newline via feedkeys('n'). Using two separate feedkeys calls ensures the -- deferred CR fires (the 'x' flag drains it) before we type the next text. From 395cf0181ccfa26ed35f00cbe970b1d928d6b4fe Mon Sep 17 00:00:00 2001 From: Dorian Karter Date: Wed, 3 Jun 2026 11:37:10 -0500 Subject: [PATCH 2/2] fix: stop wrapped bullets at whitespace separators --- lua/bullets/actions.lua | 6 +++--- test/wrapping_bullets_spec.lua | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lua/bullets/actions.lua b/lua/bullets/actions.lua index ba2ae63..99f1366 100644 --- a/lua/bullets/actions.lua +++ b/lua/bullets/actions.lua @@ -27,7 +27,7 @@ local function parse_static(line) end if not marker then indent, marker, spacing, text = line:match("^(%s*)([%*.]+)(%s+)(.*)$") - if marker and not (marker:match("^%*+$") or marker:match("^%.+$")) then + if marker and (marker == "*" or not (marker:match("^%*+$") or marker:match("^%.+$"))) then marker = nil end end @@ -45,7 +45,7 @@ local function parse_static(line) end local function parse_standard(line) - local indent, marker, spacing, text = line:match("^(%s*)([-+])(%s+)(.*)$") + local indent, marker, spacing, text = line:match("^(%s*)([-*+])(%s+)(.*)$") if not marker then return nil end @@ -256,7 +256,7 @@ local function wrapped_owner(lnum, line) local current_indent = #(line:match("^%s*") or "") for row = lnum - 1, 1, -1 do local previous_line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1] - if previous_line == "" then + if previous_line:match("^%s*$") then return nil end diff --git a/test/wrapping_bullets_spec.lua b/test/wrapping_bullets_spec.lua index 2e4b65b..5d9a79e 100644 --- a/test/wrapping_bullets_spec.lua +++ b/test/wrapping_bullets_spec.lua @@ -65,4 +65,24 @@ describe("wrapped bullets", function() "do that", }, helpers.get_lines()) end) + + active_it("does not insert wrapped bullets after whitespace-only separators", function() + helpers.new_buffer({ + "# Hello there", + "- do this", + " this is the second line of the first bullet", + " ", + " no bullets after this line", + }) + helpers.feedkeys("A") + helpers.feedkeys("ido that") + assert.are.same({ + "# Hello there", + "- do this", + " this is the second line of the first bullet", + " ", + " no bullets after this line", + "do that", + }, helpers.get_lines()) + end) end)