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
93 changes: 61 additions & 32 deletions lua/orgmode/org/indent.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,24 @@ local get_matches = ts_utils.memoize_by_buf_tick(function(bufnr)
matches[range.start.line + 1] = opts
end

if type == 'list' then
local first_list_item = node:named_child(0)
local first_list_item_linenr = first_list_item:start()
local first_item_indent = vim.fn.indent(first_list_item_linenr + 1)
opts.indent = first_item_indent
if type == 'listitem' then
local content = node:named_child(1)
if content then
local content_linenr, content_indent = content:start()
if content_linenr == range.start.line then
opts.overhang = content_indent - opts.indent
end
end
if not opts.overhang then
local bullet = node:named_child(0)
opts.overhang = vim.treesitter.get_node_text(bullet, bufnr):len() + 1
end

local parent = node:parent()
while parent and parent:type() ~= 'section' and parent:type() ~= 'listitem' do
parent = parent:parent()
end
opts.nesting_parent_linenr = parent and (parent:start() + 1)

for i = range.start.line, range['end'].line - 1 do
matches[i + 1] = opts
Expand All @@ -46,9 +59,6 @@ local get_matches = ts_utils.memoize_by_buf_tick(function(bufnr)
local parent = node:parent()
while parent and parent:type() ~= 'section' do
parent = parent:parent()
if not parent then
break
end
end
if parent then
local headline = parent:named_child('headline')
Expand Down Expand Up @@ -106,20 +116,16 @@ local function foldexpr()
return '='
end

local function get_is_list_item(line)
local line_numbered_list_item = line:match('^%s*(%d+[%)%.]%s+)')
local line_unordered_list_item = line:match('^%s*([%+%-]%s+)')
return line_numbered_list_item or line_unordered_list_item
end

local function indentexpr()
local function indentexpr(linenr, mode)
linenr = linenr or vim.v.lnum
mode = mode or vim.fn.mode()
local noindent_mode = config.org_indent_mode == 'noindent'
query = query or vim.treesitter.query.get('org', 'org_indent')

local prev_linenr = vim.fn.prevnonblank(vim.v.lnum - 1)
local prev_linenr = vim.fn.prevnonblank(linenr - 1)

local matches = get_matches(0)
local match = matches[vim.v.lnum]
local match = matches[linenr]
local prev_line_match = matches[prev_linenr]

if not match and not prev_line_match then
Expand All @@ -140,26 +146,49 @@ local function indentexpr()
return 0
end

if match.type == 'list' and prev_line_match.type == 'list' then
local prev_line_list_item = get_is_list_item(vim.fn.getline(prev_linenr))
local cur_line_list_item = get_is_list_item(vim.fn.getline(vim.v.lnum))

if cur_line_list_item then
local diff = match.indent - vim.fn.indent(match.line_nr)
local indent = vim.fn.indent(vim.v.lnum)
return indent - diff
if match.type == 'listitem' then
-- We first figure out the indent of the first line of a listitem. Then we
-- check if we're on the first line or a "hanging" line. In the latter
-- case, we add the overhang.
local first_line_indent
local parent_linenr = match.nesting_parent_linenr
if parent_linenr then
local parent_match = matches[parent_linenr]
if parent_match.type == 'listitem' then
-- Nested listitem. Because two listitems cannot start on the same line,
-- we simply fetch the parent's indentation and add its overhang.
-- Don't use parent_match.indent, it might be stale if the parent
-- already got reindented.
first_line_indent = vim.fn.indent(parent_linenr) + parent_match.overhang
elseif parent_match.type == 'headline' and not noindent_mode then
-- Un-nested list inside a section, indent according to section.
first_line_indent = parent_match.indent
else
-- Noindent mode.
first_line_indent = 0
end
else
-- Top-level list before the first headline.
first_line_indent = 0
end

if prev_line_list_item then
return vim.fn.indent(prev_linenr) + prev_line_list_item:len()
-- Add overhang if this is a hanging line.
if linenr ~= match.line_nr then
return first_line_indent + match.overhang
end
return first_line_indent
end

if prev_line_match.type == 'list' and match.type ~= 'list' then
local prev_line_list_item = get_is_list_item(vim.fn.getline(prev_linenr))
if prev_line_list_item then
return vim.fn.indent(prev_linenr) + prev_line_list_item:len()
-- In insert mode, we also count the non-listitem line *after* a listitem as
-- part of the listitem. Keep in mind that double empty lines end a list as
-- per Orgmode syntax.
if mode:match('^[iR]') and prev_line_match.type == 'listitem' and linenr - prev_linenr < 3 then
-- After the first line of a listitem, we have to add the overhang to the
-- listitem's own base indent. After all further lines, we can simply copy
-- the indentation.
if prev_linenr == prev_line_match.line_nr then
return vim.fn.indent(prev_linenr) + prev_line_match.overhang
end
return vim.fn.indent(prev_linenr)
end

if noindent_mode then
Expand Down
2 changes: 1 addition & 1 deletion queries/org/org_indent.scm
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(headline) @OrgIndentHeadline
(body (list) @OrgList)
(listitem) @OrgListItem
(body (paragraph) @OrgParagraph)
(body (drawer) @OrgDrawer)
(section (property_drawer) @OrgPropertyDrawer)
Expand Down
2 changes: 1 addition & 1 deletion scripts/gendoc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ local files = {
local destination = 'doc/orgmode_api.txt'

vim.fn.system(('lemmy-help %s > %s'):format(table.concat(files, ' '), destination))
vim.cmd[[qa!]]
vim.cmd([[qa!]])
1 change: 1 addition & 0 deletions tests/minimal_init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ M.setup({
vim.opt.runtimepath:prepend(vim.fn.fnamemodify(base_root_path, ':h'))
vim.opt.termguicolors = true
vim.opt.swapfile = false
vim.opt.expandtab = true -- Accommodates some deep nesting in indent_spec.lua
vim.cmd.language('en_US.utf-8')
vim.env.TZ = 'Europe/London'
vim.g.mapleader = ','
Expand Down
215 changes: 215 additions & 0 deletions tests/plenary/org/indent_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
local config = require('orgmode.config')
local Indent = require('orgmode.org.indent')
local helpers = require('tests.plenary.ui.helpers')

-- Helper assert function.
local function expect_whole_buffer(expected)
assert.are.same(expected, vim.api.nvim_buf_get_lines(0, 0, -1, false))
end

-- We want to run all tests under both values for `org_indent_mode`: "indent"
-- and "noindent". So it is easier to put all tests into test functions and
-- check the indent mode, then run them under two different `describe()`.

local function test_full_reindent()
local unformatted_file = {
'* TODO First task',
'SCHEDULED: <1970-01-01 Thu>',
'',
'1. Ordered list',
' a) nested list',
' over-indented',
' over-indented',
' b) nested list',
' under-indented',
'2. Ordered list',
'Not part of the list',
'',
'** Second task',
' DEADLINE: <1970-01-01 Thu>',
'',
'- Unordered list',
' + nested list',
' over-indented',
' over-indented',
' + nested list',
' under-indented',
'- unordered list',
' + nested list',
' * triple nested list',
' continuation',
' part of the first-level list',
'Not part of the list',
}
helpers.load_file_content(unformatted_file)
vim.cmd([[silent norm 0gg=G]])
local expected
if config.org_indent_mode == 'indent' then
expected = {
'* TODO First task',
' SCHEDULED: <1970-01-01 Thu>',
'',
' 1. Ordered list',
' a) nested list',
' over-indented',
' over-indented',
' b) nested list',
' under-indented',
' 2. Ordered list',
' Not part of the list',
'',
'** Second task',
' DEADLINE: <1970-01-01 Thu>',
'',
' - Unordered list',
' + nested list',
' over-indented',
' over-indented',
' + nested list',
' under-indented',
' - unordered list',
' + nested list',
' * triple nested list',
' continuation',
' part of the first-level list',
' Not part of the list',
}
elseif config.org_indent_mode == 'noindent' then
expected = {
'* TODO First task',
'SCHEDULED: <1970-01-01 Thu>',
'',
'1. Ordered list',
' a) nested list',
' over-indented',
' over-indented',
' b) nested list',
' under-indented',
'2. Ordered list',
'Not part of the list',
'',
'** Second task',
'DEADLINE: <1970-01-01 Thu>',
'',
'- Unordered list',
' + nested list',
' over-indented',
' over-indented',
' + nested list',
' under-indented',
'- unordered list',
' + nested list',
' * triple nested list',
' continuation',
' part of the first-level list',
'Not part of the list',
}
end
expect_whole_buffer(expected)
end

local function test_newly_written_list()
helpers.load_file_content({})
local user_input = vim.api.nvim_replace_termcodes('i- new item<CR>second line<CR>third line<Esc>', true, true, true)
vim.api.nvim_feedkeys(user_input, 'ntix', false)
local expected
if config.org_indent_mode == 'indent' then
expected = {
'- new item',
' second line',
' third line',
}
elseif config.org_indent_mode == 'noindent' then
expected = {
'- new item',
' second line',
' third line',
}
end
expect_whole_buffer(expected)
end

local function test_insertion_to_an_existing_list()
helpers.load_file_content({ '- first item', '- third item' })
vim.cmd([[normal! o]])
local user_input = vim.api.nvim_replace_termcodes('i- new item<CR>second line<CR>third line<Esc>', true, true, true)
vim.api.nvim_feedkeys(user_input, 'ntix', false)
local expected
if config.org_indent_mode == 'indent' then
expected = {
'- first item',
'- new item',
' second line',
' third line',
'- third item',
}
elseif config.org_indent_mode == 'noindent' then
expected = {
'- first item',
'- new item',
' second line',
' third line',
'- third item',
}
end
expect_whole_buffer(expected)
end

local function test_add_line_breaks_to_existing_file()
helpers.load_file_content({ '- first item', '- second item' })
local user_input = vim.api.nvim_replace_termcodes('wwi<CR><Esc><Down><Right>i<CR><Esc>', true, true, true)
vim.api.nvim_feedkeys(user_input, 'ntix', false)
local expected = {
'- first ',
' item',
'- ',
' second item',
}
expect_whole_buffer(expected)
end

-- The actual tests are here.

describe('with "indent",', function()
before_each(function()
config:extend({ org_indent_mode = 'indent' })
end)

it('"0gg=G" reindents the whole file', function()
test_full_reindent()
end)

it('a newly written list is well indented', function()
test_newly_written_list()
end)

it('insertion to an existing list is well indented', function()
test_insertion_to_an_existing_list()
end)

it('adding line breaks to list items maintains indent', function()
test_add_line_breaks_to_existing_file()
end)
end)

describe('with "noindent",', function()
before_each(function()
config:extend({ org_indent_mode = 'noindent' })
end)

it('"0gg=G" reindents the whole file', function()
test_full_reindent()
end)

it('a newly written list is well indented', function()
test_newly_written_list()
end)

it('insertion into an existing list is well indented', function()
test_insertion_to_an_existing_list()
end)

it('adding line breaks to list items maintains indent', function()
test_add_line_breaks_to_existing_file()
end)
end)