repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/d2.lua
Rawlocal auto = require 'std.auto'
local map = require 'std.map'
local table = require 'std.table'
local user_command = require 'std.user_command'
local NS = 'd2'
local PREVIEW_BUF_NAME = 'D2_ASCII_PREVIEW'
local PREVIEW_AUGRP = 'd2_preview_autosave'
local preview_winnr = nil
local d2_binary = nil
local d2_checked = false
local function check_d2_binary()
if d2_checked then return d2_binary end
d2_checked = true
for _, name in ipairs({ 'd2', 'd2.exe' }) do
local result = vim.system({ name, '--version' }, { text = true }):wait()
if result.code == 0 then
d2_binary = name
return d2_binary
end
end
vim.notify(
'[bugabinga/d2] `d2` not found. Install from https://github.com/terrastruct/d2',
vim.log.levels.WARN
)
return nil
end
local preview_bufnr = function()
return vim.fn.bufnr(PREVIEW_BUF_NAME)
end
local preview_window_exists = function()
return preview_winnr ~= nil and vim.api.nvim_win_is_valid(preview_winnr)
end
local close_preview_window = function()
if preview_winnr and vim.api.nvim_win_is_valid(preview_winnr) then
vim.api.nvim_win_close(preview_winnr, true)
preview_winnr = nil
end
end
local render = function(bufnr)
if not check_d2_binary() then return nil end
local input_file = vim.fn.tempname() .. '.d2'
local output_file = vim.fn.tempname() .. '.txt'
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
vim.fn.writefile(lines, input_file)
local result = vim.system({ d2_binary, input_file, output_file }, { text = true }):wait()
if result.code ~= 0 then
vim.notify('d2 render failed: ' .. (result.stderr or ''), vim.log.levels.ERROR)
return nil
end
local output_lines = vim.fn.readfile(output_file)
if #output_lines > 0 and output_lines[1] == '' then
table.remove(output_lines, 1)
end
vim.fn.delete(input_file)
vim.fn.delete(output_file)
return output_lines
end
local show_preview = function(output_lines)
close_preview_window()
local is_wide = vim.o.columns > vim.o.lines
local pbufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(pbufnr, PREVIEW_BUF_NAME)
vim.api.nvim_buf_set_lines(pbufnr, 0, -1, false, output_lines)
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = pbufnr })
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = pbufnr })
vim.api.nvim_set_option_value('swapfile', false, { buf = pbufnr })
vim.api.nvim_set_option_value('modifiable', false, { buf = pbufnr })
vim.api.nvim_set_option_value('readonly', true, { buf = pbufnr })
local is_wide = vim.o.columns > 120
if is_wide then
vim.cmd(('vertical botright %dsplit %s'):format(math.floor(vim.o.columns * 0.4), PREVIEW_BUF_NAME))
else
vim.cmd(('belowright split %s'):format(PREVIEW_BUF_NAME))
end
local winnr = vim.fn.bufwinid(pbufnr)
preview_winnr = winnr
vim.api.nvim_set_option_value('number', false, { win = winnr })
vim.api.nvim_set_option_value('wrap', false, { win = winnr })
vim.api.nvim_set_option_value('spell', false, { win = winnr })
end
local preview_toggle = function()
if preview_window_exists() then
close_preview_window()
return
end
local bufnr = vim.api.nvim_get_current_buf()
local output = render(bufnr)
if output then
show_preview(output)
end
end
local preview_update = function()
if not preview_window_exists() then return end
local cur_buf = vim.api.nvim_get_current_buf()
local output = render(cur_buf)
if output then
local preview_buf = vim.fn.bufnr(PREVIEW_BUF_NAME)
if preview_buf ~= -1 then
vim.api.nvim_set_option_value('modifiable', true, { buf = preview_buf })
vim.api.nvim_buf_set_lines(preview_buf, 0, -1, false, output)
vim.api.nvim_set_option_value('modifiable', false, { buf = preview_buf })
end
end
end
local preview_copy = function()
local pbnr = preview_bufnr()
if pbnr == -1 then
vim.notify('No D2 preview open', vim.log.levels.WARN)
return
end
local lines = vim.api.nvim_buf_get_lines(pbnr, 0, -1, false)
local text = table.concat(lines, '\n')
vim.fn.setreg('+', text)
vim.fn.setreg('*', text)
vim.fn.setreg('"', text)
vim.notify('D2 preview copied to clipboard')
end
local diag_ns = vim.api.nvim_create_namespace(NS)
local parse_validate_output = function(stderr)
local diagnostics = {}
for line in stderr:gmatch '[^\r\n]+' do
if line:match '^err:' then
local suffix = line:gsub('^err:%s*', '')
local lnum, col, msg = suffix:match '.*:%s*(%d+):(%d+):%s*(.*)'
if lnum and col then
table.insert(diagnostics, {
lnum = tonumber(lnum) - 1,
col = tonumber(col) - 1,
end_col = tonumber(col),
message = msg,
severity = vim.diagnostic.severity.ERROR,
source = 'd2',
})
else
table.insert(diagnostics, {
lnum = 0,
col = 0,
end_col = 0,
message = suffix,
severity = vim.diagnostic.severity.ERROR,
source = 'd2',
})
end
end
end
return diagnostics
end
local validate = function(args)
if not check_d2_binary() then return end
local bufnr = (args and args.buf) or vim.api.nvim_get_current_buf()
local file = vim.api.nvim_buf_get_name(bufnr)
if file == '' then
return
end
local result = vim.system({ d2_binary, 'validate', file }, { text = true }):wait()
vim.diagnostic.reset(nil, bufnr)
if result.code ~= 0 then
local diags = parse_validate_output(result.stderr)
vim.diagnostic.set(diag_ns, bufnr, diags, {})
vim.diagnostic.show(diag_ns, bufnr)
end
end
local validate_toggle = function()
local autocmds_ok, autocmds = pcall(vim.api.nvim_get_autocmds, {
group = 'd2_validate_autosave',
event = 'BufWritePost',
pattern = '*.d2',
})
if autocmds_ok and #autocmds > 0 then
pcall(vim.api.nvim_del_augroup_by_name, 'd2_validate_autosave')
vim.notify('D2 auto-validate disabled', vim.log.levels.INFO)
else
auto 'd2_validate_autosave' {
events = 'BufWritePost',
pattern = '*.d2',
description = 'Auto-validate d2 files on save',
command = validate,
}
vim.notify('D2 auto-validate enabled', vim.log.levels.INFO)
end
end
map.normal {
description = 'Toggle D2 ASCII preview',
category = 'd2',
keys = '<leader>dp',
command = preview_toggle,
buffer = 0,
}
map.normal {
description = 'Update D2 ASCII preview',
category = 'd2',
keys = '<leader>du',
command = preview_update,
buffer = 0,
}
map.normal {
description = 'Copy D2 preview to clipboard',
category = 'd2',
keys = '<leader>dy',
command = preview_copy,
buffer = 0,
}
map.normal {
description = 'Validate D2 file',
category = 'd2',
keys = '<leader>dv',
command = validate,
buffer = 0,
}
user_command.D2Preview 'Open D2 ASCII preview' (preview_toggle)
user_command.D2PreviewToggle 'Toggle D2 ASCII preview' (preview_toggle)
user_command.D2Validate 'Validate D2 file' (validate)
user_command.D2ValidateToggle 'Toggle D2 auto-validate' (validate_toggle)
auto 'd2_preview_autosave' {
events = 'BufWritePost',
pattern = '*.d2',
description = 'Auto-update D2 ASCII preview on save',
command = preview_update,
}
auto 'd2_validate_autosave' {
events = { 'BufWritePost', 'BufReadPost', 'BufEnter' },
pattern = '*.d2',
description = 'Auto-validate d2 files',
command = validate,
}
auto 'd2_preview_keymaps' {
events = 'BufEnter',
pattern = PREVIEW_BUF_NAME,
description = 'D2 preview buffer keymaps',
command = function(args)
map.normal { keys = '<leader>dp', command = preview_toggle, buffer = args.buf, description = 'Toggle D2 preview' }
map.normal { keys = '<leader>du', command = preview_update, buffer = args.buf, description = 'Update D2 preview' }
map.normal { keys = 'q', command = close_preview_window, buffer = args.buf, description = 'Close D2 preview' }
end,
}
_G.BugabingaD2 = {
preview_toggle = preview_toggle,
preview_update = preview_update,
preview_copy = preview_copy,
validate = validate,
validate_toggle = validate_toggle,
_parse_validate_output = parse_validate_output,
}
return BugabingaD2