266 lines
8.5 KiB
Lua
266 lines
8.5 KiB
Lua
local backdrop = require("scratch_comments.ui.backdrop")
|
|
local location = require("scratch_comments.location")
|
|
local notify = require("scratch_comments.ui.notify")
|
|
|
|
local M = {}
|
|
|
|
---@param lines string[]
|
|
---@param filetype string
|
|
---@return integer bufnr
|
|
local function frame_buffer(lines, filetype)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
vim.bo[bufnr].modified = false
|
|
vim.bo[bufnr].bufhidden = "wipe"
|
|
vim.bo[bufnr].filetype = filetype
|
|
return bufnr
|
|
end
|
|
|
|
---@class ScratchFrame
|
|
---@field title string
|
|
---@field context string[]
|
|
---@field filetype string
|
|
---@field comment string
|
|
---@field comment_name? string name shown for the editable comment buffer
|
|
---@field on_save? fun(text: string): boolean|string? false keeps the text unsaved; "deleted" closes the editor
|
|
---@field comment_title? string
|
|
---@field on_close? fun()
|
|
|
|
---@class ScratchFramePanes
|
|
---@field context_win integer
|
|
---@field context_buf integer
|
|
---@field comment_win integer
|
|
---@field comment_buf integer
|
|
---@field backdrop_win? integer
|
|
|
|
-- Two stacked floats, centered: the commented lines above, the comment below.
|
|
---@param frame ScratchFrame
|
|
---@param parent integer editing window to cover, not the quickfix list
|
|
---@return ScratchFramePanes
|
|
function M.open(frame, parent)
|
|
---@return { width: integer, col: integer, row: integer, context: integer, comment: integer, compact: boolean }
|
|
local function layout()
|
|
local lines = vim.api.nvim_win_get_height(parent)
|
|
local columns = vim.api.nvim_win_get_width(parent)
|
|
local compact = lines < 6
|
|
local width = math.max(1, math.min(80, columns - (compact and 0 or 4)))
|
|
if compact then
|
|
-- Two bordered panes cannot fit. The comment covers the context pane,
|
|
-- which stays open so edits and normal resizing keep the same buffers.
|
|
return {
|
|
width = width,
|
|
col = math.floor((columns - width) / 2),
|
|
row = 0,
|
|
context = 1,
|
|
comment = lines,
|
|
compact = true,
|
|
}
|
|
end
|
|
local available = lines - 4
|
|
local context = math.max(1, math.min(#frame.context, math.floor(lines * 0.4), available - 1))
|
|
local comment = math.max(
|
|
1,
|
|
math.min(
|
|
math.max(#vim.split(frame.comment, "\n"), 5),
|
|
math.floor(lines * 0.3),
|
|
available - context
|
|
)
|
|
)
|
|
return {
|
|
width = width,
|
|
col = math.floor((columns - width) / 2),
|
|
-- Each pane has a top and a bottom border line.
|
|
row = math.floor((lines - context - comment - 4) / 2),
|
|
context = context,
|
|
comment = comment,
|
|
compact = false,
|
|
}
|
|
end
|
|
|
|
local border = (vim.o.winborder == "" or vim.o.winborder == "none") and "rounded"
|
|
or vim.o.winborder
|
|
---@param place table
|
|
---@return vim.api.keyset.win_config context
|
|
---@return vim.api.keyset.win_config comment
|
|
local function placement(place)
|
|
local narrow = place.width < 4
|
|
return {
|
|
relative = "win",
|
|
win = parent,
|
|
row = place.row,
|
|
col = place.col,
|
|
width = place.width,
|
|
height = place.compact and place.comment or place.context,
|
|
title = (place.compact or narrow) and ""
|
|
or " " .. location.fit(frame.title, place.width - 2) .. " ",
|
|
title_pos = "center",
|
|
border = place.compact and "none" or border,
|
|
zindex = 45,
|
|
}, {
|
|
relative = "win",
|
|
win = parent,
|
|
row = place.compact and place.row or place.row + place.context + 2,
|
|
col = place.col,
|
|
width = place.width,
|
|
height = place.comment,
|
|
border = place.compact and "none" or border,
|
|
zindex = 50,
|
|
}
|
|
end
|
|
|
|
local dim = backdrop.open(parent)
|
|
local context_place, comment_place = placement(layout())
|
|
local context_buf = frame_buffer(frame.context, frame.filetype)
|
|
vim.bo[context_buf].modifiable = false
|
|
local context_win = vim.api.nvim_open_win(
|
|
context_buf,
|
|
false,
|
|
vim.tbl_extend("error", context_place, { style = "minimal" })
|
|
)
|
|
|
|
local comment_buf = frame_buffer(vim.split(frame.comment, "\n"), "markdown")
|
|
if frame.comment_name then
|
|
vim.api.nvim_buf_set_name(
|
|
comment_buf,
|
|
"scratch-comments://" .. comment_buf .. "/" .. frame.comment_name
|
|
)
|
|
end
|
|
local comment_win = vim.api.nvim_open_win(
|
|
comment_buf,
|
|
true,
|
|
vim.tbl_extend(
|
|
"error",
|
|
comment_place,
|
|
{ style = "minimal", title = " comment ", title_pos = "center" }
|
|
)
|
|
)
|
|
vim.wo[comment_win].wrap = true
|
|
vim.wo[comment_win].linebreak = true
|
|
for _, win in ipairs({ context_win, comment_win }) do
|
|
-- The padding column draws in one of these, depending on the window's
|
|
-- options, so all of them follow the window's background.
|
|
vim.wo[win].winhighlight = table.concat({
|
|
"NormalFloat:Normal",
|
|
"LineNr:Normal",
|
|
"LineNrAbove:Normal",
|
|
"LineNrBelow:Normal",
|
|
"CursorLineNr:Normal",
|
|
"SignColumn:Normal",
|
|
"FoldColumn:Normal",
|
|
}, ",")
|
|
vim.wo[win].statuscolumn = " "
|
|
end
|
|
|
|
local group = vim.api.nvim_create_augroup("scratch_comments_frame_" .. comment_win, {})
|
|
local tab = vim.api.nvim_win_get_tabpage(parent)
|
|
local function watch_parent()
|
|
vim.api.nvim_create_autocmd("WinClosed", {
|
|
group = group,
|
|
pattern = tostring(parent),
|
|
once = true,
|
|
callback = function()
|
|
vim.schedule(function()
|
|
if
|
|
not (
|
|
vim.api.nvim_tabpage_is_valid(tab)
|
|
and vim.api.nvim_win_is_valid(comment_win)
|
|
and vim.api.nvim_win_is_valid(context_win)
|
|
)
|
|
then
|
|
return
|
|
end
|
|
local file_win, other_win, qf_win
|
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tab)) do
|
|
if vim.api.nvim_win_get_config(win).relative == "" then
|
|
local kind = vim.bo[vim.api.nvim_win_get_buf(win)].buftype
|
|
if kind == "" then
|
|
file_win = file_win or win
|
|
elseif kind == "quickfix" then
|
|
qf_win = win
|
|
else
|
|
other_win = other_win or win
|
|
end
|
|
end
|
|
end
|
|
local replacement = file_win or other_win
|
|
if not replacement and qf_win then
|
|
if not vim.bo[comment_buf].modified then
|
|
vim.api.nvim_win_close(comment_win, true)
|
|
return
|
|
end
|
|
-- Preserve unsaved text even if the editor cannot make a split.
|
|
local ok, created = pcall(vim.api.nvim_win_call, qf_win, function()
|
|
vim.cmd("aboveleft new")
|
|
return vim.api.nvim_get_current_win()
|
|
end)
|
|
if not ok then
|
|
notify.warn("No room for an editing window; save or discard the open card")
|
|
end
|
|
replacement = ok and created or qf_win
|
|
end
|
|
if replacement then
|
|
parent = replacement
|
|
backdrop.resize(dim, parent)
|
|
local context_next, comment_next = placement(layout())
|
|
vim.api.nvim_win_set_config(context_win, context_next)
|
|
vim.api.nvim_win_set_config(comment_win, comment_next)
|
|
watch_parent()
|
|
end
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
watch_parent()
|
|
vim.api.nvim_create_autocmd({ "VimResized", "WinResized" }, {
|
|
group = group,
|
|
callback = function()
|
|
if
|
|
not (vim.api.nvim_win_is_valid(context_win) and vim.api.nvim_win_is_valid(comment_win))
|
|
then
|
|
backdrop.close(dim)
|
|
pcall(vim.api.nvim_del_augroup_by_id, group)
|
|
return
|
|
end
|
|
if not vim.api.nvim_win_is_valid(parent) then
|
|
return
|
|
end
|
|
backdrop.resize(dim, parent)
|
|
local context_resized, comment_resized = placement(layout())
|
|
vim.api.nvim_win_set_config(context_win, context_resized)
|
|
vim.api.nvim_win_set_config(comment_win, comment_resized)
|
|
end,
|
|
})
|
|
|
|
vim.api.nvim_create_autocmd("WinClosed", {
|
|
pattern = tostring(comment_win),
|
|
once = true,
|
|
callback = function()
|
|
pcall(vim.api.nvim_win_close, context_win, true)
|
|
backdrop.close(dim)
|
|
pcall(vim.api.nvim_del_augroup_by_id, group)
|
|
if frame.on_close then
|
|
frame.on_close()
|
|
end
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd("WinClosed", {
|
|
pattern = tostring(context_win),
|
|
once = true,
|
|
callback = function()
|
|
-- Not forced, so an unsaved comment stays open instead of being lost.
|
|
pcall(vim.api.nvim_win_close, comment_win, false)
|
|
end,
|
|
})
|
|
|
|
local panes = {
|
|
context_win = context_win,
|
|
context_buf = context_buf,
|
|
comment_win = comment_win,
|
|
comment_buf = comment_buf,
|
|
backdrop_win = dim,
|
|
}
|
|
return panes
|
|
end
|
|
|
|
return M
|