Skip to content

Commit 72f658d

Browse files
feat: implement preview split
## Details Add a new API `:RenderMarkdown preview` which shows a rendered buffer to the side of the current buffer rather than in it. This allows editing to happen in a buffer with no decorations and to have a rendered view to the side, like many IDEs support. When opened decorations will be removed from the "source" buffer until the "preview" buffer is closed, at which point decorations will be shown as usual. Only a single preview will be open at one time for a given buffer. Calling `:RenderMarkdown preview` a second time will cause the preview buffer to be closed, so it functions like a toggle. Configurations for the "preview" buffer are generated from the "source" buffer, so should all render the same. Custom values can be set for "preview" buffers only using `overrides.preview`, by default this is used to change `render_modes` to `true`, so rendering will happen while the buffer is being edited. Under the hood this is implemented by creating a new buffer and copying text over to it on "TextChanged" events. To avoid overly jumpy behavior we take advantage of `vim.diff` to modify as few lines between changes as possible. Overall performance seems to be pretty good, with no major catches that I noticed. This feature will likely need to be improved as it is tested under different circumstances, but as is I think it's in a good enough state to release, enjoy :)
1 parent ce7af44 commit 72f658d

File tree

9 files changed

+167
-4
lines changed

9 files changed

+167
-4
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ require('render-markdown').setup({}) -- only mandatory if you want to set custom
132132
| `:RenderMarkdown get` | `require('render-markdown').get()` | Return current state |
133133
| `:RenderMarkdown set bool?` | `require('render-markdown').set(bool?)` | Sets state, `nil` to toggle |
134134
| `:RenderMarkdown set_buf bool?` | `require('render-markdown').set_buf(bool?)` | Sets state for current buffer, `nil` to toggle |
135+
| `:RenderMarkdown preview` | `require('render-markdown').preview()` | Show rendered buffer to the side |
135136
| `:RenderMarkdown log` | `require('render-markdown').log()` | Opens the log file for this plugin |
136137
| `:RenderMarkdown expand` | `require('render-markdown').expand()` | Increase anti-conceal margin above and below by 1 |
137138
| `:RenderMarkdown contract` | `require('render-markdown').contract()` | Decrease anti-conceal margin above and below by 1 |
@@ -922,6 +923,10 @@ require('render-markdown').setup({
922923
},
923924
-- Override for different filetype values, @see :h 'filetype'.
924925
filetype = {},
926+
-- Override for preview buffer
927+
preview = {
928+
render_modes = true,
929+
},
925930
},
926931
custom_handlers = {
927932
-- Mapping from treesitter language to user defined handlers.

doc/render-markdown.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ VIM.PACK *render-markdown-install-vim.pack*
180180
:RenderMarkdown set_buf bool? require('render-markdown').set_buf(bool?) Sets state for current
181181
buffer, nil to toggle
182182

183+
:RenderMarkdown preview require('render-markdown').preview() Show rendered buffer to the
184+
side
185+
183186
:RenderMarkdown log require('render-markdown').log() Opens the log file for this
184187
plugin
185188

@@ -986,6 +989,10 @@ Default Configuration ~
986989
},
987990
-- Override for different filetype values, @see :h 'filetype'.
988991
filetype = {},
992+
-- Override for preview buffer
993+
preview = {
994+
render_modes = true,
995+
},
989996
},
990997
custom_handlers = {
991998
-- Mapping from treesitter language to user defined handlers.

lua/render-markdown/api.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ function M.buf_toggle()
4040
M.set_buf()
4141
end
4242

43+
function M.preview()
44+
require('render-markdown.core.preview').attach()
45+
end
46+
4347
function M.log()
4448
require('render-markdown.core.log').open()
4549
end
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
local env = require('render-markdown.lib.env')
2+
local manager = require('render-markdown.core.manager')
3+
4+
---@class render.md.Preview
5+
local M = {}
6+
7+
---@private
8+
M.group = vim.api.nvim_create_augroup('RenderMarkdownPreview', {})
9+
10+
---@private
11+
---@type table<integer, integer>
12+
M.buffers = {}
13+
14+
---@param buf integer
15+
---@return integer?
16+
function M.get(buf)
17+
for src, dst in pairs(M.buffers) do
18+
if buf == dst then
19+
return src
20+
end
21+
end
22+
return nil
23+
end
24+
25+
---@param src_buf? integer
26+
function M.attach(src_buf)
27+
src_buf = src_buf or env.buf.current()
28+
if not manager.attached(src_buf) then
29+
return
30+
end
31+
if M.buffers[src_buf] then
32+
vim.api.nvim_buf_delete(M.buffers[src_buf], {})
33+
return
34+
end
35+
36+
-- disable rendering for source buffer
37+
manager.set_buf(src_buf, false)
38+
39+
local src_win = env.buf.win(src_buf)
40+
local dst_buf = vim.api.nvim_create_buf(false, true)
41+
local dst_win = vim.api.nvim_open_win(dst_buf, false, {
42+
split = 'right',
43+
})
44+
M.buffers[src_buf] = dst_buf
45+
46+
env.buf.set(dst_buf, 'bufhidden', 'wipe')
47+
env.buf.set(dst_buf, 'buftype', 'nofile')
48+
env.buf.set(dst_buf, 'filetype', env.buf.get(src_buf, 'filetype'))
49+
env.buf.set(dst_buf, 'modifiable', false)
50+
env.buf.set(dst_buf, 'swapfile', false)
51+
52+
M.copy_cursor(src_win, dst_win)
53+
vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
54+
group = M.group,
55+
buffer = src_buf,
56+
callback = function(args)
57+
if env.valid(src_buf, src_win) and env.valid(dst_buf, dst_win) then
58+
M.copy_cursor(src_win, dst_win)
59+
M.copy_event(args, dst_buf)
60+
end
61+
end,
62+
})
63+
64+
M.copy_lines(src_buf, dst_buf)
65+
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
66+
group = M.group,
67+
buffer = src_buf,
68+
callback = function(args)
69+
if env.valid(src_buf, src_win) and env.valid(dst_buf, dst_win) then
70+
-- also need to copy cursor due to event ordering
71+
M.copy_lines(src_buf, dst_buf)
72+
M.copy_cursor(src_win, dst_win)
73+
M.copy_event(args, dst_buf)
74+
end
75+
end,
76+
})
77+
78+
vim.api.nvim_create_autocmd('BufWipeout', {
79+
group = M.group,
80+
buffer = dst_buf,
81+
once = true,
82+
callback = function()
83+
M.buffers[src_buf] = nil
84+
vim.api.nvim_clear_autocmds({ group = M.group, buffer = src_buf })
85+
-- enable rendering for source buffer
86+
manager.set_buf(src_buf, true)
87+
end,
88+
})
89+
end
90+
91+
---@private
92+
---@param src integer
93+
---@param dst integer
94+
function M.copy_lines(src, dst)
95+
local src_lines = vim.api.nvim_buf_get_lines(src, 0, -1, false)
96+
local dst_lines = vim.api.nvim_buf_get_lines(dst, 0, -1, false)
97+
98+
local src_text = table.concat(src_lines, '\n') .. '\n'
99+
local dst_text = table.concat(dst_lines, '\n') .. '\n'
100+
local diff = vim.diff(dst_text, src_text, { result_type = 'indices' })
101+
assert(type(diff) == 'table', 'diff must provide indices')
102+
103+
env.buf.set(dst, 'modifiable', true)
104+
for i = 1, #diff do
105+
local hunk = diff[#diff - i + 1]
106+
local start_a, count_a, start_b, count_b = unpack(hunk)
107+
local line_start = start_a - 1
108+
local line_end = start_a + count_a - 1
109+
if count_a == 0 then
110+
line_start = line_start + 1
111+
line_end = line_end + 1
112+
end
113+
vim.api.nvim_buf_set_lines(dst, line_start, line_end, false, {
114+
unpack(src_lines, start_b, start_b + count_b - 1),
115+
})
116+
end
117+
env.buf.set(dst, 'modifiable', false)
118+
end
119+
120+
---@private
121+
---@param src integer
122+
---@param dst integer
123+
function M.copy_cursor(src, dst)
124+
local cursor = vim.api.nvim_win_get_cursor(src)
125+
pcall(vim.api.nvim_win_set_cursor, dst, cursor)
126+
end
127+
128+
---@private
129+
---@param args vim.api.keyset.create_autocmd.callback_args
130+
---@param buf integer
131+
function M.copy_event(args, buf)
132+
vim.api.nvim_exec_autocmds(args.event, { buffer = buf })
133+
end
134+
135+
return M

lua/render-markdown/health.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ local state = require('render-markdown.state')
66
local M = {}
77

88
---@private
9-
M.version = '8.9.7'
9+
M.version = '8.9.8'
1010

1111
function M.check()
1212
M.start('versions')

lua/render-markdown/lib/config.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ Config.__index = Config
1111
---@param root render.md.Config
1212
---@param enabled boolean
1313
---@param buf integer
14+
---@param src? integer
1415
---@return render.md.buf.Config
15-
function Config.new(root, enabled, buf)
16+
function Config.new(root, enabled, buf, src)
1617
---@type render.md.partial.Config
1718
local config = {
1819
enabled = enabled,
@@ -42,12 +43,15 @@ function Config.new(root, enabled, buf)
4243
}
4344
config = vim.deepcopy(config)
4445
for _, name in ipairs({ 'buflisted', 'buftype', 'filetype' }) do
45-
local value = env.buf.get(buf, name)
46+
local value = env.buf.get(src or buf, name)
4647
local override = root.overrides[name][value] ---@type render.md.partial.UserConfig?
4748
if override then
4849
config = vim.tbl_deep_extend('force', config, override)
4950
end
5051
end
52+
if src then
53+
config = vim.tbl_deep_extend('force', config, root.overrides.preview)
54+
end
5155
local self = setmetatable(config, Config)
5256
self.resolved = Resolved.new(config)
5357
---@cast self -render.md.partial.Config

lua/render-markdown/settings.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,7 @@ M.overrides = {}
13601360
---@field buflisted table<boolean, render.md.partial.UserConfig>
13611361
---@field buftype table<string, render.md.partial.UserConfig>
13621362
---@field filetype table<string, render.md.partial.UserConfig>
1363+
---@field preview render.md.partial.UserConfig
13631364

13641365
---@type render.md.overrides.Config
13651366
M.overrides.default = {
@@ -1382,6 +1383,10 @@ M.overrides.default = {
13821383
},
13831384
-- Override for different filetype values, @see :h 'filetype'.
13841385
filetype = {},
1386+
-- Override for preview buffer
1387+
preview = {
1388+
render_modes = true,
1389+
},
13851390
}
13861391

13871392
---@return render.md.Schema
@@ -1393,6 +1398,7 @@ function M.overrides.schema()
13931398
buflisted = { map = { { type = 'boolean' }, Config.schema({}) } },
13941399
buftype = { map = { { type = 'string' }, Config.schema({}) } },
13951400
filetype = { map = { { type = 'string' }, Config.schema({}) } },
1401+
preview = Config.schema({}),
13961402
},
13971403
}
13981404
end

lua/render-markdown/state.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ end
5151
function M.get(buf)
5252
local result = M.cache[buf]
5353
if not result then
54-
result = Config.new(M.config, M.enabled, buf)
54+
local src = require('render-markdown.core.preview').get(buf)
55+
result = Config.new(M.config, M.enabled, buf, src)
5556
M.cache[buf] = result
5657
end
5758
return result

lua/render-markdown/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
---@field buflisted? table<boolean, render.md.partial.UserConfig>
242242
---@field buftype? table<string, render.md.partial.UserConfig>
243243
---@field filetype? table<string, render.md.partial.UserConfig>
244+
---@field preview? render.md.partial.UserConfig
244245

245246
---@class (exact) render.md.padding.UserConfig
246247
---@field highlight? string

0 commit comments

Comments
 (0)