snacks.nvim/lua/snacks/zen.lua
Andrei Heidelbacher 041bf1da9e
fix(zen): always count cmdheight towards Zen bottom offset (#1402)
## Description

Zen window height should always account for `cmdheight` (not only if
`statusline` is enabled) and be computed dynamically when `cmdheight >
0` to avoid the `cmdline` obstructing the Zen window.

## Related Issue(s)

- Fixes https://github.com/folke/snacks.nvim/issues/1401
- Fixes https://github.com/folke/snacks.nvim/issues/1375

## Screenshots

Before:
![zen-mode-cmdline](https://github.com/user-attachments/assets/c03be4af-2368-4b04-8065-49ced875aa2d)

After:
![zen-mode-fixed-cmdline](https://github.com/user-attachments/assets/14dc72bb-7431-4d05-b0fd-b6a2367bdc6d)
2025-02-24 07:19:28 +01:00

226 lines
5.9 KiB
Lua

---@class snacks.zen
---@overload fun(opts: snacks.zen.Config): snacks.win
local M = setmetatable({}, {
__call = function(M, ...)
return M.zen(...)
end,
})
M.meta = {
desc = "Zen mode • distraction-free coding",
}
---@class snacks.zen.Config
local defaults = {
-- You can add any `Snacks.toggle` id here.
-- Toggle state is restored when the window is closed.
-- Toggle config options are NOT merged.
---@type table<string, boolean>
toggles = {
dim = true,
git_signs = false,
mini_diff_signs = false,
-- diagnostics = false,
-- inlay_hints = false,
},
show = {
statusline = false, -- can only be shown when using the global statusline
tabline = false,
},
---@type snacks.win.Config
win = { style = "zen" },
--- Callback when the window is opened.
---@param win snacks.win
on_open = function(win) end,
--- Callback when the window is closed.
---@param win snacks.win
on_close = function(win) end,
--- Options for the `Snacks.zen.zoom()`
---@type snacks.zen.Config
zoom = {
toggles = {},
show = { statusline = true, tabline = true },
win = {
backdrop = false,
width = 0, -- full width
},
},
}
Snacks.config.style("zen", {
enter = true,
fixbuf = false,
minimal = false,
width = 120,
height = 0,
backdrop = { transparent = true, blend = 40 },
keys = { q = false },
zindex = 40,
wo = {
winhighlight = "NormalFloat:Normal",
},
w = {
snacks_main = true,
},
})
-- fullscreen indicator
-- only shown when the window is maximized
Snacks.config.style("zoom_indicator", {
text = "▍ zoom 󰊓 ",
minimal = true,
enter = false,
focusable = false,
height = 1,
row = 0,
col = -1,
backdrop = false,
})
Snacks.util.set_hl({
Icon = "DiagnosticWarn",
}, { prefix = "SnacksZen", default = true })
---@param opts? {statusline: boolean, tabline: boolean}
local function get_main(opts)
opts = opts or {}
local bottom = vim.o.cmdheight + (opts.statusline and vim.o.laststatus == 3 and 1 or 0)
local top = opts.tabline
and ((vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)) and 1 or 0)
or 0
---@class snacks.zen.Main values are 0-indexed
local ret = {
width = vim.o.columns,
row = top,
height = vim.o.lines - top - bottom,
}
return ret
end
M.win = nil ---@type snacks.win?
---@param opts? snacks.zen.Config
function M.zen(opts)
local toggles = opts and opts.toggles
opts = Snacks.config.get("zen", defaults, opts)
opts.toggles = toggles or opts.toggles
-- close if already open
if M.win and M.win:valid() then
M.win:close()
M.win = nil
return
end
local parent_win = vim.api.nvim_get_current_win()
local parent_zindex = vim.api.nvim_win_get_config(parent_win).zindex
local buf = vim.api.nvim_get_current_buf()
local win_opts = Snacks.win.resolve({ style = "zen" }, opts.win, { buf = buf })
win_opts.zindex = parent_zindex and parent_zindex + 1 or win_opts.zindex
if Snacks.util.is_transparent() and type(win_opts.backdrop) == "table" then
win_opts.backdrop.transparent = false
end
local zoom_indicator ---@type snacks.win?
local show_indicator = false
-- calculate window size
if win_opts.height == 0 and (opts.show.statusline or opts.show.tabline or vim.o.cmdheight > 0) then
local main = get_main(opts.show)
win_opts.row = main.row
win_opts.height = function()
return get_main(opts.show).height
end
if type(win_opts.backdrop) == "table" then
win_opts.backdrop.win = win_opts.backdrop.win or {}
win_opts.backdrop.win.row = win_opts.row
win_opts.backdrop.win.height = win_opts.height
end
if win_opts.width == 0 then
show_indicator = true
end
end
-- create window
local win = Snacks.win(win_opts)
vim.cmd([[norm! zz]])
M.win = win
if show_indicator then
zoom_indicator = Snacks.win({
show = false,
style = "zoom_indicator",
zindex = win.opts.zindex + 1,
wo = { winhighlight = "NormalFloat:SnacksZenIcon" },
})
zoom_indicator:open_buf()
local lines = vim.api.nvim_buf_get_lines(zoom_indicator.buf, 0, -1, false)
zoom_indicator.opts.width = vim.api.nvim_strwidth(lines[1] or "")
zoom_indicator:show()
end
-- set toggle states
---@type {toggle: snacks.toggle.Class, state: unknown}[]
local states = {}
for id, state in pairs(opts.toggles) do
local toggle = Snacks.toggle.get(id)
if toggle then
table.insert(states, { toggle = toggle, state = toggle:get() })
toggle:set(state)
end
end
opts.on_open(win)
-- sync cursor with the parent window
vim.api.nvim_create_autocmd("CursorMoved", {
group = win.augroup,
callback = function()
if win:win_valid() and vim.api.nvim_win_is_valid(parent_win) then
vim.api.nvim_win_set_cursor(parent_win, vim.api.nvim_win_get_cursor(win.win))
end
end,
})
-- restore toggle states when window is closed
win:on("WinClosed", function()
if zoom_indicator then
zoom_indicator:close()
end
for _, state in ipairs(states) do
state.toggle:set(state.state)
end
opts.on_close(win)
end, { win = true })
-- update the buffer of the parent window
-- when the zen buffer changes
win:on("BufWinEnter", function()
vim.api.nvim_win_set_buf(parent_win, win.buf)
end)
-- close when entering another window
win:on("WinEnter", function()
local w = vim.api.nvim_get_current_win()
if w == win.win then
return
end
-- exit if other window is not a floating window
if vim.api.nvim_win_get_config(w).relative == "" then
-- schedule so that WinClosed is properly triggered
vim.schedule(function()
win:close()
end)
end
end)
return win
end
---@param opts? snacks.zen.Config
function M.zoom(opts)
opts = Snacks.config.get("zen", defaults, opts)
return M.zen(opts and opts.zoom or nil)
end
return M