Skip to content
Draft
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
138 changes: 133 additions & 5 deletions lua/bullets/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,10 @@ local function at_eol(line)
return vim.fn.col(".") == #line + 1
end

local function insert_line(lnum, line)
vim.api.nvim_buf_set_lines(0, lnum, lnum, false, { line })
vim.api.nvim_win_set_cursor(0, { lnum + 1, #line })
local function insert_lines(lnum, lines, cursor_index)
vim.api.nvim_buf_set_lines(0, lnum, lnum, false, lines)
local line = lines[cursor_index]
vim.api.nvim_win_set_cursor(0, { lnum + cursor_index, #line })
end

local function pad_right(prefix, width)
Expand Down Expand Up @@ -248,6 +249,120 @@ local function next_prefix(bullet)
return bullet.indent .. pad_right(prefix, #bullet.marker + #bullet.closure + #bullet.spacing)
end

local function current_prefix(bullet)
if bullet.type == "std" or bullet.type == "static" then
return bullet.indent .. bullet.marker .. bullet.spacing
end

local prefix = bullet.marker .. bullet.closure .. " "
return bullet.indent .. pad_right(prefix, #bullet.marker + #bullet.closure + #bullet.spacing)
end

local function indent_unit()
if not vim.o.expandtab then
return "\t"
end

return string.rep(" ", vim.o.shiftwidth > 0 and vim.o.shiftwidth or vim.o.tabstop)
end

local function style_for_bullet(bullet)
if bullet.type == "std" then
return "std" .. bullet.marker
end
if bullet.type == "static" then
return bullet.marker
end
if bullet.type == "num" then
return "num"
end
if bullet.type == "abc" then
return bullet.marker == bullet.marker:lower() and "abc" or "ABC"
end
if bullet.type == "rom" then
return bullet.marker == bullet.marker:lower() and "rom" or "ROM"
end

return nil
end

local function bullet_for_style(style, indent)
if style == "num" then
return { type = "num", indent = indent, marker = "1", closure = ".", spacing = " ", text = "" }
end
if style == "abc" then
return { type = "abc", indent = indent, marker = "a", closure = ".", spacing = " ", text = "" }
end
if style == "ABC" then
return { type = "abc", indent = indent, marker = "A", closure = ".", spacing = " ", text = "" }
end
if style == "rom" then
return { type = "rom", indent = indent, marker = "i", closure = ".", spacing = " ", text = "" }
end
if style == "ROM" then
return { type = "rom", indent = indent, marker = "I", closure = ".", spacing = " ", text = "" }
end

local marker = style:match("^std(.+)$")
if marker then
return { type = "std", indent = indent, marker = marker, spacing = " ", text = "" }
end

return nil
end

local function child_bullet(bullet)
local current_style = style_for_bullet(bullet)
if not current_style then
return nil
end

for index, style in ipairs(config.options.outline_levels) do
if style == current_style then
local next_style = config.options.outline_levels[index + 1]
return next_style and bullet_for_style(next_style, bullet.indent .. indent_unit()) or nil
end
end

return nil
end

local function ends_with_colon(text)
return text:sub(-1) == ":" or text:sub(-3) == ":"
end

local function spaced_lines(prefix)
local lines = {}
for _ = 2, config.options.line_spacing do
table.insert(lines, "")
end
table.insert(lines, prefix)
return lines, #lines
end

local function delete_empty_bullet(lnum, bullet)
local behavior = config.options.delete_last_bullet_if_empty
if behavior == 0 then
feed("<CR>")
return true
end

if behavior == 2 and bullet.indent ~= "" then
local promoted = vim.deepcopy(bullet)
promoted.indent = promoted.indent
:gsub("\t$", "")
:gsub(string.rep(" ", vim.o.shiftwidth > 0 and vim.o.shiftwidth or vim.o.tabstop) .. "$", "")
local prefix = next_prefix(promoted)
vim.api.nvim_set_current_line(prefix or "")
vim.api.nvim_win_set_cursor(0, { lnum, #(prefix or "") })
return true
end

vim.api.nvim_set_current_line(bullet.indent)
vim.api.nvim_win_set_cursor(0, { lnum, #bullet.indent })
return true
end

local function wrapped_owner(lnum, line)
if not config.options.enable_wrapped_lines or line:match("^%s*$") then
return nil
Expand Down Expand Up @@ -290,13 +405,26 @@ function M.insert_new_bullet()
return ""
end

local prefix = next_prefix(bullet)
if bullet.text == "" and delete_empty_bullet(lnum, bullet) then
return ""
end

local prefix
if config.options.auto_indent_after_colon and ends_with_colon(bullet.text) then
local child = child_bullet(bullet)
if child then
prefix = current_prefix(child)
end
end

prefix = prefix or next_prefix(bullet)
if not prefix then
feed("<CR>")
return ""
end

insert_line(lnum, prefix)
local lines, cursor_index = spaced_lines(prefix)
insert_lines(lnum, lines, cursor_index)

if mode == "n" then
vim.cmd.startinsert({ bang = true })
Expand Down
86 changes: 81 additions & 5 deletions test/bullets_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ describe("Bullets.vim", function()
}, helpers.get_lines())
end)

it("deletes the last bullet if it is empty", function()
active_it("deletes the last bullet if it is empty", function()
require("bullets").setup({ delete_last_bullet_if_empty = 1 })
helpers.new_buffer({
"# Hello there",
"- this is the first bullet",
Expand All @@ -235,8 +236,8 @@ describe("Bullets.vim", function()
}, lines)
end)

it("promote the last bullet when configured to", function()
vim.g.bullets_delete_last_bullet_if_empty = 2
active_it("promotes the last bullet when configured to", function()
require("bullets").setup({ delete_last_bullet_if_empty = 2 })
helpers.new_buffer({
"# Hello there",
"- this is the first bullet",
Expand All @@ -258,8 +259,8 @@ describe("Bullets.vim", function()
}, lines)
end)

it("does not delete the last bullet when configured not to", function()
vim.g.bullets_delete_last_bullet_if_empty = 0
active_it("does not delete the last bullet when configured not to", function()
require("bullets").setup({ delete_last_bullet_if_empty = 0 })
helpers.new_buffer({
"# Hello there",
"- this is the first bullet",
Expand All @@ -280,6 +281,81 @@ describe("Bullets.vim", function()
}, lines)
end)

active_it("adds configured blank spacing before the next bullet", function()
require("bullets").setup({ line_spacing = 2 })
helpers.test_bullet_inserted("second bullet", {
"# Hello there",
"1. first bullet",
}, {
"# Hello there",
"1. first bullet",
"",
"2. second bullet",
})
end)

active_it("can disable right padding for ordered bullets", function()
require("bullets").setup({ pad_right = false })
helpers.test_bullet_inserted("second bullet", {
"# Hello there",
"9. first bullet",
}, {
"# Hello there",
"9. first bullet",
"10. second bullet",
})
end)

active_it("indents after a line ending in a colon", function()
require("bullets").setup({ auto_indent_after_colon = true })
helpers.new_buffer({
"# Hello there",
"a. first bullet",
})
helpers.feedkeys("A<CR>second bullet:<CR>third bullet<CR>fourth bullet<Esc>")
assert.are.same({
"# Hello there",
"a. first bullet",
"b. second bullet:",
" i. third bullet",
" ii. fourth bullet",
}, helpers.get_lines())
end)

active_it("indents after a line ending in a fullwidth colon", function()
require("bullets").setup({ auto_indent_after_colon = true })
helpers.new_buffer({
"# Hello there",
"a. first bullet",
})
helpers.feedkeys("A<CR>second bullet:<CR>third bullet<Esc>")
assert.are.same({
"# Hello there",
"a. first bullet",
"b. second bullet:",
" i. third bullet",
}, helpers.get_lines())
end)

active_it("can disable colon auto indentation", function()
require("bullets").setup({ auto_indent_after_colon = false })
helpers.test_bullet_inserted("second bullet:", {
"# Hello there",
"a. first bullet",
}, {
"# Hello there",
"a. first bullet",
"b. second bullet:",
})
helpers.feedkeys("A<CR>third bullet<Esc>")
assert.are.same({
"# Hello there",
"a. first bullet",
"b. second bullet:",
"c. third bullet",
}, helpers.get_lines())
end)

active_it("toggles roman numeral bullets with enable_roman_list", function()
-- Disable alpha lists to isolate test to roman numerals
require("bullets").setup({ max_alpha_characters = 0, enable_roman_list = true })
Expand Down
Loading