Skip to content

Commit

Permalink
Refactor indentexpr() to fix noindent indentation for lists.
Browse files Browse the repository at this point in the history
Closes #473.

I tried doing small fixes to this code, but kept running into edge
cases. Hence, this complete rewrite. :) The important points:

- The queries in `indent.scm` no longer match on top-level (i.e
  un-nested) lists, but instead on list items of all levels.

- List item indentation no longer relies on the previous non-empty line.
  Each list item stores whether it's in a top-level or a nested list and
  calculates its indent based on that.

- The check whether we are in bulleted line or not no longer uses
  `str.match()`, since its pattern was buggy and forgot a few kinds of
  bullets. (namely, indented `*` bullets and `a.` ordered bullets)
  Instead, we compare the current line number to `match.line_nr`. We can
  do that because we query list items instead of lists now.

There is an edge case when the user is appending to a list. We want that
next line to be indented (see #472), but it's technically outside of the
list. At the same time, if an unindented line follows a list, it should
not become part of the list.

The best solution I found for this was to make the behavior of
`indentexpr()` depend on whether we are in insert mode. If yes, the line
after a list is part of the list. If not, it isn't.

The new code also correctly takes into account that two consecutive
empty lines always end a preceding list.
  • Loading branch information
troiganto committed Sep 12, 2023
1 parent e63a64c commit 03272d1
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 50 deletions.
85 changes: 56 additions & 29 deletions lua/orgmode/org/indent.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
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
1 change: 1 addition & 0 deletions tests/minimal_init.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 20 additions & 20 deletions tests/plenary/org/indent_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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>',
Expand All @@ -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
Expand All @@ -81,27 +81,27 @@ 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>',
'',
'- 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
Expand All @@ -123,7 +123,7 @@ local function test_newly_written_list()
expected = {
'- new item',
' second line',
'third line',
' third line',
}
end
expect_whole_buffer(expected)
Expand All @@ -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
Expand Down

0 comments on commit 03272d1

Please sign in to comment.