Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/d2.lua

Raw
local 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