Skip to content

Commit 3da1bfc

Browse files
feat: improve table support for all conceal levels
## Details Currently if users change the conceallevel from the non default value they were likely to experience problems with table rendering. Level 2 would likely function just as well as the default of 3, but 0 and 1 required some modifications. Level 0, concealing is entirely disabled: - Alignment according to indicator is impossible, text cannot be shifted - Rather than shifting text add a special case and right pad cells based on the amount of filler needed to get spaces to align - The width calculation required no changes Level 1, concealed blocks display a single character: - Alignment according to indicator is possible, but we need to conceal an additional character to account for the one that gets added - Width calculation must mark one less character as concealed per block Add unit tests to validate behavior.
1 parent 3368691 commit 3da1bfc

File tree

6 files changed

+115
-34
lines changed

6 files changed

+115
-34
lines changed

Diff for: lua/render-markdown/core/context.lua

+26-13
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ local Str = require('render-markdown.lib.str')
44
local log = require('render-markdown.core.log')
55
local util = require('render-markdown.core.util')
66

7+
---@class render.md.context.Conceal
8+
---@field enabled boolean
9+
---@field block integer
10+
---@field rows? table<integer, [integer, integer][]>
11+
712
---@class render.md.Context
813
---@field private buf integer
914
---@field private win integer
1015
---@field private ranges render.md.Range[]
1116
---@field private callouts table<integer, render.md.CustomCallout>
1217
---@field private checkboxes table<integer, render.md.CustomCheckbox>
13-
---@field private conceal? table<integer, [integer, integer][]>
1418
---@field private links table<integer, [integer, integer, integer][]>
1519
---@field private window_width? integer
1620
---@field mode string
21+
---@field conceal render.md.context.Conceal
1722
---@field last_heading? integer
1823
local Context = {}
1924
Context.__index = Context
@@ -28,20 +33,26 @@ function Context.new(buf, win, mode, offset)
2833
self.buf = buf
2934
self.win = win
3035

31-
local ranges = { Context.compute_range(self.buf, self.win, offset) }
36+
local ranges = {}
3237
for _, window in ipairs(util.windows(buf)) do
33-
if window ~= self.win then
34-
table.insert(ranges, Context.compute_range(self.buf, window, offset))
35-
end
38+
table.insert(ranges, Context.compute_range(self.buf, window, offset))
3639
end
3740
self.ranges = Range.coalesce(ranges)
3841

3942
self.callouts = {}
4043
self.checkboxes = {}
41-
self.conceal = nil
4244
self.links = {}
4345
self.window_width = nil
4446
self.mode = mode
47+
48+
local conceal_level = util.get('win', self.win, 'conceallevel')
49+
self.conceal = {
50+
enabled = conceal_level > 0,
51+
-- At conceal level 1 each block is replaced with one character
52+
block = conceal_level == 1 and 1 or 0,
53+
rows = nil,
54+
}
55+
4556
self.last_heading = nil
4657
return self
4758
end
@@ -93,7 +104,7 @@ end
93104

94105
---@return integer
95106
function Context:tab_size()
96-
return util.get('buf', self.buf, 'tabstop') --[[@as integer]]
107+
return util.get('buf', self.buf, 'tabstop')
97108
end
98109

99110
---@param node? render.md.Node
@@ -215,9 +226,12 @@ function Context:concealed(node)
215226
for _, index in ipairs(vim.fn.str2list(node.text)) do
216227
local ch = vim.fn.nr2char(index)
217228
for _, range in ipairs(ranges) do
218-
-- Essentially vim.treesitter.is_in_node_range but only care about column
229+
-- Column level implementation of vim.treesitter.is_in_node_range
219230
if col >= range[1] and col + 1 <= range[2] then
220231
result = result + Str.width(ch)
232+
if col == range[1] then
233+
result = result - self.conceal.block
234+
end
221235
end
222236
end
223237
col = col + #ch
@@ -229,18 +243,17 @@ end
229243
---@param row integer
230244
---@return [integer, integer][]
231245
function Context:get_conceal(row)
232-
if self.conceal == nil then
233-
self.conceal = self:compute_conceal()
246+
if self.conceal.rows == nil then
247+
self.conceal.rows = self:compute_conceal()
234248
end
235-
return self.conceal[row] or {}
249+
return self.conceal.rows[row] or {}
236250
end
237251

238252
---Cached row level implementation of vim.treesitter.get_captures_at_pos
239253
---@private
240254
---@return table<integer, [integer, integer][]>
241255
function Context:compute_conceal()
242-
local conceallevel = util.get('win', self.win, 'conceallevel')
243-
if conceallevel == 0 then
256+
if not self.conceal.enabled then
244257
return {}
245258
end
246259
local ranges = {}

Diff for: lua/render-markdown/core/util.lua

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ end
7676
---@param id integer
7777
---@param name string
7878
---@return render.md.option.Value
79+
---@overload fun(variant: 'buf', id: integer, name: 'buftype'): string
80+
---@overload fun(variant: 'buf', id: integer, name: 'filetype'): string
81+
---@overload fun(variant: 'buf', id: integer, name: 'tabstop'): integer
82+
---@overload fun(variant: 'win', id: integer, name: 'conceallevel'): integer
83+
---@overload fun(variant: 'win', id: integer, name: 'diff'): boolean
7984
function M.get(variant, id, name)
8085
if variant == 'buf' then
8186
return vim.api.nvim_get_option_value(name, { buf = id })

Diff for: lua/render-markdown/health.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local state = require('render-markdown.state')
44
local M = {}
55

66
---@private
7-
M.version = '7.5.2'
7+
M.version = '7.5.3'
88

99
function M.check()
1010
M.start('version')

Diff for: lua/render-markdown/init.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ local M = {}
2525
---@field public enabled? boolean
2626
---@field public query? string
2727

28-
---@alias render.md.option.Value number|string|boolean
28+
---@alias render.md.option.Value number|integer|string|boolean
2929

3030
---@class (exact) render.md.UserWindowOption
3131
---@field public default? render.md.option.Value

Diff for: lua/render-markdown/render/table.lua

+38-19
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ function Render:setup()
4545
return false
4646
end
4747

48-
local delim, table_rows = nil, {}
48+
---@type render.md.table.DelimRow?
49+
local delim = nil
50+
---@type render.md.Node[]
51+
local table_rows = {}
4952
self.node:for_each_child(function(row)
5053
if row.type == 'pipe_table_delimiter_row' then
5154
delim = self:parse_delim(row)
@@ -57,11 +60,13 @@ function Render:setup()
5760
end
5861
end
5962
end)
63+
6064
-- Ensure delimiter and rows exist for table
6165
if delim == nil or #table_rows == 0 then
6266
return false
6367
end
6468

69+
---@type render.md.table.Row[]
6570
local rows = {}
6671
table.sort(table_rows)
6772
for _, table_row in ipairs(table_rows) do
@@ -70,14 +75,16 @@ function Render:setup()
7075
table.insert(rows, row)
7176
end
7277
end
73-
-- Store the max width information in the delimiter
78+
79+
-- Store the max width in the delimiter
7480
for _, row in ipairs(rows) do
7581
for i, column in ipairs(row.columns) do
76-
local delim_column, width = delim.columns[i], column.width
82+
local width = column.width
7783
if self.table.cell == 'trimmed' then
7884
local space_available = column.space.left + column.space.right - (2 * self.table.padding)
7985
width = width - math.max(space_available, 0)
8086
end
87+
local delim_column = delim.columns[i]
8188
delim_column.width = math.max(delim_column.width, width)
8289
end
8390
end
@@ -95,6 +102,7 @@ function Render:parse_delim(row)
95102
if pipes == nil or cells == nil then
96103
return nil
97104
end
105+
---@type render.md.table.DelimColumn[]
98106
local columns = {}
99107
for i, cell in ipairs(cells) do
100108
local width = pipes[i + 1].start_col - pipes[i].end_col
@@ -106,22 +114,25 @@ function Render:parse_delim(row)
106114
elseif self.table.cell == 'trimmed' then
107115
width = self.table.min_width
108116
end
109-
table.insert(columns, { width = width, alignment = Render.alignment(cell) })
117+
---@type render.md.table.DelimColumn
118+
local column = { width = width, alignment = Render.alignment(cell) }
119+
table.insert(columns, column)
110120
end
121+
---@type render.md.table.DelimRow
111122
return { node = row, columns = columns }
112123
end
113124

114125
---@private
115-
---@param cell render.md.Node
126+
---@param node render.md.Node
116127
---@return render.md.table.Alignment
117-
function Render.alignment(cell)
118-
local align_left = cell:child('pipe_table_align_left') ~= nil
119-
local align_right = cell:child('pipe_table_align_right') ~= nil
120-
if align_left and align_right then
128+
function Render.alignment(node)
129+
local has_left = node:child('pipe_table_align_left') ~= nil
130+
local has_right = node:child('pipe_table_align_right') ~= nil
131+
if has_left and has_right then
121132
return 'center'
122-
elseif align_left then
133+
elseif has_left then
123134
return 'left'
124-
elseif align_right then
135+
elseif has_right then
125136
return 'right'
126137
else
127138
return 'default'
@@ -137,6 +148,7 @@ function Render:parse_row(row, num_columns)
137148
if pipes == nil or cells == nil or #cells ~= num_columns then
138149
return nil
139150
end
151+
---@type render.md.table.Column[]
140152
local columns = {}
141153
for i, cell in ipairs(cells) do
142154
local start_col, end_col = pipes[i].end_col, pipes[i + 1].start_col
@@ -152,14 +164,17 @@ function Render:parse_row(row, num_columns)
152164
if width < 0 then
153165
return nil
154166
end
155-
table.insert(columns, {
167+
---@type render.md.table.Column
168+
local column = {
156169
row = cell.start_row,
157170
start_col = cell.start_col,
158171
end_col = cell.end_col,
159172
width = width,
160173
space = space,
161-
})
174+
}
175+
table.insert(columns, column)
162176
end
177+
---@type render.md.table.Row
163178
return { node = row, pipes = pipes, columns = columns }
164179
end
165180

@@ -200,20 +215,20 @@ end
200215
function Render:delimiter()
201216
local delim, border = self.data.delim, self.table.border
202217

218+
local indicator, icon = self.table.alignment_indicator, border[11]
203219
local sections = Iter.list.map(delim.columns, function(column)
204-
local indicator, box = self.table.alignment_indicator, border[11]
205220
-- If column is small there's no good place to put the alignment indicator
206221
-- Alignment indicator must be exactly one character wide
207222
-- Do not put an indicator for default alignment
208223
if column.width < 3 or Str.width(indicator) ~= 1 or column.alignment == 'default' then
209-
return box:rep(column.width)
224+
return icon:rep(column.width)
210225
end
211226
if column.alignment == 'left' then
212-
return indicator .. box:rep(column.width - 1)
227+
return indicator .. icon:rep(column.width - 1)
213228
elseif column.alignment == 'right' then
214-
return box:rep(column.width - 1) .. indicator
229+
return icon:rep(column.width - 1) .. indicator
215230
else
216-
return indicator .. box:rep(column.width - 2) .. indicator
231+
return indicator .. icon:rep(column.width - 2) .. indicator
217232
end
218233
end)
219234

@@ -250,7 +265,10 @@ function Render:row(row)
250265
for i, column in ipairs(row.columns) do
251266
local delim_column = delim.columns[i]
252267
local filler = delim_column.width - column.width
253-
if delim_column.alignment == 'center' then
268+
if not self.context.conceal.enabled then
269+
-- Without concealing it is impossible to do full alignment
270+
self:shift(column, 'right', filler)
271+
elseif delim_column.alignment == 'center' then
254272
local shift = math.floor((filler + column.space.right - column.space.left) / 2)
255273
self:shift(column, 'left', shift)
256274
self:shift(column, 'right', filler - shift)
@@ -290,6 +308,7 @@ function Render:shift(column, side, amount)
290308
virt_text_pos = 'inline',
291309
})
292310
elseif amount < 0 then
311+
amount = amount - self.context.conceal.block
293312
self.marks:add(true, column.row, col + amount, {
294313
priority = 0,
295314
end_col = col,

Diff for: tests/table_spec.lua

+44
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,48 @@ describe('table.md', function()
225225
' └───────────┴───────────┘',
226226
})
227227
end)
228+
229+
it('conceallevel 0', function()
230+
util.setup('tests/data/table.md', { win_options = { conceallevel = { rendered = 0 } } })
231+
232+
util.assert_screen({
233+
'󰫎 1 󰲡 Table with Inline',
234+
' 2',
235+
' ┌───────────┬──────────────────────────┐',
236+
' 3 │ Heading 1 │ `Heading 2` │',
237+
' 4 ├───────────┼─────────────────────────━┤',
238+
' 5 │ `Item 行` │ 󰖟 [link](https://行.com) │',
239+
' └───────────┴──────────────────────────┘',
240+
' 6',
241+
'󰫎 7 󰲡 Table no Inline',
242+
' 8',
243+
' ┌───────────┬───────────┐',
244+
' 9 │ Heading 1 │ Heading 2 │',
245+
' 10 ├───────────┼───────────┤',
246+
' 11 │ Item 1 │ Item 2 │',
247+
' └───────────┴───────────┘',
248+
})
249+
end)
250+
251+
it('conceallevel 1', function()
252+
util.setup('tests/data/table.md', { win_options = { conceallevel = { rendered = 1 } } })
253+
254+
util.assert_screen({
255+
'󰫎 1 󰲡 Table with Inline',
256+
' 2',
257+
' ┌───────────┬────────────────────────┐',
258+
' 3 │ Heading 1 │ Heading 2 │',
259+
' 4 ├───────────┼───────────────────────━┤',
260+
' 5 │ Item 行 │ 󰖟 link │',
261+
' └───────────┴────────────────────────┘',
262+
' 6',
263+
'󰫎 7 󰲡 Table no Inline',
264+
' 8',
265+
' ┌───────────┬───────────┐',
266+
' 9 │ Heading 1 │ Heading 2 │',
267+
' 10 ├───────────┼───────────┤',
268+
' 11 │ Item 1 │ Item 2 │',
269+
' └───────────┴───────────┘',
270+
})
271+
end)
228272
end)

0 commit comments

Comments
 (0)