diff --git a/lua/orgmode/org/indent.lua b/lua/orgmode/org/indent.lua index 08dbf03e2..4b630ecb7 100644 --- a/lua/orgmode/org/indent.lua +++ b/lua/orgmode/org/indent.lua @@ -31,11 +31,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 = ts.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 @@ -47,9 +60,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') @@ -107,12 +117,6 @@ 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(linenr, mode) linenr = linenr or vim.v.lnum mode = mode or vim.fn.mode() @@ -143,26 +147,49 @@ local function indentexpr(linenr, mode) 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(linenr)) - - if cur_line_list_item then - local diff = match.indent - vim.fn.indent(match.line_nr) - local indent = vim.fn.indent(linenr) - 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 indent mode, we also count the non-listem 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 diff --git a/queries/org/org_indent.scm b/queries/org/org_indent.scm index e10ebd3be..c9a78ad82 100644 --- a/queries/org/org_indent.scm +++ b/queries/org/org_indent.scm @@ -1,5 +1,5 @@ (headline) @OrgIndentHeadline -(body (list) @OrgList) +(listitem) @OrgListItem (body (paragraph) @OrgParagraph) (body (drawer) @OrgDrawer) (section (property_drawer) @OrgPropertyDrawer) diff --git a/tests/minimal_init.vim b/tests/minimal_init.vim index e5d7e82d7..cb6f28e67 100644 --- a/tests/minimal_init.vim +++ b/tests/minimal_init.vim @@ -3,6 +3,7 @@ set rtp+=./plenary.nvim set rtp+=./nvim-treesitter set termguicolors set noswapfile +set expandtab " Accommodates some deep nesting in indent_spec.lua language en_US.utf-8 runtime plugin/plenary.vim runtime plugin/nvim-treesitter.lua diff --git a/tests/plenary/org/indent_spec.lua b/tests/plenary/org/indent_spec.lua index 12df251af..13f5e004d 100644 --- a/tests/plenary/org/indent_spec.lua +++ b/tests/plenary/org/indent_spec.lua @@ -51,12 +51,12 @@ local function test_full_reindent() '', ' 1. Ordered list', ' a) nested list', - ' over-indented', - ' over-indented', + ' over-indented', + ' over-indented', ' b) nested list', - ' under-indented', + ' under-indented', ' 2. Ordered list', - ' Not part of the list', + ' Not part of the list', '', '** Second task', ' DEADLINE: <1970-01-01 Thu>', @@ -68,10 +68,10 @@ local function test_full_reindent() ' + nested list', ' under-indented', ' - unordered list', - ' + nested list', - ' * triple nested list', - ' continuation', - ' part of the first-level 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 @@ -81,12 +81,12 @@ local function test_full_reindent() '', '1. Ordered list', ' a) nested list', - 'over-indented', - 'over-indented', - 'b) nested list', - 'under-indented', + ' over-indented', + ' over-indented', + ' b) nested list', + ' under-indented', '2. Ordered list', - ' Not part of the list', + 'Not part of the list', '', '** Second task', 'DEADLINE: <1970-01-01 Thu>', @@ -94,14 +94,14 @@ local function test_full_reindent() '- Unordered list', ' + nested list', ' over-indented', - 'over-indented', + ' over-indented', ' + nested list', ' under-indented', '- unordered list', - ' + nested list', - ' * triple nested list', - 'continuation', - 'part of the first-level list', + ' + nested list', + ' * triple nested list', + ' continuation', + ' part of the first-level list', 'Not part of the list', } end @@ -123,7 +123,7 @@ local function test_newly_written_list() expected = { '- new item', ' second line', - 'third line', + ' third line', } end expect_whole_buffer(expected) @@ -148,7 +148,7 @@ local function test_insertion_to_an_existing_list() '- first item', '- new item', ' second line', - 'third line', + ' third line', '- third item', } end