Skip to content

Commit 1d87dd5

Browse files
author
troiganto
committed
Refactor indentexpr() to fix noindent indentation for lists.
Closes nvim-orgmode#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 nvim-orgmode#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.
1 parent 36c13b2 commit 1d87dd5

File tree

4 files changed

+78
-50
lines changed

4 files changed

+78
-50
lines changed

lua/orgmode/org/indent.lua

+56-29
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,24 @@ local get_matches = ts_utils.memoize_by_buf_tick(function(bufnr)
3131
matches[range.start.line + 1] = opts
3232
end
3333

34-
if type == 'list' then
35-
local first_list_item = node:named_child(0)
36-
local first_list_item_linenr = first_list_item:start()
37-
local first_item_indent = vim.fn.indent(first_list_item_linenr + 1)
38-
opts.indent = first_item_indent
34+
if type == 'listitem' then
35+
local content = node:named_child(1)
36+
if content then
37+
local content_linenr, content_indent = content:start()
38+
if content_linenr == range.start.line then
39+
opts.overhang = content_indent - opts.indent
40+
end
41+
end
42+
if not opts.overhang then
43+
local bullet = node:named_child(0)
44+
opts.overhang = ts.get_node_text(bullet, bufnr):len() + 1
45+
end
46+
47+
local parent = node:parent()
48+
while parent and parent:type() ~= 'section' and parent:type() ~= 'listitem' do
49+
parent = parent:parent()
50+
end
51+
opts.nesting_parent_linenr = parent and (parent:start() + 1)
3952

4053
for i = range.start.line, range['end'].line - 1 do
4154
matches[i + 1] = opts
@@ -47,9 +60,6 @@ local get_matches = ts_utils.memoize_by_buf_tick(function(bufnr)
4760
local parent = node:parent()
4861
while parent and parent:type() ~= 'section' do
4962
parent = parent:parent()
50-
if not parent then
51-
break
52-
end
5363
end
5464
if parent then
5565
local headline = parent:named_child('headline')
@@ -107,12 +117,6 @@ local function foldexpr()
107117
return '='
108118
end
109119

110-
local function get_is_list_item(line)
111-
local line_numbered_list_item = line:match('^%s*(%d+[%)%.]%s+)')
112-
local line_unordered_list_item = line:match('^%s*([%+%-]%s+)')
113-
return line_numbered_list_item or line_unordered_list_item
114-
end
115-
116120
local function indentexpr(linenr, mode)
117121
linenr = linenr or vim.v.lnum
118122
mode = mode or vim.fn.mode()
@@ -143,26 +147,49 @@ local function indentexpr(linenr, mode)
143147
return 0
144148
end
145149

146-
if match.type == 'list' and prev_line_match.type == 'list' then
147-
local prev_line_list_item = get_is_list_item(vim.fn.getline(prev_linenr))
148-
local cur_line_list_item = get_is_list_item(vim.fn.getline(linenr))
149-
150-
if cur_line_list_item then
151-
local diff = match.indent - vim.fn.indent(match.line_nr)
152-
local indent = vim.fn.indent(linenr)
153-
return indent - diff
150+
if match.type == 'listitem' then
151+
-- We first figure out the indent of the first line of a listitem. Then we
152+
-- check if we're on the first line or a "hanging" line. In the latter
153+
-- case, we add the overhang.
154+
local first_line_indent
155+
local parent_linenr = match.nesting_parent_linenr
156+
if parent_linenr then
157+
local parent_match = matches[parent_linenr]
158+
if parent_match.type == 'listitem' then
159+
-- Nested listitem. Because two listitems cannot start on the same line,
160+
-- we simply fetch the parent's indentation and add its overhang.
161+
-- Don't use parent_match.indent, it might be stale if the parent
162+
-- already got reindented.
163+
first_line_indent = vim.fn.indent(parent_linenr) + parent_match.overhang
164+
elseif parent_match.type == 'headline' and not noindent_mode then
165+
-- Un-nested list inside a section, indent according to section.
166+
first_line_indent = parent_match.indent
167+
else
168+
-- Noindent mode.
169+
first_line_indent = 0
170+
end
171+
else
172+
-- Top-level list before the first headline.
173+
first_line_indent = 0
154174
end
155-
156-
if prev_line_list_item then
157-
return vim.fn.indent(prev_linenr) + prev_line_list_item:len()
175+
-- Add overhang if this is a hanging line.
176+
if linenr ~= match.line_nr then
177+
return first_line_indent + match.overhang
158178
end
179+
return first_line_indent
159180
end
160181

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

168195
if noindent_mode then

queries/org/org_indent.scm

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
(headline) @OrgIndentHeadline
2-
(body (list) @OrgList)
2+
(listitem) @OrgListItem
33
(body (paragraph) @OrgParagraph)
44
(body (drawer) @OrgDrawer)
55
(section (property_drawer) @OrgPropertyDrawer)

tests/minimal_init.vim

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ set rtp+=./plenary.nvim
33
set rtp+=./nvim-treesitter
44
set termguicolors
55
set noswapfile
6+
set expandtab " Accommodates some deep nesting in indent_spec.lua
67
language en_US.utf-8
78
runtime plugin/plenary.vim
89
runtime plugin/nvim-treesitter.lua

tests/plenary/org/indent_spec.lua

+20-20
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ local function test_full_reindent()
5151
'',
5252
' 1. Ordered list',
5353
' a) nested list',
54-
' over-indented',
55-
' over-indented',
54+
' over-indented',
55+
' over-indented',
5656
' b) nested list',
57-
' under-indented',
57+
' under-indented',
5858
' 2. Ordered list',
59-
' Not part of the list',
59+
' Not part of the list',
6060
'',
6161
'** Second task',
6262
' DEADLINE: <1970-01-01 Thu>',
@@ -68,10 +68,10 @@ local function test_full_reindent()
6868
' + nested list',
6969
' under-indented',
7070
' - unordered list',
71-
' + nested list',
72-
' * triple nested list',
73-
' continuation',
74-
' part of the first-level list',
71+
' + nested list',
72+
' * triple nested list',
73+
' continuation',
74+
' part of the first-level list',
7575
' Not part of the list',
7676
}
7777
elseif config.org_indent_mode == 'noindent' then
@@ -81,27 +81,27 @@ local function test_full_reindent()
8181
'',
8282
'1. Ordered list',
8383
' a) nested list',
84-
'over-indented',
85-
'over-indented',
86-
'b) nested list',
87-
'under-indented',
84+
' over-indented',
85+
' over-indented',
86+
' b) nested list',
87+
' under-indented',
8888
'2. Ordered list',
89-
' Not part of the list',
89+
'Not part of the list',
9090
'',
9191
'** Second task',
9292
'DEADLINE: <1970-01-01 Thu>',
9393
'',
9494
'- Unordered list',
9595
' + nested list',
9696
' over-indented',
97-
'over-indented',
97+
' over-indented',
9898
' + nested list',
9999
' under-indented',
100100
'- unordered list',
101-
' + nested list',
102-
' * triple nested list',
103-
'continuation',
104-
'part of the first-level list',
101+
' + nested list',
102+
' * triple nested list',
103+
' continuation',
104+
' part of the first-level list',
105105
'Not part of the list',
106106
}
107107
end
@@ -123,7 +123,7 @@ local function test_newly_written_list()
123123
expected = {
124124
'- new item',
125125
' second line',
126-
'third line',
126+
' third line',
127127
}
128128
end
129129
expect_whole_buffer(expected)
@@ -148,7 +148,7 @@ local function test_insertion_to_an_existing_list()
148148
'- first item',
149149
'- new item',
150150
' second line',
151-
'third line',
151+
' third line',
152152
'- third item',
153153
}
154154
end

0 commit comments

Comments
 (0)