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
174 changes: 172 additions & 2 deletions lua/bullets/actions.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
local M = {}
local config = require("bullets.config")
local ordinal = require("bullets.ordinal")

local function feed(keys)
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), "n", false)
Expand All @@ -18,6 +20,127 @@ local function parse_standard(line)
}
end

local function parse_numeric(line)
local indent, marker, closure, spacing, text = line:match("^(%s*)(%d+)([.)])(%s+)(.*)$")
if not marker then
return nil
end

return {
type = "num",
indent = indent,
marker = marker,
closure = closure,
spacing = spacing,
text = text,
}
end

local function parse_alpha(line)
local max = config.options.max_alpha_characters
if max == 0 then
return nil
end

local indent, marker, closure, spacing, text = line:match("^(%s*)(%a+)([.)])(%s+)(.*)$")
if not marker or #marker > max or not (marker == marker:lower() or marker == marker:upper()) then
return nil
end

return {
type = "abc",
indent = indent,
marker = marker,
closure = closure,
spacing = spacing,
text = text,
}
end

local function parse_roman(line)
if not config.options.enable_roman_list then
return nil
end

local indent, marker, closure, spacing, text = line:match("^(%s*)(%a+)([.)])(%s+)(.*)$")
if not marker or not (marker == marker:lower() or marker == marker:upper()) or not ordinal.is_roman(marker) then
return nil
end

return {
type = "rom",
indent = indent,
marker = marker,
closure = closure,
spacing = spacing,
text = text,
}
end

local function parse_line(line)
local standard = parse_standard(line)
if standard then
standard.type = "std"
return { standard }
end

local numeric = parse_numeric(line)
if numeric then
return { numeric }
end

local alpha = parse_alpha(line)
local roman = parse_roman(line)

return vim.tbl_filter(function(item)
return item ~= nil
end, { alpha, roman })
end

local resolve_bullet

local function previous_ordered_type(lnum, indent)
for row = lnum - 1, 1, -1 do
local line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1]
if line == "" then
return nil
end

local bullet = resolve_bullet(parse_line(line), row)
if bullet and bullet.indent == indent and (bullet.type == "abc" or bullet.type == "rom") then
return bullet.type
end
end

return nil
end

function resolve_bullet(bullets, lnum)
if #bullets == 0 then
return nil
end
if #bullets == 1 then
return bullets[1]
end

local previous_type = previous_ordered_type(lnum, bullets[1].indent)
if previous_type then
for _, bullet in ipairs(bullets) do
if bullet.type == previous_type then
return bullet
end
end
end

for _, bullet in ipairs(bullets) do
if bullet.type == "rom" then
return bullet
end
end

return bullets[1]
end

local function at_eol(line)
return vim.fn.col(".") == #line + 1
end
Expand All @@ -27,11 +150,52 @@ local function insert_line(lnum, line)
vim.api.nvim_win_set_cursor(0, { lnum + 1, #line })
end

local function pad_right(prefix, width)
if not config.options.pad_right or #prefix >= width then
return prefix
end

return prefix .. string.rep(" ", width - #prefix)
end

local function next_marker(bullet)
if bullet.type == "num" then
return tostring(tonumber(bullet.marker) + 1)
end
if bullet.type == "abc" then
local marker =
ordinal.number_to_abc(ordinal.abc_to_number(bullet.marker) + 1, bullet.marker == bullet.marker:lower())
if #marker > config.options.max_alpha_characters then
return nil
end
return marker
end
if bullet.type == "rom" then
return ordinal.number_to_roman(ordinal.roman_to_number(bullet.marker) + 1, bullet.marker == bullet.marker:lower())
end

return bullet.marker
end

local function next_prefix(bullet)
local marker = next_marker(bullet)
if not marker then
return nil
end

if bullet.type == "std" 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

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 = parse_standard(line)
local bullet = resolve_bullet(parse_line(line), lnum)

if mode ~= "n" and not at_eol(line) then
feed("<CR>")
Expand All @@ -48,7 +212,13 @@ function M.insert_new_bullet()
return ""
end

insert_line(lnum, bullet.indent .. bullet.marker .. bullet.spacing)
local prefix = next_prefix(bullet)
if not prefix then
feed("<CR>")
return ""
end

insert_line(lnum, prefix)

if mode == "n" then
vim.cmd.startinsert({ bang = true })
Expand Down
95 changes: 95 additions & 0 deletions lua/bullets/ordinal.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
local M = {}

local roman_values = {
{ 1000, "m" },
{ 900, "cm" },
{ 500, "d" },
{ 400, "cd" },
{ 100, "c" },
{ 90, "xc" },
{ 50, "l" },
{ 40, "xl" },
{ 10, "x" },
{ 9, "ix" },
{ 5, "v" },
{ 4, "iv" },
{ 1, "i" },
}

function M.abc_to_number(value)
local result = 0
local lower = value:lower()

for i = 1, #lower do
result = result * 26 + lower:byte(i) - string.byte("a") + 1
end

return result
end

function M.number_to_abc(value, lower)
local base = lower and string.byte("a") or string.byte("A")
local result = ""

while value > 0 do
value = value - 1
result = string.char(base + value % 26) .. result
value = math.floor(value / 26)
end

return result
end

function M.roman_to_number(value)
local roman = value:lower()
local result = 0
local index = 1

while index <= #roman do
local matched = false
for _, pair in ipairs(roman_values) do
local number, letters = pair[1], pair[2]
if roman:sub(index, index + #letters - 1) == letters then
result = result + number
index = index + #letters
matched = true
break
end
end

if not matched then
return nil
end
end

return result
end

function M.number_to_roman(value, lower)
local result = ""

for _, pair in ipairs(roman_values) do
local number, letters = pair[1], pair[2]
while value >= number do
result = result .. letters
value = value - number
end
end

if lower then
return result
end

return result:upper()
end

function M.is_roman(value)
local number = M.roman_to_number(value)
if not number then
return false
end

return M.number_to_roman(number, value == value:lower()) == value
end

return M
17 changes: 9 additions & 8 deletions test/alphabetic_bullets_spec.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local helpers = require("test.helpers")
local active_it = it
local it = pending

describe("Bullets.vim", function()
Expand All @@ -7,7 +8,7 @@ describe("Bullets.vim", function()
helpers.reset_config()
end)

it("adds a new upper case bullet", function()
active_it("adds a new upper case bullet", function()
helpers.new_buffer({
"# Hello there",
"A. this is the first bullet",
Expand All @@ -31,7 +32,7 @@ describe("Bullets.vim", function()
}, helpers.get_lines())
end)

it("adds a new lower case bullet", function()
active_it("adds a new lower case bullet", function()
helpers.new_buffer({
"# Hello there",
"a. this is the first bullet",
Expand All @@ -56,7 +57,7 @@ describe("Bullets.vim", function()
end)

it("adds a new bullet and loops at z", function()
vim.g.bullets_renumber_on_change = 0
require("bullets").setup({ renumber_on_change = false })
helpers.new_buffer({
"# Hello there",
"y. this is the first bullet",
Expand All @@ -80,7 +81,7 @@ describe("Bullets.vim", function()
}, helpers.get_lines())
end)

it("does not add a new bullet when mixed case", function()
active_it("does not add a new bullet when mixed case", function()
-- "Ab." is mixed case so the plugin doesn't recognise it as a bullet.
-- CR is therefore deferred via feedkeys('n'); the 'tx' flag in our outer
-- feedkeys drains that deferred CR, leaving normal mode on a new empty line.
Expand All @@ -99,8 +100,8 @@ describe("Bullets.vim", function()
end)

describe("g:bullets_max_alpha_characters", function()
it("stops adding items after configured max (default 2)", function()
vim.g.bullets_renumber_on_change = 0
active_it("stops adding items after configured max (default 2)", function()
require("bullets").setup({ renumber_on_change = false })
helpers.new_buffer({
"# Hello there",
"zy. this is the first bullet",
Expand All @@ -120,8 +121,8 @@ describe("Bullets.vim", function()
}, helpers.get_lines())
end)

it("does not bullets if configured as 0", function()
vim.g.bullets_max_alpha_characters = 0
active_it("does not bullets if configured as 0", function()
require("bullets").setup({ max_alpha_characters = 0 })
helpers.new_buffer({
"# Hello there",
"a. this is the first bullet",
Expand Down
Loading