1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 02:50:03 +08:00
SpaceVim/bundle/trouble.nvim/lua/trouble/view.lua
2023-07-01 20:30:44 +08:00

525 lines
13 KiB
Lua

local renderer = require("trouble.renderer")
local config = require("trouble.config")
local folds = require("trouble.folds")
local util = require("trouble.util")
local highlight = vim.api.nvim_buf_add_highlight
---@class TroubleView
---@field buf number
---@field win number
---@field group boolean
---@field items Item[]
---@field folded table<string, boolean>
---@field parent number
---@field float number
local View = {}
View.__index = View
-- keep track of buffers with added highlights
-- highlights are cleared on BufLeave of Trouble
local hl_bufs = {}
local function clear_hl(bufnr)
if vim.api.nvim_buf_is_valid(bufnr) then
vim.api.nvim_buf_clear_namespace(bufnr, config.namespace, 0, -1)
end
end
---Find a rogue Trouble buffer that might have been spawned by i.e. a session.
local function find_rogue_buffer()
for _, v in ipairs(vim.api.nvim_list_bufs()) do
if vim.fn.bufname(v) == "Trouble" then
return v
end
end
return nil
end
---Find pre-existing Trouble buffer, delete its windows then wipe it.
---@private
local function wipe_rogue_buffer()
local bn = find_rogue_buffer()
if bn then
local win_ids = vim.fn.win_findbuf(bn)
for _, id in ipairs(win_ids) do
if vim.fn.win_gettype(id) ~= "autocmd" and vim.api.nvim_win_is_valid(id) then
vim.api.nvim_win_close(id, true)
end
end
vim.api.nvim_buf_set_name(bn, "")
vim.schedule(function()
pcall(vim.api.nvim_buf_delete, bn, {})
end)
end
end
function View:new(opts)
opts = opts or {}
local group
if opts.group ~= nil then
group = opts.group
else
group = config.options.group
end
local this = {
buf = vim.api.nvim_get_current_buf(),
win = opts.win or vim.api.nvim_get_current_win(),
parent = opts.parent,
items = {},
group = group,
}
setmetatable(this, self)
return this
end
function View:set_option(name, value, win)
if win then
return vim.api.nvim_set_option_value(name, value, { win = self.win, scope = "local" })
else
return vim.api.nvim_set_option_value(name, value, { buf = self.buf })
end
end
---@param text Text
function View:render(text)
self:unlock()
self:set_lines(text.lines)
self:lock()
clear_hl(self.buf)
for _, data in ipairs(text.hl) do
highlight(self.buf, config.namespace, data.group, data.line, data.from, data.to)
end
end
function View:clear()
return vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, {})
end
function View:unlock()
self:set_option("modifiable", true)
self:set_option("readonly", false)
end
function View:lock()
self:set_option("readonly", true)
self:set_option("modifiable", false)
end
function View:set_lines(lines, first, last, strict)
first = first or 0
last = last or -1
strict = strict or false
return vim.api.nvim_buf_set_lines(self.buf, first, last, strict, lines)
end
function View:is_valid()
return vim.api.nvim_buf_is_valid(self.buf) and vim.api.nvim_buf_is_loaded(self.buf)
end
function View:update(opts)
util.debug("update")
renderer.render(self, opts)
end
function View:setup(opts)
util.debug("setup")
opts = opts or {}
vim.cmd("setlocal nonu")
vim.cmd("setlocal nornu")
if not pcall(vim.api.nvim_buf_set_name, self.buf, "Trouble") then
wipe_rogue_buffer()
vim.api.nvim_buf_set_name(self.buf, "Trouble")
end
self:set_option("bufhidden", "wipe")
self:set_option("buftype", "nofile")
self:set_option("swapfile", false)
self:set_option("buflisted", false)
self:set_option("winfixwidth", true, true)
self:set_option("wrap", false, true)
self:set_option("spell", false, true)
self:set_option("list", false, true)
self:set_option("winfixheight", true, true)
self:set_option("signcolumn", "no", true)
self:set_option("foldmethod", "manual", true)
self:set_option("foldcolumn", "0", true)
self:set_option("foldlevel", 3, true)
self:set_option("foldenable", false, true)
self:set_option("winhighlight", "Normal:TroubleNormal,EndOfBuffer:TroubleNormal,SignColumn:TroubleNormal", true)
self:set_option("fcs", "eob: ", true)
for action, keys in pairs(config.options.action_keys) do
if type(keys) == "string" then
keys = { keys }
end
for _, key in pairs(keys) do
vim.api.nvim_buf_set_keymap(self.buf, "n", key, [[<cmd>lua require("trouble").action("]] .. action .. [[")<cr>]], {
silent = true,
noremap = true,
nowait = true,
})
end
end
if config.options.position == "top" or config.options.position == "bottom" then
vim.api.nvim_win_set_height(self.win, config.options.height)
else
vim.api.nvim_win_set_width(self.win, config.options.width)
end
self:set_option("filetype", "Trouble")
vim.api.nvim_exec(
[[
augroup TroubleHighlights
autocmd! * <buffer>
autocmd BufEnter <buffer> lua require("trouble").action("on_enter")
autocmd CursorMoved <buffer> lua require("trouble").action("auto_preview")
autocmd BufLeave <buffer> lua require("trouble").action("on_leave")
augroup END
]],
false
)
if not opts.parent then
self:on_enter()
end
self:lock()
self:update(opts)
end
function View:on_enter()
util.debug("on_enter")
self.parent = self.parent or vim.fn.win_getid(vim.fn.winnr("#"))
if (not self:is_valid_parent(self.parent)) or self.parent == self.win then
util.debug("not valid parent")
for _, win in pairs(vim.api.nvim_list_wins()) do
if self:is_valid_parent(win) and win ~= self.win then
self.parent = win
break
end
end
end
if not vim.api.nvim_win_is_valid(self.parent) then
return self:close()
end
self.parent_state = {
buf = vim.api.nvim_win_get_buf(self.parent),
cursor = vim.api.nvim_win_get_cursor(self.parent),
}
end
function View:on_leave()
util.debug("on_leave")
self:close_preview()
end
function View:close_preview()
-- Clear preview highlights
for buf, _ in pairs(hl_bufs) do
clear_hl(buf)
end
hl_bufs = {}
-- Reset parent state
local valid_win = vim.api.nvim_win_is_valid(self.parent)
local valid_buf = self.parent_state and vim.api.nvim_buf_is_valid(self.parent_state.buf)
if self.parent_state and valid_buf and valid_win then
vim.api.nvim_win_set_buf(self.parent, self.parent_state.buf)
vim.api.nvim_win_set_cursor(self.parent, self.parent_state.cursor)
end
self.parent_state = nil
end
function View:is_float(win)
local opts = vim.api.nvim_win_get_config(win)
return opts and opts.relative and opts.relative ~= ""
end
function View:is_valid_parent(win)
if not vim.api.nvim_win_is_valid(win) then
return false
end
-- dont do anything for floating windows
if View:is_float(win) then
return false
end
local buf = vim.api.nvim_win_get_buf(win)
-- Skip special buffers
if vim.api.nvim_buf_get_option(buf, "buftype") ~= "" then
return false
end
return true
end
function View:on_win_enter()
util.debug("on_win_enter")
local current_win = vim.api.nvim_get_current_win()
if vim.fn.winnr("$") == 1 and current_win == self.win then
vim.cmd([[q]])
return
end
if not self:is_valid_parent(current_win) then
return
end
local current_buf = vim.api.nvim_get_current_buf()
-- update parent when needed
if current_win ~= self.parent and current_win ~= self.win then
self.parent = current_win
-- update diagnostics to match the window we are viewing
if self:is_valid() then
vim.defer_fn(function()
util.debug("update_on_win_enter")
self:update()
end, 100)
end
end
-- check if another buffer took over our window
local parent = self.parent
if current_win == self.win and current_buf ~= self.buf then
-- open the buffer in the parent
vim.api.nvim_win_set_buf(parent, current_buf)
-- HACK: some window local settings need to be reset
vim.api.nvim_win_set_option(parent, "winhl", "")
-- close the current trouble window
vim.api.nvim_win_close(self.win, false)
-- open a new trouble window
require("trouble").open()
-- switch back to the opened window / buffer
View.switch_to(parent, current_buf)
-- util.warn("win_enter pro")
end
end
function View:focus()
View.switch_to(self.win, self.buf)
local line = self:get_line()
if line == 1 then
self:next_item()
if config.options.padding then
self:next_item()
end
end
end
function View.switch_to(win, buf)
if win then
vim.api.nvim_set_current_win(win)
if buf then
vim.api.nvim_win_set_buf(win, buf)
end
end
end
function View:switch_to_parent()
-- vim.cmd("wincmd p")
View.switch_to(self.parent)
end
function View:close()
util.debug("close")
if vim.api.nvim_win_is_valid(self.win) then
if vim.api.nvim_win_is_valid(self.parent) then
vim.api.nvim_set_current_win(self.parent)
end
vim.api.nvim_win_close(self.win, {})
end
if vim.api.nvim_buf_is_valid(self.buf) then
vim.api.nvim_buf_delete(self.buf, {})
end
end
function View.create(opts)
opts = opts or {}
if opts.win then
View.switch_to(opts.win)
vim.cmd("enew")
else
vim.cmd("below new")
local pos = { bottom = "J", top = "K", left = "H", right = "L" }
vim.cmd("wincmd " .. (pos[config.options.position] or "K"))
end
local buffer = View:new(opts)
buffer:setup(opts)
if opts and opts.auto then
buffer:switch_to_parent()
end
return buffer
end
function View:get_cursor()
return vim.api.nvim_win_get_cursor(self.win)
end
function View:get_line()
return self:get_cursor()[1]
end
function View:get_col()
return self:get_cursor()[2]
end
function View:current_item()
local line = self:get_line()
local item = self.items[line]
return item
end
function View:next_item(opts)
opts = opts or { skip_groups = false }
local line = opts.first and 0 or self:get_line() + 1
if line > #self.items then
if config.options.cycle_results then
self:first_item(opts)
end
else
for i = line, vim.api.nvim_buf_line_count(self.buf), 1 do
if self.items[i] and not (opts.skip_groups and self.items[i].is_file) then
vim.api.nvim_win_set_cursor(self.win, { i, self:get_col() })
if opts.jump then
self:jump()
end
return
end
end
end
end
function View:previous_item(opts)
opts = opts or { skip_groups = false }
local line = opts.last and vim.api.nvim_buf_line_count(self.buf) or self:get_line() - 1
for i = 0, vim.api.nvim_buf_line_count(self.buf), 1 do
if self.items[i] then
if line < i + (opts.skip_groups and 1 or 0) then
if config.options.cycle_results then
self:last_item(opts)
end
return
end
break
end
end
for i = line, 0, -1 do
if self.items[i] and not (opts.skip_groups and self.items[i].is_file) then
vim.api.nvim_win_set_cursor(self.win, { i, self:get_col() })
if opts.jump then
self:jump()
end
return
end
end
end
function View:first_item(opts)
opts = opts or {}
opts.first = true
return self:next_item(opts)
end
function View:last_item(opts)
opts = opts or {}
opts.last = true
return self:previous_item(opts)
end
function View:hover(opts)
opts = opts or {}
local item = opts.item or self:current_item()
if not (item and item.full_text) then
return
end
local lines = {}
for line in item.full_text:gmatch("([^\n]*)\n?") do
table.insert(lines, line)
end
vim.lsp.util.open_floating_preview(lines, "plaintext", { border = "single" })
end
function View:jump(opts)
opts = opts or {}
local item = opts.item or self:current_item()
if not item then
return
end
if item.is_file == true then
folds.toggle(item.filename)
self:update()
else
util.jump_to_item(opts.win or self.parent, opts.precmd, item)
end
end
function View:toggle_fold()
folds.toggle(self:current_item().filename)
self:update()
end
function View:_preview()
if not vim.api.nvim_win_is_valid(self.parent) then
return
end
local item = self:current_item()
if not item then
return
end
util.debug("preview")
if item.is_file ~= true then
vim.api.nvim_win_set_buf(self.parent, item.bufnr)
local pos = { item.start.line + 1, item.start.character }
local line_count = vim.api.nvim_buf_line_count(item.bufnr)
pos[1] = math.min(pos[1], line_count)
vim.api.nvim_win_set_cursor(self.parent, pos)
vim.api.nvim_buf_call(item.bufnr, function()
-- Center preview line on screen and open enough folds to show it
vim.cmd("norm! zz zv")
if not vim.api.nvim_buf_is_loaded(item.bufnr) then
vim.fn.bufload(item.bufnr)
end
end)
clear_hl(item.bufnr)
hl_bufs[item.bufnr] = true
for row = item.start.line, item.finish.line, 1 do
local col_start = 0
local col_end = -1
if row == item.start.line then
col_start = item.start.character
end
if row == item.finish.line then
col_end = item.finish.character
end
highlight(item.bufnr, config.namespace, "TroublePreview", row, col_start, col_end)
end
end
end
-- View.preview = View._preview
View.preview = util.throttle(50, View._preview)
return View