Skip to content

Commit 767707e

Browse files
feat: support same buffer in multiple windows
Issue: #184 This plugin optimizes performance for large files by only attempting to render what is visible in the window + some amount of buffer. This is likely what users want in most instances however does not account for the fact that the same file can be open in multiple windows at the same time. When this occurs and one of the windows is scrolled the other window will start losing decorations. To account for this we now iterate through all windows for the current buffer and collect all visible ranges. These ranges are coalesced based on any overlaps. This is done in a new `range` module. Much of the logic in the `context` module needed to be updated to account for multiple ranges rather than a single top and bottom. This was fairly easy for most things and involved adding a loop over the ranges. It was convenient that all the range checking logic was centralized in the `context` module and did not leak into any of the rendering logic, so it seems like we have a good abstraction. Since we went the more complicated route with the implementation handling multiple ranges the performance impact should be negligible. It should be none with a single window and as little as possible with multiple windows since no matter what we will need to do more work to render more lines.
1 parent a9643f4 commit 767707e

File tree

5 files changed

+131
-34
lines changed

5 files changed

+131
-34
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- heading margin / padding based on level [#182](https://github.com/MeanderingProgrammer/render-markdown.nvim/issues/182)
1919
& border virtual option [#183](https://github.com/MeanderingProgrammer/render-markdown.nvim/issues/183)
2020
[aad1a12](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/aad1a1220dc9da5757e3af3befbc7fc3869dd334)
21+
- config command to debug configurations [a9643f4](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/a9643f4377f39f4abf943fbc73be69f33f5f2f1d)
2122

2223
### Bug Fixes
2324

lua/render-markdown/core/context.lua

+68-32
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
local NodeInfo = require('render-markdown.core.node_info')
2+
local Range = require('render-markdown.core.range')
23
local log = require('render-markdown.core.log')
34
local str = require('render-markdown.core.str')
45
local util = require('render-markdown.core.util')
56

67
---@class render.md.Context
78
---@field private buf integer
89
---@field private win integer
9-
---@field private top integer
10-
---@field private bottom integer
10+
---@field private ranges render.md.Range[]
1111
---@field private components table<integer, render.md.CustomComponent>
1212
---@field private conceal? table<integer, [integer, integer][]>
1313
---@field private links table<integer, [integer, integer, integer][]>
@@ -24,9 +24,15 @@ function Context.new(buf, win, offset)
2424
local self = setmetatable({}, Context)
2525
self.buf = buf
2626
self.win = win
27-
local top = util.view(win).topline - 1
28-
self.top = math.max(top - offset, 0)
29-
self.bottom = self:compute_bottom(top, offset)
27+
28+
local ranges = { Context.compute_range(self.buf, self.win, offset) }
29+
for _, window_id in ipairs(vim.fn.win_findbuf(buf)) do
30+
if window_id ~= self.win then
31+
table.insert(ranges, Context.compute_range(self.buf, window_id, offset))
32+
end
33+
end
34+
self.ranges = Range.coalesce(ranges)
35+
3036
self.components = {}
3137
self.conceal = nil
3238
self.links = {}
@@ -36,20 +42,25 @@ function Context.new(buf, win, offset)
3642
end
3743

3844
---@private
39-
---@param top integer
45+
---@param buf integer
46+
---@param win integer
4047
---@param offset integer
41-
---@return integer
42-
function Context:compute_bottom(top, offset)
48+
---@return render.md.Range
49+
function Context.compute_range(buf, win, offset)
50+
local top = util.view(win).topline - 1
51+
top = math.max(top - offset, 0)
52+
4353
local bottom = top
44-
local lines = vim.api.nvim_buf_line_count(self.buf)
45-
local target = vim.api.nvim_win_get_height(self.win) + offset
46-
while bottom < lines and target > 0 do
54+
local lines = vim.api.nvim_buf_line_count(buf)
55+
local size = vim.api.nvim_win_get_height(win) + offset
56+
while bottom < lines and size > 0 do
4757
bottom = bottom + 1
48-
if util.visible(self.win, bottom) then
49-
target = target - 1
58+
if util.visible(win, bottom) then
59+
size = size - 1
5060
end
5161
end
52-
return bottom
62+
63+
return Range.new(top, bottom)
5364
end
5465

5566
---@param info render.md.NodeInfo
@@ -124,39 +135,62 @@ function Context:get_width()
124135
return self.window_width
125136
end
126137

127-
---@param other render.md.Context
138+
---@param win integer
128139
---@return boolean
129-
function Context:contains_range(other)
130-
return self.top <= other.top and self.bottom >= other.bottom
140+
function Context:contains_window(win)
141+
local window_range = Context.compute_range(self.buf, win, 0)
142+
for _, range in ipairs(self.ranges) do
143+
if range:contains(window_range) then
144+
return true
145+
end
146+
end
147+
return false
131148
end
132149

133-
---@return Range2
134-
function Context:range()
135-
return { self.top, self.bottom }
150+
---@private
151+
---@param top integer
152+
---@param bottom integer
153+
---@return boolean
154+
function Context:overlap(top, bottom)
155+
for _, range in ipairs(self.ranges) do
156+
if range:overlaps(top, bottom) then
157+
return true
158+
end
159+
end
160+
return false
136161
end
137162

138163
---@param node TSNode
139164
---@return boolean
140165
function Context:contains_node(node)
141166
local top, _, bottom, _ = node:range()
142-
return top < self.bottom and bottom >= self.top
167+
return self:overlap(top, bottom)
143168
end
144169

145170
---@param info render.md.NodeInfo
146171
---@return boolean
147172
function Context:contains_info(info)
148-
return info.start_row < self.bottom and info.end_row >= self.top
173+
return self:overlap(info.start_row, info.end_row)
174+
end
175+
176+
---@param parser vim.treesitter.LanguageTree
177+
function Context:parse(parser)
178+
for _, range in ipairs(self.ranges) do
179+
parser:parse({ range.top, range.bottom })
180+
end
149181
end
150182

151183
---@param root TSNode
152184
---@param query vim.treesitter.Query
153185
---@param callback fun(capture: string, node: render.md.NodeInfo)
154186
function Context:query(root, query, callback)
155-
for id, node in query:iter_captures(root, self.buf, self.top, self.bottom) do
156-
local capture = query.captures[id]
157-
local info = NodeInfo.new(self.buf, node)
158-
log.node_info(capture, info)
159-
callback(capture, info)
187+
for _, range in ipairs(self.ranges) do
188+
for id, node in query:iter_captures(root, self.buf, range.top, range.bottom) do
189+
local capture = query.captures[id]
190+
local info = NodeInfo.new(self.buf, node)
191+
log.node_info(capture, info)
192+
callback(capture, info)
193+
end
160194
end
161195
end
162196

@@ -207,7 +241,7 @@ function Context:compute_conceal()
207241
end
208242
local ranges = {}
209243
local parser = vim.treesitter.get_parser(self.buf)
210-
parser:parse(self:range())
244+
self:parse(parser)
211245
parser:for_each_tree(function(tree, language_tree)
212246
local nodes = self:compute_conceal_nodes(language_tree:lang(), tree:root())
213247
for _, node in ipairs(nodes) do
@@ -237,9 +271,11 @@ function Context:compute_conceal_nodes(language, root)
237271
return {}
238272
end
239273
local nodes = {}
240-
for _, node, metadata in query:iter_captures(root, self.buf, self.top, self.bottom) do
241-
if metadata.conceal ~= nil then
242-
table.insert(nodes, node)
274+
for _, range in ipairs(self.ranges) do
275+
for _, node, metadata in query:iter_captures(root, self.buf, range.top, range.bottom) do
276+
if metadata.conceal ~= nil then
277+
table.insert(nodes, node)
278+
end
243279
end
244280
end
245281
return nodes
@@ -265,7 +301,7 @@ function M.contains_range(buf, win)
265301
if context == nil then
266302
return false
267303
end
268-
return context:contains_range(Context.new(buf, win, 0))
304+
return context:contains_window(win)
269305
end
270306

271307
---@param buf integer

lua/render-markdown/core/range.lua

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---@class render.md.Range
2+
---@field top integer
3+
---@field bottom integer
4+
local Range = {}
5+
Range.__index = Range
6+
7+
---@param top integer
8+
---@param bottom integer
9+
---@return render.md.Range
10+
function Range.new(top, bottom)
11+
local self = setmetatable({}, Range)
12+
self.top = top
13+
self.bottom = bottom
14+
return self
15+
end
16+
17+
---@param a render.md.Range
18+
---@param b render.md.Range
19+
---@return boolean
20+
function Range.__lt(a, b)
21+
if a.top ~= b.top then
22+
return a.top < b.top
23+
else
24+
return a.bottom < b.bottom
25+
end
26+
end
27+
28+
---@param other render.md.Range
29+
---@return boolean
30+
function Range:contains(other)
31+
return self.top <= other.top and self.bottom >= other.bottom
32+
end
33+
34+
---@param top integer
35+
---@param bottom integer
36+
---@return boolean
37+
function Range:overlaps(top, bottom)
38+
return top < self.bottom and bottom >= self.top
39+
end
40+
41+
---@param ranges render.md.Range[]
42+
---@return render.md.Range[]
43+
function Range.coalesce(ranges)
44+
if #ranges < 2 then
45+
return ranges
46+
end
47+
table.sort(ranges)
48+
local result = { ranges[1] }
49+
for i = 2, #ranges do
50+
local range, current = ranges[i], result[#result]
51+
if range.top <= current.bottom + 1 then
52+
current.bottom = math.max(current.bottom, range.bottom)
53+
else
54+
table.insert(result, range)
55+
end
56+
end
57+
return result
58+
end
59+
60+
return Range

lua/render-markdown/core/ui.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ function M.parse_buffer(buf, win)
160160
-- Reset buffer context
161161
Context.reset(buf, win)
162162
-- Make sure injections are processed
163-
parser:parse(Context.get(buf):range())
163+
Context.get(buf):parse(parser)
164164
-- Parse markdown after all other nodes to take advantage of state
165165
local marks, markdown_roots = {}, {}
166166
parser:for_each_tree(function(tree, language_tree)

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.1.12'
7+
M.version = '7.1.13'
88

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

0 commit comments

Comments
 (0)