|
1 | 1 | local config = require('orgmode.config')
|
| 2 | +local headline_lib = require('orgmode.treesitter.headline') |
2 | 3 | local ts_utils = require('nvim-treesitter.ts_utils')
|
3 | 4 | local query = nil
|
4 | 5 |
|
| 6 | +local function get_indent_pad(linenr) |
| 7 | + local indent_mode = config.org_indent_mode == 'indent' |
| 8 | + if indent_mode then |
| 9 | + local headline = headline_lib.from_cursor({ linenr, 0 }) |
| 10 | + if not headline then |
| 11 | + return 0 |
| 12 | + end |
| 13 | + return headline:level() + 1 |
| 14 | + end |
| 15 | + return 0 |
| 16 | +end |
| 17 | + |
| 18 | +local function get_indent_for_match(matches, linenr, mode) |
| 19 | + linenr = linenr or vim.v.lnum |
| 20 | + mode = mode or vim.fn.mode() |
| 21 | + local prev_linenr = vim.fn.prevnonblank(linenr - 1) |
| 22 | + local match = matches[linenr] |
| 23 | + local prev_line_match = matches[prev_linenr] |
| 24 | + local indent = 0 |
| 25 | + |
| 26 | + if not match and not prev_line_match then |
| 27 | + return indent + get_indent_pad(linenr) |
| 28 | + end |
| 29 | + |
| 30 | + match = match or {} |
| 31 | + prev_line_match = prev_line_match or {} |
| 32 | + |
| 33 | + if match.type == 'headline' then |
| 34 | + -- We ensure we check headlines (even if a bit redundant) to ensure nothing else is checked below |
| 35 | + return 0 |
| 36 | + end |
| 37 | + if match.type == 'listitem' then |
| 38 | + -- We first figure out the indent of the first line of a listitem. Then we |
| 39 | + -- check if we're on the first line or a "hanging" line. In the latter |
| 40 | + -- case, we add the overhang. |
| 41 | + local first_line_indent = nil |
| 42 | + local parent_linenr = match.nesting_parent_linenr |
| 43 | + if parent_linenr then |
| 44 | + local parent_match = matches[parent_linenr] |
| 45 | + if parent_match.type == 'listitem' then |
| 46 | + -- Nested listitem. We recursively find the correct indent for this |
| 47 | + -- based on its parents correct indentation level. |
| 48 | + first_line_indent = vim.fn.indent(parent_linenr) + parent_match.overhang |
| 49 | + end |
| 50 | + end |
| 51 | + -- If the first_line_indent wasn't found then this is the root of the list, as such we just pad accordingly |
| 52 | + indent = first_line_indent or (0 + get_indent_pad(linenr)) |
| 53 | + -- If the current line is hanging content as part of the listitem but not on the same line we want to indent it |
| 54 | + -- such that it's in line with the general content body, not the bullet. |
| 55 | + -- |
| 56 | + -- - I am the "first" line listitem |
| 57 | + -- I am the content body as part of the listitem, but on a different line! |
| 58 | + if linenr ~= match.line_nr then |
| 59 | + indent = indent + match.overhang |
| 60 | + end |
| 61 | + return indent |
| 62 | + end |
| 63 | + if mode:match('^[iR]') and prev_line_match.type == 'listitem' and linenr - prev_linenr < 3 then |
| 64 | + -- In insert mode, we also count the non-listitem line *after* a listitem as |
| 65 | + -- part of the listitem. Keep in mind that double empty lines end a list as |
| 66 | + -- per Orgmode syntax. |
| 67 | + -- |
| 68 | + -- After the first line of a listitem, we have to add the overhang to the |
| 69 | + -- listitem's own base indent. After all further lines, we can simply copy |
| 70 | + -- the indentation. |
| 71 | + indent = get_indent_for_match(matches, prev_linenr) |
| 72 | + if prev_linenr == prev_line_match.line_nr then |
| 73 | + indent = indent + prev_line_match.overhang |
| 74 | + end |
| 75 | + return indent |
| 76 | + end |
| 77 | + if match.indent_type == 'block' then |
| 78 | + -- Blocks do some precalculation of their own against the intended indent level of the parent. As such we just want |
| 79 | + -- to return their indent without any other modifications. |
| 80 | + return match.indent |
| 81 | + end |
| 82 | + |
| 83 | + return indent + get_indent_pad(linenr) |
| 84 | +end |
| 85 | + |
5 | 86 | local get_matches = ts_utils.memoize_by_buf_tick(function(bufnr)
|
6 | 87 | local tree = vim.treesitter.get_parser(bufnr, 'org', {}):parse()
|
7 | 88 | if not tree or not #tree then
|
@@ -47,27 +128,81 @@ local get_matches = ts_utils.memoize_by_buf_tick(function(bufnr)
|
47 | 128 | while parent and parent:type() ~= 'section' and parent:type() ~= 'listitem' do
|
48 | 129 | parent = parent:parent()
|
49 | 130 | end
|
| 131 | + local prev_sibling = node:prev_sibling() |
| 132 | + opts.prev_sibling_linenr = prev_sibling and (prev_sibling:start() + 1) |
50 | 133 | opts.nesting_parent_linenr = parent and (parent:start() + 1)
|
51 | 134 |
|
52 | 135 | for i = range.start.line, range['end'].line - 1 do
|
53 | 136 | matches[i + 1] = opts
|
54 | 137 | end
|
55 | 138 | end
|
56 | 139 |
|
57 |
| - if type == 'paragraph' or type == 'drawer' or type == 'property_drawer' or type == 'block' then |
58 |
| - opts.indent_type = 'other' |
| 140 | + if type == 'block' then |
| 141 | + opts.indent_type = 'block' |
59 | 142 | local parent = node:parent()
|
60 |
| - while parent and parent:type() ~= 'section' do |
| 143 | + while parent and parent:type() ~= 'section' and parent:type() ~= 'listitem' do |
61 | 144 | parent = parent:parent()
|
62 | 145 | end
|
63 |
| - if parent then |
64 |
| - local headline = parent:named_child('headline') |
65 |
| - local stars = vim.treesitter.get_node_text(headline:field('stars')[1], bufnr):len() |
66 |
| - opts.indent = stars + 1 |
67 |
| - for i = range.start.line, range['end'].line - 1 do |
68 |
| - matches[i + 1] = opts |
| 146 | + -- We want to find the difference in indentation level between the item to be indented and the parent node. |
| 147 | + -- If the item is in the block, we shouldn't change the indentation beyond how much we modify the indent of the |
| 148 | + -- block header and footer. This keeps code correctly indented in `BEGIN_SRC` blocks as well as ensuring |
| 149 | + -- `BEGIN_EXAMPLE` blocks don't have their indentation changed inside of them. |
| 150 | + local parent_linenr = parent:start() + 1 |
| 151 | + local parent_indent = get_indent_for_match(matches, parent:start() + 1) |
| 152 | + |
| 153 | + -- We want to align to the listitem body, not the bullet |
| 154 | + if parent:type() == 'listitem' then |
| 155 | + parent_indent = parent_indent + matches[parent_linenr].overhang |
| 156 | + else |
| 157 | + parent_indent = get_indent_pad(range.start.line + 1) |
| 158 | + end |
| 159 | + |
| 160 | + local curr_header_indent = vim.fn.indent(range.start.line + 1) |
| 161 | + local header_indent_diff = curr_header_indent - parent_indent |
| 162 | + local new_header_indent = curr_header_indent - header_indent_diff |
| 163 | + -- Ensure the block footer is properly aligned with the header |
| 164 | + matches[range.start.line + 1] = vim.tbl_deep_extend('force', opts, { |
| 165 | + indent = new_header_indent, |
| 166 | + }) |
| 167 | + matches[range['end'].line] = vim.tbl_deep_extend('force', opts, { |
| 168 | + indent = new_header_indent, |
| 169 | + }) |
| 170 | + |
| 171 | + local content_indent_pad = 0 |
| 172 | + -- Only include the header line and the content. Do not include the footer in the loop. |
| 173 | + for i = range.start.line + 1, range['end'].line - 2 do |
| 174 | + local curr_indent = vim.fn.indent(i + 1) |
| 175 | + -- Correctly align the pad to the new header position if it was underindented |
| 176 | + local new_indent_pad = new_header_indent - curr_indent |
| 177 | + -- If the current content indentaion is less than the new header indent we want to increase all of the |
| 178 | + -- content by the largest difference in indentation between a given content line and the new header indent. |
| 179 | + if curr_indent < new_header_indent then |
| 180 | + content_indent_pad = math.max(new_indent_pad, content_indent_pad) |
| 181 | + else |
| 182 | + -- If the current content indentation is more than the new header indentation, but it was the current |
| 183 | + -- content indentation was less than the current header indent then we want to add some indentation onto |
| 184 | + -- the content by the largest negative difference (meaning -1 > -2 > -3 so take -1 as the pad). |
| 185 | + -- |
| 186 | + -- We do a check for 0 here as we don't want to do a max of neg number against 0. 0 will always win. As |
| 187 | + -- such if the current pad is 0 just set to the new calculated pad. |
| 188 | + if content_indent_pad == 0 then |
| 189 | + content_indent_pad = new_indent_pad |
| 190 | + else |
| 191 | + content_indent_pad = math.max(new_indent_pad, content_indent_pad) |
| 192 | + end |
69 | 193 | end
|
70 | 194 | end
|
| 195 | + -- If any of the content is underindented relative to the header and footer, we need to indent all of the |
| 196 | + -- content until the most underindented content is equal in indention to the header and footer. |
| 197 | + -- |
| 198 | + -- Only loop the content. |
| 199 | + for i = range.start.line + 1, range['end'].line - 2 do |
| 200 | + matches[i + 1] = vim.tbl_deep_extend('force', opts, { |
| 201 | + indent = vim.fn.indent(i + 1) + content_indent_pad, |
| 202 | + }) |
| 203 | + end |
| 204 | + elseif type == 'paragraph' or type == 'drawer' or type == 'property_drawer' then |
| 205 | + opts.indent_type = 'other' |
71 | 206 | end
|
72 | 207 | end
|
73 | 208 | end
|
@@ -119,87 +254,10 @@ end
|
119 | 254 | local function indentexpr(linenr, mode)
|
120 | 255 | linenr = linenr or vim.v.lnum
|
121 | 256 | mode = mode or vim.fn.mode()
|
122 |
| - local noindent_mode = config.org_indent_mode == 'noindent' |
123 | 257 | query = query or vim.treesitter.query.get('org', 'org_indent')
|
124 |
| - |
125 |
| - local prev_linenr = vim.fn.prevnonblank(linenr - 1) |
126 |
| - |
127 | 258 | local matches = get_matches(0)
|
128 |
| - local match = matches[linenr] |
129 |
| - local prev_line_match = matches[prev_linenr] |
130 |
| - |
131 |
| - if not match and not prev_line_match then |
132 |
| - return -1 |
133 |
| - end |
134 |
| - |
135 |
| - match = match or {} |
136 |
| - prev_line_match = prev_line_match or {} |
137 |
| - |
138 |
| - if prev_line_match.type == 'headline' then |
139 |
| - if noindent_mode or (match.type == 'headline' and match.stars > 0) then |
140 |
| - return 0 |
141 |
| - end |
142 |
| - return prev_line_match.indent |
143 |
| - end |
144 |
| - |
145 |
| - if match.type == 'headline' then |
146 |
| - return 0 |
147 |
| - end |
148 |
| - |
149 |
| - if match.type == 'listitem' then |
150 |
| - -- We first figure out the indent of the first line of a listitem. Then we |
151 |
| - -- check if we're on the first line or a "hanging" line. In the latter |
152 |
| - -- case, we add the overhang. |
153 |
| - local first_line_indent |
154 |
| - local parent_linenr = match.nesting_parent_linenr |
155 |
| - if parent_linenr then |
156 |
| - local parent_match = matches[parent_linenr] |
157 |
| - if parent_match.type == 'listitem' then |
158 |
| - -- Nested listitem. Because two listitems cannot start on the same line, |
159 |
| - -- we simply fetch the parent's indentation and add its overhang. |
160 |
| - -- Don't use parent_match.indent, it might be stale if the parent |
161 |
| - -- already got reindented. |
162 |
| - first_line_indent = vim.fn.indent(parent_linenr) + parent_match.overhang |
163 |
| - elseif parent_match.type == 'headline' and not noindent_mode then |
164 |
| - -- Un-nested list inside a section, indent according to section. |
165 |
| - first_line_indent = parent_match.indent |
166 |
| - else |
167 |
| - -- Noindent mode. |
168 |
| - first_line_indent = 0 |
169 |
| - end |
170 |
| - else |
171 |
| - -- Top-level list before the first headline. |
172 |
| - first_line_indent = 0 |
173 |
| - end |
174 |
| - -- Add overhang if this is a hanging line. |
175 |
| - if linenr ~= match.line_nr then |
176 |
| - return first_line_indent + match.overhang |
177 |
| - end |
178 |
| - return first_line_indent |
179 |
| - end |
180 |
| - |
181 |
| - -- In insert mode, we also count the non-listitem line *after* a listitem as |
182 |
| - -- part of the listitem. Keep in mind that double empty lines end a list as |
183 |
| - -- per Orgmode syntax. |
184 |
| - if mode:match('^[iR]') and prev_line_match.type == 'listitem' and linenr - prev_linenr < 3 then |
185 |
| - -- After the first line of a listitem, we have to add the overhang to the |
186 |
| - -- listitem's own base indent. After all further lines, we can simply copy |
187 |
| - -- the indentation. |
188 |
| - if prev_linenr == prev_line_match.line_nr then |
189 |
| - return vim.fn.indent(prev_linenr) + prev_line_match.overhang |
190 |
| - end |
191 |
| - return vim.fn.indent(prev_linenr) |
192 |
| - end |
193 |
| - |
194 |
| - if noindent_mode then |
195 |
| - return 0 |
196 |
| - end |
197 |
| - |
198 |
| - if match.indent_type == 'other' then |
199 |
| - return match.indent |
200 |
| - end |
201 | 259 |
|
202 |
| - return vim.fn.indent(prev_linenr) |
| 260 | + return get_indent_for_match(matches, linenr, mode) |
203 | 261 | end
|
204 | 262 |
|
205 | 263 | local function foldtext()
|
|
0 commit comments