Update generated neovim config
This commit is contained in:
@ -0,0 +1,203 @@
|
||||
local Actions = require("trouble.config.actions")
|
||||
local Config = require("trouble.config")
|
||||
local Util = require("trouble.util")
|
||||
local View = require("trouble.view")
|
||||
|
||||
---@alias trouble.ApiFn fun(opts?: trouble.Config|string): trouble.View
|
||||
|
||||
---@class trouble.api: trouble.actions
|
||||
local M = {}
|
||||
M.last_mode = nil ---@type string?
|
||||
|
||||
--- Finds all open views matching the filter.
|
||||
---@param opts? trouble.Config|string
|
||||
---@param filter? trouble.View.filter
|
||||
---@return trouble.View[], trouble.Config
|
||||
function M._find(opts, filter)
|
||||
opts = Config.get(opts)
|
||||
if opts.mode == "last" then
|
||||
opts.mode = M.last_mode
|
||||
opts = Config.get(opts)
|
||||
end
|
||||
M.last_mode = opts.mode or M.last_mode
|
||||
filter = filter or { open = true, mode = opts.mode }
|
||||
return vim.tbl_map(function(v)
|
||||
return v.view
|
||||
end, View.get(filter)), opts
|
||||
end
|
||||
|
||||
--- Finds the last open view matching the filter.
|
||||
---@param opts? trouble.Mode|string
|
||||
---@param filter? trouble.View.filter
|
||||
---@return trouble.View?, trouble.Mode
|
||||
function M._find_last(opts, filter)
|
||||
local views, _opts = M._find(opts, filter)
|
||||
---@cast _opts trouble.Mode
|
||||
return views[#views], _opts
|
||||
end
|
||||
|
||||
-- Opens trouble with the given mode.
|
||||
-- If a view is already open with the same mode,
|
||||
-- it will be focused unless `opts.focus = false`.
|
||||
-- When a view is already open and `opts.new = true`,
|
||||
-- a new view will be created.
|
||||
---@param opts? trouble.Mode | { new?: boolean, refresh?: boolean } | string
|
||||
---@return trouble.View?
|
||||
function M.open(opts)
|
||||
opts = opts or {}
|
||||
local view, _opts = M._find_last(opts)
|
||||
if not view or _opts.new then
|
||||
if not _opts.mode then
|
||||
return Util.error("No mode specified")
|
||||
elseif not vim.tbl_contains(Config.modes(), _opts.mode) then
|
||||
return Util.error("Invalid mode `" .. _opts.mode .. "`")
|
||||
end
|
||||
view = View.new(_opts)
|
||||
end
|
||||
if view then
|
||||
if view:is_open() then
|
||||
if opts.refresh ~= false then
|
||||
view:refresh()
|
||||
end
|
||||
else
|
||||
view:open()
|
||||
end
|
||||
if _opts.focus ~= false then
|
||||
view:wait(function()
|
||||
view.win:focus()
|
||||
end)
|
||||
end
|
||||
end
|
||||
return view
|
||||
end
|
||||
|
||||
-- Closes the last open view matching the filter.
|
||||
---@param opts? trouble.Mode|string
|
||||
---@return trouble.View?
|
||||
function M.close(opts)
|
||||
local view = M._find_last(opts)
|
||||
if view then
|
||||
view:close()
|
||||
return view
|
||||
end
|
||||
end
|
||||
|
||||
-- Toggle the view with the given mode.
|
||||
---@param opts? trouble.Mode|string
|
||||
---@return trouble.View?
|
||||
function M.toggle(opts)
|
||||
if M.is_open(opts) then
|
||||
---@diagnostic disable-next-line: return-type-mismatch
|
||||
return M.close(opts)
|
||||
else
|
||||
return M.open(opts)
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns true if there is an open view matching the mode.
|
||||
---@param opts? trouble.Mode|string
|
||||
function M.is_open(opts)
|
||||
return M._find_last(opts) ~= nil
|
||||
end
|
||||
|
||||
-- Refresh all open views. Normally this is done automatically,
|
||||
-- unless you disabled auto refresh.
|
||||
---@param opts? trouble.Mode|string
|
||||
function M.refresh(opts)
|
||||
for _, view in ipairs(M._find(opts)) do
|
||||
view:refresh()
|
||||
end
|
||||
end
|
||||
|
||||
-- Proxy to last view's action.
|
||||
---@param action trouble.Action.spec
|
||||
function M._action(action)
|
||||
return function(opts)
|
||||
opts = opts or {}
|
||||
if type(opts) == "string" then
|
||||
opts = { mode = opts }
|
||||
end
|
||||
opts = vim.tbl_deep_extend("force", {
|
||||
refresh = false,
|
||||
}, opts)
|
||||
local view = M.open(opts)
|
||||
if view then
|
||||
view:action(action, opts)
|
||||
end
|
||||
return view
|
||||
end
|
||||
end
|
||||
|
||||
-- Get all items from the active view for a given mode.
|
||||
---@param opts? trouble.Mode|string
|
||||
function M.get_items(opts)
|
||||
local view = M._find_last(opts)
|
||||
local ret = {} ---@type trouble.Item[]
|
||||
if view then
|
||||
for _, source in pairs(view.sections) do
|
||||
vim.list_extend(ret, source.items or {})
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
-- Renders a trouble list as a statusline component.
|
||||
-- Check the docs for examples.
|
||||
---@param opts? trouble.Mode|string|{hl_group?:string}
|
||||
---@return {get: (fun():string), has: (fun():boolean)}
|
||||
function M.statusline(opts)
|
||||
local Spec = require("trouble.spec")
|
||||
local Section = require("trouble.view.section")
|
||||
local Render = require("trouble.view.render")
|
||||
opts = Config.get(opts)
|
||||
opts.indent_guides = false
|
||||
opts.icons.indent.ws = ""
|
||||
local renderer = Render.new(opts, {
|
||||
multiline = false,
|
||||
indent = false,
|
||||
})
|
||||
local status = nil ---@type string?
|
||||
---@cast opts trouble.Mode
|
||||
|
||||
local s = Spec.section(opts)
|
||||
s.max_items = s.max_items or opts.max_items
|
||||
local section = Section.new(s, opts)
|
||||
section.on_update = function()
|
||||
status = nil
|
||||
if package.loaded["lualine"] then
|
||||
vim.schedule(function()
|
||||
require("lualine").refresh()
|
||||
end)
|
||||
else
|
||||
vim.cmd.redrawstatus()
|
||||
end
|
||||
end
|
||||
section:listen()
|
||||
section:refresh()
|
||||
return {
|
||||
has = function()
|
||||
return section.node and section.node:count() > 0
|
||||
end,
|
||||
get = function()
|
||||
if status then
|
||||
return status
|
||||
end
|
||||
renderer:clear()
|
||||
renderer:sections({ section })
|
||||
status = renderer:statusline()
|
||||
if opts.hl_group then
|
||||
status = require("trouble.config.highlights").fix_statusline(status, opts.hl_group)
|
||||
end
|
||||
return status
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return setmetatable(M, {
|
||||
__index = function(_, k)
|
||||
if k == "last_mode" then
|
||||
return nil
|
||||
end
|
||||
return M._action(k)
|
||||
end,
|
||||
})
|
||||
@ -0,0 +1,135 @@
|
||||
local M = {}
|
||||
local uv = vim.loop or vim.uv
|
||||
|
||||
M.budget = 1
|
||||
local Scheduler = {}
|
||||
Scheduler._queue = {}
|
||||
Scheduler._executor = assert(uv.new_check())
|
||||
|
||||
function Scheduler.step()
|
||||
local budget = M.budget * 1e6
|
||||
local start = uv.hrtime()
|
||||
while #Scheduler._queue > 0 and uv.hrtime() - start < budget do
|
||||
local a = table.remove(Scheduler._queue, 1)
|
||||
a:_step()
|
||||
if a.running then
|
||||
table.insert(Scheduler._queue, a)
|
||||
end
|
||||
end
|
||||
if #Scheduler._queue == 0 then
|
||||
return Scheduler._executor:stop()
|
||||
end
|
||||
end
|
||||
|
||||
---@param a Async
|
||||
function Scheduler.add(a)
|
||||
table.insert(Scheduler._queue, a)
|
||||
if not Scheduler._executor:is_active() then
|
||||
Scheduler._executor:start(vim.schedule_wrap(Scheduler.step))
|
||||
end
|
||||
end
|
||||
|
||||
--- @alias AsyncCallback fun(result?:any, error?:string)
|
||||
|
||||
--- @class Async
|
||||
--- @field running boolean
|
||||
--- @field result? any
|
||||
--- @field error? string
|
||||
--- @field callbacks AsyncCallback[]
|
||||
--- @field thread thread
|
||||
local Async = {}
|
||||
Async.__index = Async
|
||||
|
||||
function Async.new(fn)
|
||||
local self = setmetatable({}, Async)
|
||||
self.callbacks = {}
|
||||
self.running = true
|
||||
self.thread = coroutine.create(fn)
|
||||
Scheduler.add(self)
|
||||
return self
|
||||
end
|
||||
|
||||
---@param result? any
|
||||
---@param error? string
|
||||
function Async:_done(result, error)
|
||||
if self.running then
|
||||
self.running = false
|
||||
self.result = result
|
||||
self.error = error
|
||||
end
|
||||
for _, callback in ipairs(self.callbacks) do
|
||||
callback(result, error)
|
||||
end
|
||||
-- only run each callback once.
|
||||
-- _done can possibly be called multiple times.
|
||||
-- so we need to clear callbacks after executing them.
|
||||
self.callbacks = {}
|
||||
end
|
||||
|
||||
function Async:_step()
|
||||
local ok, res = coroutine.resume(self.thread)
|
||||
if not ok then
|
||||
return self:_done(nil, res)
|
||||
elseif res == "abort" then
|
||||
return self:_done(nil, "abort")
|
||||
elseif coroutine.status(self.thread) == "dead" then
|
||||
return self:_done(res)
|
||||
end
|
||||
end
|
||||
|
||||
function Async:cancel()
|
||||
self:_done(nil, "abort")
|
||||
end
|
||||
|
||||
---@param cb AsyncCallback
|
||||
function Async:await(cb)
|
||||
if not cb then
|
||||
error("callback is required")
|
||||
end
|
||||
if self.running then
|
||||
table.insert(self.callbacks, cb)
|
||||
else
|
||||
cb(self.result, self.error)
|
||||
end
|
||||
end
|
||||
|
||||
function Async:sync()
|
||||
while self.running do
|
||||
vim.wait(10)
|
||||
end
|
||||
return self.error and error(self.error) or self.result
|
||||
end
|
||||
|
||||
--- @return boolean
|
||||
function M.is_async(obj)
|
||||
return obj and type(obj) == "table" and getmetatable(obj) == Async
|
||||
end
|
||||
|
||||
---@generic F
|
||||
---@param fn F
|
||||
---@return F|fun(...): Async
|
||||
function M.wrap(fn)
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
return Async.new(function()
|
||||
return fn(unpack(args))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- This will yield when called from a coroutine
|
||||
---@async
|
||||
function M.yield(...)
|
||||
if coroutine.running() == nil then
|
||||
error("Trying to yield from a non-yieldable context")
|
||||
return ...
|
||||
end
|
||||
return coroutine.yield(...)
|
||||
end
|
||||
|
||||
---@async
|
||||
function M.abort()
|
||||
return M.yield("abort")
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,60 @@
|
||||
---@class trouble.CacheM: {[string]: trouble.Cache}
|
||||
local M = {}
|
||||
|
||||
---@type table<string, {name:string, hit: number, miss: number, ratio?:number}>
|
||||
M.stats = {}
|
||||
|
||||
---@class trouble.Cache: {[string]: any}
|
||||
---@field data table<string, any>
|
||||
---@field name string
|
||||
---@field size number
|
||||
local C = {}
|
||||
|
||||
function C:__index(key)
|
||||
local ret = C[key]
|
||||
if ret then
|
||||
return ret
|
||||
end
|
||||
ret = self.data[key]
|
||||
M.stats[self.name] = M.stats[self.name] or { name = self.name, hit = 0, miss = 0 }
|
||||
local stats = M.stats[self.name]
|
||||
if ret ~= nil then
|
||||
stats.hit = stats.hit + 1
|
||||
else
|
||||
stats.miss = stats.miss + 1
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function C:__newindex(key, value)
|
||||
if self.data[key] ~= nil and value == nil then
|
||||
self.size = self.size - 1
|
||||
elseif self.data[key] == nil and value ~= nil then
|
||||
self.size = self.size + 1
|
||||
end
|
||||
self.data[key] = value
|
||||
end
|
||||
|
||||
function C:clear()
|
||||
self.data = {}
|
||||
self.size = 0
|
||||
end
|
||||
|
||||
function M.new(name)
|
||||
return setmetatable({ data = {}, name = name, size = 0 }, C)
|
||||
end
|
||||
|
||||
function M.report()
|
||||
for _, v in pairs(M.stats) do
|
||||
v.ratio = math.ceil(v.hit / (v.hit + v.miss) * 100)
|
||||
end
|
||||
return M.stats
|
||||
end
|
||||
|
||||
function M.__index(_, k)
|
||||
M[k] = M.new(k)
|
||||
return M[k]
|
||||
end
|
||||
|
||||
local ret = setmetatable(M, M)
|
||||
return ret
|
||||
@ -0,0 +1,136 @@
|
||||
local Config = require("trouble.config")
|
||||
local Parser = require("trouble.config.parser")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param prefix string
|
||||
---@param line string
|
||||
---@param col number
|
||||
function M.complete(prefix, line, col)
|
||||
line = line:sub(1, col):match("Trouble%s*(.*)$")
|
||||
local parsed = M.parse(line)
|
||||
local candidates = {} ---@type string[]
|
||||
if vim.tbl_isempty(parsed.opts) then
|
||||
if not parsed.mode then
|
||||
vim.list_extend(candidates, Config.modes())
|
||||
else
|
||||
if not parsed.action then
|
||||
vim.list_extend(candidates, M.actions())
|
||||
end
|
||||
vim.list_extend(candidates, M.complete_opts())
|
||||
end
|
||||
else
|
||||
vim.list_extend(candidates, M.complete_opts())
|
||||
end
|
||||
candidates = vim.tbl_filter(function(x)
|
||||
return tostring(x):find(prefix, 1, true) == 1
|
||||
end, candidates)
|
||||
table.sort(candidates)
|
||||
return candidates
|
||||
end
|
||||
|
||||
function M.complete_opts()
|
||||
local candidates = {} ---@type string[]
|
||||
local stack = { { k = "", t = Config.get() } }
|
||||
while #stack > 0 do
|
||||
local top = table.remove(stack)
|
||||
for k, v in pairs(top.t) do
|
||||
if type(k) == "number" then
|
||||
k = "[" .. k .. "]"
|
||||
elseif k:match("^[a-z_]+$") then
|
||||
k = "." .. k
|
||||
else
|
||||
k = ("[%q]"):format(k)
|
||||
end
|
||||
local kk = top.k .. k
|
||||
candidates[#candidates + 1] = kk:gsub("^%.", "") .. "="
|
||||
if type(v) == "table" and not Util.islist(v) then
|
||||
table.insert(stack, { k = kk, t = v })
|
||||
end
|
||||
end
|
||||
end
|
||||
vim.list_extend(candidates, {
|
||||
"new=true",
|
||||
})
|
||||
for _, w in ipairs({ "win", "preview" }) do
|
||||
local winopts = {
|
||||
"type=float",
|
||||
"type=split",
|
||||
"position=top",
|
||||
"position=bottom",
|
||||
"position=left",
|
||||
"position=right",
|
||||
"relative=editor",
|
||||
"relative=win",
|
||||
}
|
||||
vim.list_extend(
|
||||
candidates,
|
||||
vim.tbl_map(function(x)
|
||||
return w .. "." .. x
|
||||
end, winopts)
|
||||
)
|
||||
end
|
||||
return candidates
|
||||
end
|
||||
|
||||
function M.actions()
|
||||
local actions = vim.tbl_keys(require("trouble.api"))
|
||||
vim.list_extend(actions, vim.tbl_keys(require("trouble.config.actions")))
|
||||
return actions
|
||||
end
|
||||
|
||||
---@param input string
|
||||
function M.parse(input)
|
||||
---@type {mode: string, action: string, opts: trouble.Config, errors: string[], args: string[]}
|
||||
local ret = Parser.parse(input)
|
||||
local modes = Config.modes()
|
||||
local actions = M.actions()
|
||||
|
||||
-- Args can be mode and/or action
|
||||
for _, a in ipairs(ret.args) do
|
||||
if vim.tbl_contains(modes, a) then
|
||||
ret.mode = a
|
||||
elseif vim.tbl_contains(actions, a) then
|
||||
ret.action = a
|
||||
else
|
||||
table.insert(ret.errors, "Unknown argument: " .. a)
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.execute(input)
|
||||
if input.args:match("^%s*$") then
|
||||
---@type {name: string, desc: string}[]
|
||||
local modes = vim.tbl_map(function(x)
|
||||
local m = Config.get(x)
|
||||
local desc = m.desc or x:gsub("^%l", string.upper)
|
||||
desc = Util.camel(desc, " ")
|
||||
return { name = x, desc = desc }
|
||||
end, Config.modes())
|
||||
|
||||
vim.ui.select(modes, {
|
||||
prompt = "Select Trouble Mode:",
|
||||
format_item = function(x)
|
||||
return x.desc and (x.desc .. " (" .. x.name .. ")") or x.name
|
||||
end,
|
||||
}, function(mode)
|
||||
if mode then
|
||||
require("trouble").open({ mode = mode.name })
|
||||
end
|
||||
end)
|
||||
else
|
||||
local ret = M.parse(input.args)
|
||||
ret.action = ret.action or "open"
|
||||
ret.opts.mode = ret.opts.mode or ret.mode
|
||||
if #ret.errors > 0 then
|
||||
Util.error("Error parsing command:\n- input: `" .. input.args .. "`\nErrors:\n" .. table.concat(ret.errors, "\n"))
|
||||
return
|
||||
end
|
||||
require("trouble")[ret.action](ret.opts)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,181 @@
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@alias trouble.Action.ctx {item?: trouble.Item, node?: trouble.Node, opts?: table}
|
||||
---@alias trouble.ActionFn fun(view:trouble.View, ctx:trouble.Action.ctx)
|
||||
---@alias trouble.Action {action: trouble.ActionFn, desc?: string, mode?: string}
|
||||
---@alias trouble.Action.spec string|trouble.ActionFn|trouble.Action|{action: string}
|
||||
|
||||
---@class trouble.actions: {[string]: trouble.ActionFn}
|
||||
local M = {
|
||||
-- Refresh the trouble source
|
||||
refresh = function(self)
|
||||
self:refresh()
|
||||
end,
|
||||
-- Close the trouble window
|
||||
close = function(self)
|
||||
self:close()
|
||||
end,
|
||||
-- Closes the preview and goes to the main window.
|
||||
-- The Trouble window is not closed.
|
||||
cancel = function(self)
|
||||
self:goto_main()
|
||||
end,
|
||||
-- Focus the trouble window
|
||||
focus = function(self)
|
||||
self.win:focus()
|
||||
end,
|
||||
-- Open the preview
|
||||
preview = function(self, ctx)
|
||||
local Preview = require("trouble.view.preview")
|
||||
if Preview.is_open() then
|
||||
Preview.close()
|
||||
else
|
||||
self:preview(ctx.item)
|
||||
end
|
||||
end,
|
||||
-- Open the preview
|
||||
delete = function(self)
|
||||
local enabled = self.opts.auto_refresh
|
||||
self:delete()
|
||||
if enabled and not self.opts.auto_refresh then
|
||||
Util.warn("Auto refresh **disabled**", { id = "toggle_refresh" })
|
||||
end
|
||||
end,
|
||||
-- Toggle the preview
|
||||
toggle_preview = function(self, ctx)
|
||||
self.opts.auto_preview = not self.opts.auto_preview
|
||||
local enabled = self.opts.auto_preview and "enabled" or "disabled"
|
||||
local notify = (enabled == "enabled") and Util.info or Util.warn
|
||||
notify("Auto preview **" .. enabled .. "**", { id = "toggle_preview" })
|
||||
local Preview = require("trouble.view.preview")
|
||||
if self.opts.auto_preview then
|
||||
if ctx.item then
|
||||
self:preview()
|
||||
end
|
||||
else
|
||||
Preview.close()
|
||||
end
|
||||
end,
|
||||
-- Toggle the auto refresh
|
||||
toggle_refresh = function(self)
|
||||
self.opts.auto_refresh = not self.opts.auto_refresh
|
||||
local enabled = self.opts.auto_refresh and "enabled" or "disabled"
|
||||
local notify = (enabled == "enabled") and Util.info or Util.warn
|
||||
notify("Auto refresh **" .. enabled .. "**", { id = "toggle_refresh" })
|
||||
end,
|
||||
|
||||
filter = function(self, ctx)
|
||||
self:filter(ctx.opts.filter)
|
||||
end,
|
||||
-- Show the help
|
||||
help = function(self)
|
||||
self:help()
|
||||
end,
|
||||
-- Go to the next item
|
||||
next = function(self, ctx)
|
||||
self:move({ down = vim.v.count1, jump = ctx.opts.jump })
|
||||
end,
|
||||
-- Go to the previous item
|
||||
prev = function(self, ctx)
|
||||
self:move({ up = vim.v.count1, jump = ctx.opts.jump })
|
||||
end,
|
||||
-- Go to the first item
|
||||
first = function(self, ctx)
|
||||
self:move({ idx = vim.v.count1, jump = ctx.opts.jump })
|
||||
end,
|
||||
-- Go to the last item
|
||||
last = function(self, ctx)
|
||||
self:move({ idx = -vim.v.count1, jump = ctx.opts.jump })
|
||||
end,
|
||||
-- Jump to the item if on an item, otherwise do nothing
|
||||
jump_only = function(self, ctx)
|
||||
if ctx.item then
|
||||
self:jump(ctx.item)
|
||||
end
|
||||
end,
|
||||
-- Jump to the item if on an item, otherwise fold the node
|
||||
jump = function(self, ctx)
|
||||
if ctx.item then
|
||||
self:jump(ctx.item)
|
||||
elseif ctx.node then
|
||||
self:fold(ctx.node)
|
||||
end
|
||||
end,
|
||||
-- Jump to the item and close the trouble window
|
||||
jump_close = function(self, ctx)
|
||||
if ctx.item then
|
||||
self:jump(ctx.item)
|
||||
self:close()
|
||||
end
|
||||
end,
|
||||
-- Open the item in a split
|
||||
jump_split = function(self, ctx)
|
||||
if ctx.item then
|
||||
self:jump(ctx.item, { split = true })
|
||||
end
|
||||
end,
|
||||
-- Open the item in a vsplit
|
||||
jump_vsplit = function(self, ctx)
|
||||
if ctx.item then
|
||||
self:jump(ctx.item, { vsplit = true })
|
||||
end
|
||||
end,
|
||||
-- Dump the item to the console
|
||||
inspect = function(_, ctx)
|
||||
vim.print(ctx.item or (ctx.node and ctx.node.item))
|
||||
end,
|
||||
fold_reduce = function(self)
|
||||
self:fold_level({ add = vim.v.count1 })
|
||||
end,
|
||||
fold_open_all = function(self)
|
||||
self:fold_level({ level = 1000 })
|
||||
end,
|
||||
fold_more = function(self)
|
||||
self:fold_level({ add = -vim.v.count1 })
|
||||
end,
|
||||
fold_close_all = function(self)
|
||||
self:fold_level({ level = 0 })
|
||||
end,
|
||||
fold_update = function(self, ctx)
|
||||
self:fold_level({})
|
||||
self:fold(ctx.node, { action = "open" })
|
||||
end,
|
||||
fold_update_all = function(self)
|
||||
self:fold_level({})
|
||||
end,
|
||||
fold_disable = function(self)
|
||||
self.renderer.foldenable = false
|
||||
self:render()
|
||||
end,
|
||||
fold_enable = function(self)
|
||||
self.renderer.foldenable = true
|
||||
self:render()
|
||||
end,
|
||||
fold_toggle_enable = function(self)
|
||||
self.renderer.foldenable = not self.renderer.foldenable
|
||||
self:render()
|
||||
end,
|
||||
}
|
||||
|
||||
for _, fold_action in ipairs({ "toggle", "open", "close" }) do
|
||||
for _, recursive in ipairs({ true, false }) do
|
||||
local desc = "Fold " .. fold_action .. " " .. (recursive and "recursive" or "")
|
||||
local name = "fold_" .. fold_action .. (recursive and "_recursive" or "")
|
||||
M[name] = {
|
||||
action = function(self, ctx)
|
||||
self:fold(ctx.node, { action = fold_action, recursive = recursive })
|
||||
end,
|
||||
desc = desc,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable(M, {
|
||||
__index = function(_, k)
|
||||
if k == "previous" then
|
||||
Util.warn("`previous` is deprecated, use `prev` instead")
|
||||
else
|
||||
Util.error("Action not found: " .. k)
|
||||
end
|
||||
end,
|
||||
})
|
||||
@ -0,0 +1,113 @@
|
||||
local Util = require("trouble.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- stylua: ignore
|
||||
M.colors = {
|
||||
-- General
|
||||
Normal = "NormalFloat",
|
||||
NormalNC = "NormalFloat",
|
||||
Text = "Normal",
|
||||
Preview = "Visual",
|
||||
|
||||
-- Item
|
||||
Filename = "Directory",
|
||||
Basename = "TroubleFilename",
|
||||
Directory = "Directory",
|
||||
IconDirectory = "Special",
|
||||
Source = "Comment",
|
||||
Code = "Special",
|
||||
Pos = "LineNr",
|
||||
Count = "TabLineSel",
|
||||
|
||||
-- Indent Guides
|
||||
Indent = "LineNr",
|
||||
IndentFoldClosed = "CursorLineNr",
|
||||
IndentFoldOpen = "TroubleIndent",
|
||||
IndentTop = "TroubleIndent",
|
||||
IndentMiddle = "TroubleIndent",
|
||||
IndentLast = "TroubleIndent",
|
||||
IndentWs = "TroubleIndent",
|
||||
|
||||
-- LSP Symbol Kinds
|
||||
IconArray = "@punctuation.bracket",
|
||||
IconBoolean = "@boolean",
|
||||
IconClass = "@type",
|
||||
IconConstant = "@constant",
|
||||
IconConstructor = "@constructor",
|
||||
IconEnum = "@lsp.type.enum",
|
||||
IconEnumMember = "@lsp.type.enumMember",
|
||||
IconEvent = "Special",
|
||||
IconField = "@variable.member",
|
||||
IconFile = "Normal",
|
||||
IconFunction = "@function",
|
||||
IconInterface = "@lsp.type.interface",
|
||||
IconKey = "@lsp.type.keyword",
|
||||
IconMethod = "@function.method",
|
||||
IconModule = "@module",
|
||||
IconNamespace = "@module",
|
||||
IconNull = "@constant.builtin",
|
||||
IconNumber = "@number",
|
||||
IconObject = "@constant",
|
||||
IconOperator = "@operator",
|
||||
IconPackage = "@module",
|
||||
IconProperty = "@property",
|
||||
IconString = "@string",
|
||||
IconStruct = "@lsp.type.struct",
|
||||
IconTypeParameter = "@lsp.type.typeParameter",
|
||||
IconVariable = "@variable",
|
||||
}
|
||||
|
||||
function M.setup()
|
||||
M.link(M.colors)
|
||||
M.source("fs")
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
group = vim.api.nvim_create_augroup("trouble.colorscheme", { clear = true }),
|
||||
callback = function()
|
||||
M._fixed = {}
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param prefix? string
|
||||
---@param links table<string, string>
|
||||
function M.link(links, prefix)
|
||||
for k, v in pairs(links) do
|
||||
k = (prefix or "Trouble") .. k
|
||||
vim.api.nvim_set_hl(0, k, { link = v, default = true })
|
||||
end
|
||||
end
|
||||
|
||||
---@param source string
|
||||
---@param links? table<string, string>
|
||||
function M.source(source, links)
|
||||
---@type table<string, string>
|
||||
links = vim.tbl_extend("force", {
|
||||
Filename = "TroubleFilename",
|
||||
Basename = "TroubleFilename",
|
||||
Source = "TroubleSource",
|
||||
Pos = "TroublePos",
|
||||
Count = "TroubleCount",
|
||||
}, links or {})
|
||||
M.link(links, "Trouble" .. Util.camel(source))
|
||||
end
|
||||
|
||||
M._fixed = {} ---@type table<string, string>
|
||||
---@param sl string
|
||||
function M.fix_statusline(sl, statusline_hl)
|
||||
local bg = vim.api.nvim_get_hl(0, { name = statusline_hl, link = false })
|
||||
bg = bg and bg.bg or nil
|
||||
|
||||
return sl:gsub("%%#(.-)#", function(hl)
|
||||
if not M._fixed[hl] then
|
||||
local opts = vim.api.nvim_get_hl(0, { name = hl, link = false }) or {}
|
||||
opts.bg = bg
|
||||
local group = "TroubleStatusline" .. vim.tbl_count(M._fixed)
|
||||
vim.api.nvim_set_hl(0, group, opts)
|
||||
M._fixed[hl] = group
|
||||
end
|
||||
return "%#" .. M._fixed[hl] .. "#"
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,337 @@
|
||||
---@class trouble.Config.mod: trouble.Config
|
||||
local M = {}
|
||||
|
||||
---@class trouble.Mode: trouble.Config,trouble.Section.spec
|
||||
---@field desc? string
|
||||
---@field sections? string[]
|
||||
|
||||
---@class trouble.Config
|
||||
---@field mode? string
|
||||
---@field config? fun(opts:trouble.Config)
|
||||
---@field formatters? table<string,trouble.Formatter> custom formatters
|
||||
---@field filters? table<string, trouble.FilterFn> custom filters
|
||||
---@field sorters? table<string, trouble.SorterFn> custom sorters
|
||||
local defaults = {
|
||||
debug = false,
|
||||
auto_close = false, -- auto close when there are no items
|
||||
auto_open = false, -- auto open when there are items
|
||||
auto_preview = true, -- automatically open preview when on an item
|
||||
auto_refresh = true, -- auto refresh when open
|
||||
auto_jump = false, -- auto jump to the item when there's only one
|
||||
focus = false, -- Focus the window when opened
|
||||
restore = true, -- restores the last location in the list when opening
|
||||
follow = true, -- Follow the current item
|
||||
indent_guides = true, -- show indent guides
|
||||
max_items = 200, -- limit number of items that can be displayed per section
|
||||
multiline = true, -- render multi-line messages
|
||||
pinned = false, -- When pinned, the opened trouble window will be bound to the current buffer
|
||||
warn_no_results = true, -- show a warning when there are no results
|
||||
open_no_results = false, -- open the trouble window when there are no results
|
||||
---@type trouble.Window.opts
|
||||
win = {}, -- window options for the results window. Can be a split or a floating window.
|
||||
-- Window options for the preview window. Can be a split, floating window,
|
||||
-- or `main` to show the preview in the main editor window.
|
||||
---@type trouble.Window.opts
|
||||
preview = {
|
||||
type = "main",
|
||||
-- when a buffer is not yet loaded, the preview window will be created
|
||||
-- in a scratch buffer with only syntax highlighting enabled.
|
||||
-- Set to false, if you want the preview to always be a real loaded buffer.
|
||||
scratch = true,
|
||||
},
|
||||
-- Throttle/Debounce settings. Should usually not be changed.
|
||||
---@type table<string, number|{ms:number, debounce?:boolean}>
|
||||
throttle = {
|
||||
refresh = 20, -- fetches new data when needed
|
||||
update = 10, -- updates the window
|
||||
render = 10, -- renders the window
|
||||
follow = 100, -- follows the current item
|
||||
preview = { ms = 100, debounce = true }, -- shows the preview for the current item
|
||||
},
|
||||
-- Key mappings can be set to the name of a builtin action,
|
||||
-- or you can define your own custom action.
|
||||
---@type table<string, trouble.Action.spec|false>
|
||||
keys = {
|
||||
["?"] = "help",
|
||||
r = "refresh",
|
||||
R = "toggle_refresh",
|
||||
q = "close",
|
||||
o = "jump_close",
|
||||
["<esc>"] = "cancel",
|
||||
["<cr>"] = "jump",
|
||||
["<2-leftmouse>"] = "jump",
|
||||
["<c-s>"] = "jump_split",
|
||||
["<c-v>"] = "jump_vsplit",
|
||||
-- go down to next item (accepts count)
|
||||
-- j = "next",
|
||||
["}"] = "next",
|
||||
["]]"] = "next",
|
||||
-- go up to prev item (accepts count)
|
||||
-- k = "prev",
|
||||
["{"] = "prev",
|
||||
["[["] = "prev",
|
||||
dd = "delete",
|
||||
d = { action = "delete", mode = "v" },
|
||||
i = "inspect",
|
||||
p = "preview",
|
||||
P = "toggle_preview",
|
||||
zo = "fold_open",
|
||||
zO = "fold_open_recursive",
|
||||
zc = "fold_close",
|
||||
zC = "fold_close_recursive",
|
||||
za = "fold_toggle",
|
||||
zA = "fold_toggle_recursive",
|
||||
zm = "fold_more",
|
||||
zM = "fold_close_all",
|
||||
zr = "fold_reduce",
|
||||
zR = "fold_open_all",
|
||||
zx = "fold_update",
|
||||
zX = "fold_update_all",
|
||||
zn = "fold_disable",
|
||||
zN = "fold_enable",
|
||||
zi = "fold_toggle_enable",
|
||||
gb = { -- example of a custom action that toggles the active view filter
|
||||
action = function(view)
|
||||
view:filter({ buf = 0 }, { toggle = true })
|
||||
end,
|
||||
desc = "Toggle Current Buffer Filter",
|
||||
},
|
||||
s = { -- example of a custom action that toggles the severity
|
||||
action = function(view)
|
||||
local f = view:get_filter("severity")
|
||||
local severity = ((f and f.filter.severity or 0) + 1) % 5
|
||||
view:filter({ severity = severity }, {
|
||||
id = "severity",
|
||||
template = "{hl:Title}Filter:{hl} {severity}",
|
||||
del = severity == 0,
|
||||
})
|
||||
end,
|
||||
desc = "Toggle Severity Filter",
|
||||
},
|
||||
},
|
||||
---@type table<string, trouble.Mode>
|
||||
modes = {
|
||||
-- sources define their own modes, which you can use directly,
|
||||
-- or override like in the example below
|
||||
lsp_references = {
|
||||
-- some modes are configurable, see the source code for more details
|
||||
params = {
|
||||
include_declaration = true,
|
||||
},
|
||||
},
|
||||
-- The LSP base mode for:
|
||||
-- * lsp_definitions, lsp_references, lsp_implementations
|
||||
-- * lsp_type_definitions, lsp_declarations, lsp_command
|
||||
lsp_base = {
|
||||
params = {
|
||||
-- don't include the current location in the results
|
||||
include_current = false,
|
||||
},
|
||||
},
|
||||
-- more advanced example that extends the lsp_document_symbols
|
||||
symbols = {
|
||||
desc = "document symbols",
|
||||
mode = "lsp_document_symbols",
|
||||
focus = false,
|
||||
win = { position = "right" },
|
||||
filter = {
|
||||
-- remove Package since luals uses it for control flow structures
|
||||
["not"] = { ft = "lua", kind = "Package" },
|
||||
any = {
|
||||
-- all symbol kinds for help / markdown files
|
||||
ft = { "help", "markdown" },
|
||||
-- default set of symbol kinds
|
||||
kind = {
|
||||
"Class",
|
||||
"Constructor",
|
||||
"Enum",
|
||||
"Field",
|
||||
"Function",
|
||||
"Interface",
|
||||
"Method",
|
||||
"Module",
|
||||
"Namespace",
|
||||
"Package",
|
||||
"Property",
|
||||
"Struct",
|
||||
"Trait",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
-- stylua: ignore
|
||||
icons = {
|
||||
---@type trouble.Indent.symbols
|
||||
indent = {
|
||||
top = "│ ",
|
||||
middle = "├╴",
|
||||
last = "└╴",
|
||||
-- last = "-╴",
|
||||
-- last = "╰╴", -- rounded
|
||||
fold_open = " ",
|
||||
fold_closed = " ",
|
||||
ws = " ",
|
||||
},
|
||||
folder_closed = " ",
|
||||
folder_open = " ",
|
||||
kinds = {
|
||||
Array = " ",
|
||||
Boolean = " ",
|
||||
Class = " ",
|
||||
Constant = " ",
|
||||
Constructor = " ",
|
||||
Enum = " ",
|
||||
EnumMember = " ",
|
||||
Event = " ",
|
||||
Field = " ",
|
||||
File = " ",
|
||||
Function = " ",
|
||||
Interface = " ",
|
||||
Key = " ",
|
||||
Method = " ",
|
||||
Module = " ",
|
||||
Namespace = " ",
|
||||
Null = " ",
|
||||
Number = " ",
|
||||
Object = " ",
|
||||
Operator = " ",
|
||||
Package = " ",
|
||||
Property = " ",
|
||||
String = " ",
|
||||
Struct = " ",
|
||||
TypeParameter = " ",
|
||||
Variable = " ",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@type trouble.Config
|
||||
local options
|
||||
|
||||
---@param opts? trouble.Config
|
||||
function M.setup(opts)
|
||||
if vim.fn.has("nvim-0.9.2") == 0 then
|
||||
local msg = "trouble.nvim requires Neovim >= 0.9.2"
|
||||
vim.notify_once(msg, vim.log.levels.ERROR, { title = "trouble.nvim" })
|
||||
error(msg)
|
||||
return
|
||||
end
|
||||
opts = opts or {}
|
||||
|
||||
if opts.auto_open then
|
||||
require("trouble.util").warn({
|
||||
"You specified `auto_open = true` in your global config.",
|
||||
"This is probably not what you want.",
|
||||
"Add it to the mode you want to auto open instead.",
|
||||
"```lua",
|
||||
"opts = {",
|
||||
" modes = {",
|
||||
" diagnostics = { auto_open = true },",
|
||||
" }",
|
||||
"}",
|
||||
"```",
|
||||
"Disabling global `auto_open`.",
|
||||
})
|
||||
opts.auto_open = nil
|
||||
end
|
||||
opts.mode = nil
|
||||
options = {}
|
||||
options = M.get(opts)
|
||||
require("trouble.config.highlights").setup()
|
||||
vim.api.nvim_create_user_command("Trouble", function(input)
|
||||
require("trouble.command").execute(input)
|
||||
end, {
|
||||
nargs = "*",
|
||||
complete = function(...)
|
||||
return require("trouble.command").complete(...)
|
||||
end,
|
||||
desc = "Trouble",
|
||||
})
|
||||
require("trouble.view.main").setup()
|
||||
vim.schedule(function()
|
||||
for mode, mode_opts in pairs(options.modes) do
|
||||
if mode_opts.auto_open then
|
||||
require("trouble.view").new(M.get(mode))
|
||||
end
|
||||
end
|
||||
end)
|
||||
return options
|
||||
end
|
||||
|
||||
--- Update the default config.
|
||||
--- Should only be used by source to extend the default config.
|
||||
---@param config trouble.Config
|
||||
function M.defaults(config)
|
||||
options = vim.tbl_deep_extend("force", config, options)
|
||||
end
|
||||
|
||||
function M.modes()
|
||||
require("trouble.sources").load()
|
||||
local ret = {} ---@type string[]
|
||||
for k, v in pairs(options.modes) do
|
||||
if v.source or v.mode or v.sections then
|
||||
ret[#ret + 1] = k
|
||||
end
|
||||
end
|
||||
table.sort(ret)
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param ...? trouble.Config|string
|
||||
---@return trouble.Config
|
||||
function M.get(...)
|
||||
options = options or M.setup()
|
||||
|
||||
-- check if we need to load sources
|
||||
for i = 1, select("#", ...) do
|
||||
---@type trouble.Config?
|
||||
local opts = select(i, ...)
|
||||
if type(opts) == "string" or (type(opts) == "table" and opts.mode) then
|
||||
M.modes() -- trigger loading of sources
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
---@type trouble.Config[]
|
||||
local all = { {}, defaults, options or {} }
|
||||
|
||||
---@type table<string, boolean>
|
||||
local modes = {}
|
||||
local first_mode ---@type string?
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
---@type trouble.Config?
|
||||
local opts = select(i, ...)
|
||||
if type(opts) == "string" then
|
||||
opts = { mode = opts }
|
||||
end
|
||||
if opts then
|
||||
table.insert(all, opts)
|
||||
local idx = #all
|
||||
while opts.mode and not modes[opts.mode] do
|
||||
first_mode = first_mode or opts.mode
|
||||
modes[opts.mode or ""] = true
|
||||
opts = options.modes[opts.mode] or {}
|
||||
table.insert(all, idx, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ret = vim.tbl_deep_extend("force", unpack(all))
|
||||
|
||||
if type(ret.config) == "function" then
|
||||
ret.config(ret)
|
||||
end
|
||||
ret.mode = first_mode
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
return setmetatable(M, {
|
||||
__index = function(_, key)
|
||||
options = options or M.setup()
|
||||
assert(options, "should be setup")
|
||||
return options[key]
|
||||
end,
|
||||
})
|
||||
@ -0,0 +1,80 @@
|
||||
local M = {}
|
||||
|
||||
---@param t table<any, any>
|
||||
---@param dotted_key string
|
||||
---@param value any
|
||||
function M.dotset(t, dotted_key, value)
|
||||
local keys = vim.split(dotted_key, ".", { plain = true })
|
||||
for i = 1, #keys - 1 do
|
||||
local key = keys[i]
|
||||
t[key] = t[key] or {}
|
||||
if type(t[key]) ~= "table" then
|
||||
t[key] = {}
|
||||
end
|
||||
t = t[key]
|
||||
end
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
t[keys[#keys]] = value
|
||||
end
|
||||
|
||||
---@return {args: string[], opts: table<string, any>, errors: string[]}
|
||||
function M.parse(input)
|
||||
---@type string?, string?
|
||||
local positional, options = input:match("^%s*(.-)%s*([a-z%._]+%s*=.*)$")
|
||||
positional = positional or input
|
||||
positional = vim.trim(positional)
|
||||
local ret = {
|
||||
args = positional == "" and {} or vim.split(positional, "%s+"),
|
||||
opts = {},
|
||||
errors = {},
|
||||
}
|
||||
if not options then
|
||||
return ret
|
||||
end
|
||||
input = options
|
||||
local parser = vim.treesitter.get_string_parser(input, "lua")
|
||||
parser:parse()
|
||||
local query = vim.treesitter.query.parse(
|
||||
"lua",
|
||||
[[
|
||||
(ERROR) @error
|
||||
(assignment_statement (variable_list name: (_)) @name)
|
||||
(assignment_statement (expression_list value: (_)) @value)
|
||||
(_ value: (identifier) @global (#has-ancestor? @global expression_list))
|
||||
]]
|
||||
)
|
||||
---@type table<string, any>
|
||||
local env = {
|
||||
dotset = M.dotset,
|
||||
opts = ret.opts,
|
||||
}
|
||||
local lines = {} ---@type string[]
|
||||
local name = ""
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
for id, node in query:iter_captures(parser:trees()[1]:root(), input) do
|
||||
local capture = query.captures[id]
|
||||
local text = vim.treesitter.get_node_text(node, input)
|
||||
if capture == "name" then
|
||||
name = text
|
||||
elseif capture == "value" then
|
||||
table.insert(lines, ("dotset(opts, %q, %s)"):format(name, text))
|
||||
elseif capture == "global" then
|
||||
env[text] = text
|
||||
elseif capture == "error" then
|
||||
table.insert(ret.errors, text)
|
||||
end
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
local code = table.concat(lines, "\n")
|
||||
env.vim = vim
|
||||
-- selene: allow(incorrect_standard_library_use)
|
||||
local chunk = load(code, "trouble", "t", env)
|
||||
chunk()
|
||||
end)
|
||||
if not ok then
|
||||
table.insert(ret.errors, err)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,112 @@
|
||||
local Config = require("trouble.config")
|
||||
local Docs = require("lazy.docs")
|
||||
local LazyUtil = require("lazy.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.update()
|
||||
local config = Docs.extract("lua/trouble/config/init.lua", "\n(--@class trouble%.Mode.-\n})")
|
||||
config = config:gsub("%s*debug = false.\n", "\n")
|
||||
Docs.save({
|
||||
config = config,
|
||||
colors = Docs.colors({
|
||||
modname = "trouble.config.highlights",
|
||||
path = "lua/trouble/config/highlights.lua",
|
||||
name = "Trouble",
|
||||
}),
|
||||
modes = M.modes(),
|
||||
api = M.api(),
|
||||
})
|
||||
end
|
||||
|
||||
---@return ReadmeBlock
|
||||
function M.modes()
|
||||
---@type string[]
|
||||
local lines = {}
|
||||
|
||||
local exclude = { "fs", "todo" }
|
||||
local modes = Config.modes()
|
||||
for _, mode in ipairs(modes) do
|
||||
if not vim.tbl_contains(exclude, mode) then
|
||||
local m = Config.get(mode)
|
||||
lines[#lines + 1] = ("- **%s**: %s"):format(mode, m.desc or "")
|
||||
end
|
||||
end
|
||||
|
||||
return { content = table.concat(lines, "\n") }
|
||||
end
|
||||
|
||||
---@return ReadmeBlock
|
||||
function M.api()
|
||||
local lines = vim.split(LazyUtil.read_file("lua/trouble/api.lua"), "\n")
|
||||
|
||||
local funcs = {}
|
||||
|
||||
---@type string[]
|
||||
local f = {}
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
if line:match("^%-%-") then
|
||||
f[#f + 1] = line
|
||||
elseif line:match("^function") and not line:match("^function M%._") then
|
||||
f[#f + 1] = line:gsub("^function M", [[require("trouble")]])
|
||||
funcs[#funcs + 1] = table.concat(f, "\n")
|
||||
f = {}
|
||||
else
|
||||
f = {}
|
||||
end
|
||||
end
|
||||
|
||||
lines = vim.split(LazyUtil.read_file("lua/trouble/config/actions.lua"), "\n")
|
||||
f = {}
|
||||
---@type table<string, string>
|
||||
local comments = {}
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
if line:match("^%s*%-%-") then
|
||||
f[#f + 1] = line:gsub("^%s*[%-]*%s*", "")
|
||||
elseif line:match("^%s*[%w_]+ = function") then
|
||||
local name = line:match("^%s*([%w_]+)")
|
||||
if not name:match("^_") and #f > 0 then
|
||||
comments[name] = table.concat(f, "\n")
|
||||
end
|
||||
f = {}
|
||||
else
|
||||
f = {}
|
||||
end
|
||||
end
|
||||
local Actions = require("trouble.config.actions")
|
||||
local names = vim.tbl_keys(Actions)
|
||||
table.sort(names)
|
||||
|
||||
local exclude = { "close" }
|
||||
|
||||
for _, k in ipairs(names) do
|
||||
local desc = comments[k] or k:gsub("_", " ")
|
||||
local action = Actions[k]
|
||||
if type(Actions[k]) == "table" then
|
||||
desc = action.desc or desc
|
||||
action = action.action
|
||||
end
|
||||
desc = table.concat(
|
||||
vim.tbl_map(function(line)
|
||||
return ("-- %s"):format(line)
|
||||
end, vim.split(desc, "\n")),
|
||||
"\n"
|
||||
)
|
||||
if type(action) == "function" and not vim.tbl_contains(exclude, k) then
|
||||
funcs[#funcs + 1] = ([[
|
||||
%s
|
||||
---@param opts? trouble.Mode | { new? : boolean } | string
|
||||
---@return trouble.View
|
||||
require("trouble").%s(opts)]]):format(desc, k)
|
||||
end
|
||||
end
|
||||
return { content = table.concat(funcs, "\n\n"), lang = "lua" }
|
||||
end
|
||||
|
||||
M.update()
|
||||
print("Updated docs")
|
||||
-- M.api()
|
||||
|
||||
return M
|
||||
@ -0,0 +1,136 @@
|
||||
local Util = require("trouble.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class trouble.ViewFilter.opts
|
||||
---@field id? string
|
||||
---@field template? string
|
||||
---@field data? table<string, any>
|
||||
---@field toggle? boolean
|
||||
---@field del? boolean
|
||||
|
||||
---@class trouble.ViewFilter
|
||||
---@field id string
|
||||
---@field filter trouble.Filter
|
||||
---@field template? string
|
||||
---@field data? table<string, any>
|
||||
|
||||
---@param opts? {lines:boolean}
|
||||
---@param range trouble.Range
|
||||
---@param pos trouble.Pos
|
||||
function M.overlaps(pos, range, opts)
|
||||
if opts and opts.lines then
|
||||
return pos[1] >= range.pos[1] and pos[1] <= range.end_pos[1]
|
||||
else
|
||||
return (pos[1] > range.pos[1] or (pos[1] == range.pos[1] and pos[2] >= range.pos[2]))
|
||||
and (pos[1] < range.end_pos[1] or (pos[1] == range.end_pos[1] and pos[2] <= range.end_pos[2]))
|
||||
end
|
||||
end
|
||||
|
||||
---@alias trouble.Filter.ctx {opts:trouble.Config, main?:trouble.Main}
|
||||
---@alias trouble.FilterFn fun(item:trouble.Item, value: any, ctx:trouble.Filter.ctx): boolean
|
||||
---@class trouble.Filters: {[string]: trouble.FilterFn}
|
||||
M.filters = {
|
||||
buf = function(item, buf, ctx)
|
||||
if buf == 0 then
|
||||
return ctx.main and ctx.main.filename == item.filename or false
|
||||
end
|
||||
return item.buf == buf
|
||||
end,
|
||||
---@param fts string|string[]
|
||||
ft = function(item, fts, _)
|
||||
fts = type(fts) == "table" and fts or { fts }
|
||||
local ft = item.buf and vim.bo[item.buf].filetype
|
||||
return ft and vim.tbl_contains(fts, ft) or false
|
||||
end,
|
||||
range = function(item, buf, ctx)
|
||||
local main = ctx.main
|
||||
if not main or (main.buf ~= item.buf) then
|
||||
return false
|
||||
end
|
||||
local range = item.range --[[@as trouble.Range]]
|
||||
if range then
|
||||
return M.overlaps(main.cursor, range, { lines = true })
|
||||
else
|
||||
return M.overlaps(main.cursor, item, { lines = true })
|
||||
end
|
||||
end,
|
||||
["not"] = function(item, filter, ctx)
|
||||
---@cast filter trouble.Filter
|
||||
return not M.is(item, filter, ctx)
|
||||
end,
|
||||
any = function(item, any, ctx)
|
||||
---@cast any trouble.Filter[]
|
||||
for k, f in pairs(any) do
|
||||
if type(k) == "string" then
|
||||
f = { [k] = f }
|
||||
end
|
||||
if M.is(item, f, ctx) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
}
|
||||
|
||||
---@param item trouble.Item
|
||||
---@param filter trouble.Filter
|
||||
---@param ctx trouble.Filter.ctx
|
||||
function M.is(item, filter, ctx)
|
||||
if type(filter) == "table" and Util.islist(filter) then
|
||||
for _, f in ipairs(filter) do
|
||||
if not M.is(item, f, ctx) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
filter = type(filter) == "table" and filter or { filter }
|
||||
for k, v in pairs(filter) do
|
||||
---@type trouble.FilterFn?
|
||||
local filter_fn = ctx.opts.filters and ctx.opts.filters[k] or M.filters[k]
|
||||
if filter_fn then
|
||||
if not filter_fn(item, v, ctx) then
|
||||
return false
|
||||
end
|
||||
elseif type(k) == "number" then
|
||||
if type(v) == "function" then
|
||||
if not v(item) then
|
||||
return false
|
||||
end
|
||||
elseif not item[v] then
|
||||
return false
|
||||
end
|
||||
elseif type(v) == "table" then
|
||||
if not vim.tbl_contains(v, item[k]) then
|
||||
return false
|
||||
end
|
||||
elseif item[k] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param items trouble.Item[]
|
||||
---@param filter? trouble.Filter
|
||||
---@param ctx trouble.Filter.ctx
|
||||
function M.filter(items, filter, ctx)
|
||||
-- fast path for empty filter
|
||||
if not filter or (type(filter) == "table" and vim.tbl_isempty(filter)) then
|
||||
return items, {}
|
||||
end
|
||||
if type(filter) == "function" then
|
||||
return filter(items)
|
||||
end
|
||||
local ret = {} ---@type trouble.Item[]
|
||||
for _, item in ipairs(items) do
|
||||
if M.is(item, filter, ctx) then
|
||||
ret[#ret + 1] = item
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,253 @@
|
||||
local Cache = require("trouble.cache")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias trouble.spec.format string|trouble.Format|(string|trouble.Format)[]
|
||||
---@alias trouble.Format {text:string, hl?:string}
|
||||
|
||||
---@alias trouble.Formatter fun(ctx: trouble.Formatter.ctx): trouble.spec.format?
|
||||
---@alias trouble.Formatter.ctx {item: trouble.Item, node:trouble.Node, field:string, value:string, opts:trouble.Config}
|
||||
|
||||
---@param source string
|
||||
---@param field string
|
||||
function M.default_hl(source, field)
|
||||
if not source then
|
||||
return "Trouble" .. Util.camel(field)
|
||||
end
|
||||
local key = source .. field
|
||||
local value = Cache.default_hl[key]
|
||||
if value then
|
||||
return value
|
||||
end
|
||||
local hl = "Trouble" .. Util.camel(source) .. Util.camel(field)
|
||||
Cache.default_hl[key] = hl
|
||||
return hl
|
||||
end
|
||||
|
||||
---@type (fun(file: string, ext: string): string, string)[]
|
||||
local icons = {
|
||||
function(file)
|
||||
return require("mini.icons").get("file", file)
|
||||
end,
|
||||
function(file, ext)
|
||||
return require("nvim-web-devicons").get_icon(file, ext, { default = true })
|
||||
end,
|
||||
}
|
||||
function M.get_icon(file, ext)
|
||||
while #icons > 0 do
|
||||
local ok, icon, hl = pcall(icons[1], file, ext)
|
||||
if ok then
|
||||
return icon, hl
|
||||
end
|
||||
table.remove(icons, 1)
|
||||
end
|
||||
end
|
||||
|
||||
---@param fn trouble.Formatter
|
||||
---@param field string
|
||||
function M.cached_formatter(fn, field)
|
||||
local cache = {}
|
||||
---@param ctx trouble.Formatter.ctx
|
||||
return function(ctx)
|
||||
local key = ctx.item.source .. field .. (ctx.item[field] or "")
|
||||
local result = cache[key]
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
result = fn(ctx)
|
||||
cache[key] = result
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
---@type table<string, trouble.Formatter>
|
||||
M.formatters = {
|
||||
pos = function(ctx)
|
||||
return {
|
||||
text = "[" .. ctx.item.pos[1] .. ", " .. (ctx.item.pos[2] + 1) .. "]",
|
||||
}
|
||||
end,
|
||||
code = function(ctx)
|
||||
if not ctx.item.code or ctx.item.code == vim.NIL then
|
||||
return
|
||||
end
|
||||
return {
|
||||
text = "(" .. ctx.item.code .. ")",
|
||||
hl = "TroubleCode",
|
||||
}
|
||||
end,
|
||||
severity = function(ctx)
|
||||
local severity = ctx.item.severity or vim.diagnostic.severity.ERROR
|
||||
local name = vim.diagnostic.severity[severity] or "OTHER"
|
||||
return {
|
||||
text = name,
|
||||
hl = "Diagnostic" .. Util.camel(name:lower()),
|
||||
}
|
||||
end,
|
||||
severity_icon = function(ctx)
|
||||
local severity = ctx.item.severity or vim.diagnostic.severity.ERROR
|
||||
if not vim.diagnostic.severity[severity] then
|
||||
return
|
||||
end
|
||||
if type(severity) == "string" then
|
||||
severity = vim.diagnostic.severity[severity:upper()] or vim.diagnostic.severity.ERROR
|
||||
end
|
||||
local name = Util.camel(vim.diagnostic.severity[severity]:lower())
|
||||
local sign = vim.fn.sign_getdefined("DiagnosticSign" .. name)[1]
|
||||
if vim.fn.has("nvim-0.10.0") == 1 then
|
||||
local config = vim.diagnostic.config() or {}
|
||||
if config.signs == nil or type(config.signs) == "boolean" then
|
||||
return { text = sign and sign.text or name:sub(1, 1), hl = "DiagnosticSign" .. name }
|
||||
end
|
||||
local signs = config.signs or {}
|
||||
if type(signs) == "function" then
|
||||
signs = signs(0, 0) --[[@as vim.diagnostic.Opts.Signs]]
|
||||
end
|
||||
return {
|
||||
text = signs.text and signs.text[severity] or sign and sign.text or name:sub(1, 1),
|
||||
hl = "DiagnosticSign" .. name,
|
||||
}
|
||||
else
|
||||
return sign and { text = sign.text, hl = sign.texthl } or { text = name } or nil
|
||||
end
|
||||
end,
|
||||
file_icon = function(ctx)
|
||||
local item = ctx.item --[[@as Diagnostic|trouble.Item]]
|
||||
local file = vim.fn.fnamemodify(item.filename, ":t")
|
||||
local ext = vim.fn.fnamemodify(item.filename, ":e")
|
||||
local icon, color = M.get_icon(file, ext)
|
||||
return icon and { text = icon .. " ", hl = color } or ""
|
||||
end,
|
||||
count = function(ctx)
|
||||
return {
|
||||
text = (" %d "):format(ctx.node:count()),
|
||||
}
|
||||
end,
|
||||
filename = function(ctx)
|
||||
return {
|
||||
text = vim.fn.fnamemodify(ctx.item.filename, ":p:~:."),
|
||||
}
|
||||
end,
|
||||
dirname = function(ctx)
|
||||
return {
|
||||
text = vim.fn.fnamemodify(ctx.item.dirname, ":p:~:."),
|
||||
}
|
||||
end,
|
||||
filter = function(ctx)
|
||||
return {
|
||||
text = vim.inspect(ctx.item.filter):gsub("%s+", " "),
|
||||
hl = "ts.lua",
|
||||
}
|
||||
end,
|
||||
kind_icon = function(ctx)
|
||||
if not ctx.item.kind then
|
||||
return
|
||||
end
|
||||
local icon = ctx.opts.icons.kinds[ctx.item.kind]
|
||||
if icon then
|
||||
return {
|
||||
text = icon,
|
||||
hl = "TroubleIcon" .. ctx.item.kind,
|
||||
}
|
||||
end
|
||||
end,
|
||||
directory = function(ctx)
|
||||
if ctx.node:source() == "fs" then
|
||||
local directory = ctx.item.directory or ""
|
||||
local parent = ctx.node:parent_item()
|
||||
if parent and parent.directory then
|
||||
directory = directory:sub(#parent.directory + 1)
|
||||
return { text = directory, hl = "TroubleDirectory" }
|
||||
end
|
||||
return { text = vim.fn.fnamemodify(directory, ":~"), hl = "TroubleDirectory" }
|
||||
end
|
||||
end,
|
||||
directory_icon = function(ctx)
|
||||
if ctx.node:source() == "fs" then
|
||||
local text = ctx.node.folded and ctx.opts.icons.folder_closed or ctx.opts.icons.folder_open
|
||||
return { text = text, hl = "TroubleIconDirectory" }
|
||||
end
|
||||
end,
|
||||
}
|
||||
M.formatters.severity_icon = M.cached_formatter(M.formatters.severity_icon, "severity")
|
||||
M.formatters.severity = M.cached_formatter(M.formatters.severity, "severity")
|
||||
|
||||
---@param ctx trouble.Formatter.ctx
|
||||
function M.field(ctx)
|
||||
---@type trouble.Format[]
|
||||
local format = { { fi = ctx.field, text = vim.trim(tostring(ctx.item[ctx.field] or "")) } }
|
||||
|
||||
local opts = ctx.opts
|
||||
|
||||
local formatter = opts.formatters and opts.formatters[ctx.field] or M.formatters[ctx.field]
|
||||
|
||||
if formatter then
|
||||
local result = formatter(ctx)
|
||||
if not result then
|
||||
return
|
||||
end
|
||||
result = type(result) == "table" and Util.islist(result) and result or { result }
|
||||
format = {}
|
||||
---@cast result (string|trouble.Format)[]
|
||||
for _, f in ipairs(result) do
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
format[#format + 1] = type(f) == "string" and { text = f } or f
|
||||
end
|
||||
end
|
||||
for _, f in ipairs(format) do
|
||||
f.hl = f.hl or M.default_hl(ctx.item.source, ctx.field)
|
||||
end
|
||||
return format
|
||||
end
|
||||
|
||||
---@param format string
|
||||
---@param ctx {item: trouble.Item, node:trouble.Node, opts:trouble.Config}
|
||||
function M.format(format, ctx)
|
||||
---@type trouble.Format[]
|
||||
local ret = {}
|
||||
local hl ---@type string?
|
||||
while true do
|
||||
---@type string?,string,string
|
||||
local before, fields, after = format:match("^(.-){(.-)}(.*)$")
|
||||
if not before then
|
||||
break
|
||||
end
|
||||
format = after
|
||||
if #before > 0 then
|
||||
ret[#ret + 1] = { text = before, hl = hl }
|
||||
end
|
||||
|
||||
for _, field in Util.split(fields, "|") do
|
||||
---@type string,string
|
||||
local field_name, field_hl = field:match("^(.-):(.+)$")
|
||||
if field_name then
|
||||
field = field_name
|
||||
end
|
||||
if field == "hl" then
|
||||
hl = field_hl
|
||||
else
|
||||
---@cast ctx trouble.Formatter.ctx
|
||||
ctx.field = field
|
||||
ctx.value = ctx.item[field]
|
||||
local ff = M.field(ctx)
|
||||
if ff then
|
||||
for _, f in ipairs(ff) do
|
||||
if hl or field_hl then
|
||||
f.hl = field_hl or hl
|
||||
end
|
||||
ret[#ret + 1] = f
|
||||
end
|
||||
-- only render the first field
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if #format > 0 then
|
||||
ret[#ret + 1] = { text = format, hl = hl }
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,185 @@
|
||||
local Cache = require("trouble.cache")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@alias trouble.Pos {[1]:number, [2]:number}
|
||||
---@class trouble.Range
|
||||
---@field pos trouble.Pos
|
||||
---@field end_pos trouble.Pos
|
||||
|
||||
---@class trouble.Item: {[string]: any}
|
||||
---@field id? string
|
||||
---@field parent? trouble.Item
|
||||
---@field buf? number
|
||||
---@field filename string
|
||||
---@field pos trouble.Pos (1,0)-indexed
|
||||
---@field end_pos? trouble.Pos (1,0)-indexed
|
||||
---@field item table<string,any>
|
||||
---@field source string
|
||||
---@field cache table<string,any>
|
||||
---@field range? trouble.Range
|
||||
local M = {}
|
||||
|
||||
---@param opts trouble.Item | {filename?:string}
|
||||
function M.new(opts)
|
||||
local self = opts
|
||||
assert(self.source, "source is required")
|
||||
self.pos = self.pos or { 1, 0 }
|
||||
self.pos[1] = math.max(self.pos[1] or 1, 1)
|
||||
self.pos[2] = math.max(self.pos[2] or 0, 0)
|
||||
self.end_pos = self.end_pos or self.pos
|
||||
self.item = self.item or {}
|
||||
if self.buf and not self.filename then
|
||||
self.filename = vim.api.nvim_buf_get_name(self.buf)
|
||||
if self.filename == "" then
|
||||
self.filename = "[buffer:" .. self.buf .. "]"
|
||||
end
|
||||
end
|
||||
assert(self.filename, "filename is required")
|
||||
if self.filename then
|
||||
self.filename = vim.fs.normalize(self.filename)
|
||||
local parts = vim.split(self.filename, "/", { plain = true })
|
||||
self.basename = table.remove(parts)
|
||||
self.dirname = table.concat(parts, "/")
|
||||
end
|
||||
self.cache = Cache.new("item")
|
||||
return setmetatable(self, M)
|
||||
end
|
||||
|
||||
---@param items trouble.Item[]
|
||||
---@param fields? string[]
|
||||
function M.add_id(items, fields)
|
||||
for _, item in ipairs(items) do
|
||||
if not item.id then
|
||||
local id = {
|
||||
item.source,
|
||||
item.filename,
|
||||
item.pos[1] or "",
|
||||
item.pos[2] or "",
|
||||
item.end_pos[1] or "",
|
||||
item.end_pos[2] or "",
|
||||
}
|
||||
for _, field in ipairs(fields or {}) do
|
||||
table.insert(id, item[field] or "")
|
||||
end
|
||||
item.id = table.concat(id, ":")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function M:get_ft(buf)
|
||||
if self.buf and vim.api.nvim_buf_is_loaded(self.buf) then
|
||||
return vim.bo[self.buf].filetype
|
||||
end
|
||||
if not self.filename then
|
||||
return
|
||||
end
|
||||
local ft = Cache.ft[self.filename]
|
||||
if ft == nil then
|
||||
-- HACK: make sure we always pass a valid buf,
|
||||
-- otherwise some detectors will fail hard (like ts)
|
||||
ft = vim.filetype.match({ filename = self.filename, buf = buf or 0 })
|
||||
Cache.ft[self.filename] = ft or false -- cache misses too
|
||||
end
|
||||
return ft
|
||||
end
|
||||
|
||||
function M:get_lang(buf)
|
||||
local ft = self:get_ft(buf)
|
||||
return ft and ft ~= "" and vim.treesitter.language.get_lang(ft) or nil
|
||||
end
|
||||
|
||||
function M:__index(k)
|
||||
if type(k) ~= "string" then
|
||||
return
|
||||
end
|
||||
if M[k] then
|
||||
return M[k]
|
||||
end
|
||||
local item = rawget(self, "item")
|
||||
---@cast k string
|
||||
if item and item[k] ~= nil then
|
||||
return item[k]
|
||||
end
|
||||
|
||||
local obj = self
|
||||
|
||||
local start = 1
|
||||
while type(obj) == "table" do
|
||||
local dot = k:find(".", start, true)
|
||||
if not dot then
|
||||
if start == 1 then
|
||||
return
|
||||
end
|
||||
local ret = obj[k:sub(start)]
|
||||
rawset(self, k, ret)
|
||||
return ret
|
||||
end
|
||||
local key = k:sub(start, dot - 1)
|
||||
obj = obj[key]
|
||||
start = dot + 1
|
||||
end
|
||||
end
|
||||
|
||||
---@param item trouble.Item
|
||||
function M:add_child(item)
|
||||
item.parent = self
|
||||
end
|
||||
|
||||
---@param items trouble.Item[]
|
||||
---@param opts? {mode?:"range"|"full"|"after", multiline?:boolean}
|
||||
function M.add_text(items, opts)
|
||||
opts = opts or {}
|
||||
opts.mode = opts.mode or "range"
|
||||
local todo = {} ---@type table<string, {buf?:number, rows:number[]}>
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
if not item.item.text and item.filename then
|
||||
-- schedule to get the lines
|
||||
todo[item.filename] = todo[item.filename] or { rows = {} }
|
||||
todo[item.filename].buf = todo[item.filename].buf or item.buf
|
||||
for r = item.pos[1], item.end_pos and item.end_pos[1] or item.pos[1] do
|
||||
table.insert(todo[item.filename].rows, r)
|
||||
if not opts.multiline then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- get the lines and range text
|
||||
local buf_lines = {} ---@type table<string, table<number, string>>
|
||||
for path, t in pairs(todo) do
|
||||
buf_lines[path] = Util.get_lines({
|
||||
rows = t.rows,
|
||||
buf = t.buf,
|
||||
path = path,
|
||||
}) or {}
|
||||
end
|
||||
for _, item in ipairs(items) do
|
||||
if not item.item.text and item.filename then
|
||||
local lines = {} ---@type string[]
|
||||
for row = item.pos[1], item.end_pos[1] do
|
||||
local line = buf_lines[item.filename][row] or ""
|
||||
if row == item.pos[1] and row == item.end_pos[1] then
|
||||
if opts.mode == "after" then
|
||||
line = line:sub(item.pos[2] + 1)
|
||||
elseif opts.mode == "range" then
|
||||
line = line:sub(item.pos[2] + 1, item.end_pos[2])
|
||||
end
|
||||
elseif row == item.pos[1] then
|
||||
line = line:sub(item.pos[2] + 1)
|
||||
elseif row == item.end_pos[1] then
|
||||
line = line:sub(1, item.end_pos[2]) --[[@as string]]
|
||||
end
|
||||
if line ~= "" then
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
end
|
||||
item.item.text = table.concat(lines, "\n")
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,201 @@
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@alias trouble.Promise.state "pending" | "fulfilled" | "rejected"
|
||||
|
||||
---@class trouble.Promise
|
||||
---@field state trouble.Promise.state
|
||||
---@field value any?
|
||||
---@field queue (fun())[]
|
||||
---@field resolve fun(value)
|
||||
---@field reject fun(reason)
|
||||
---@field has_next boolean
|
||||
local P = {}
|
||||
P.__index = P
|
||||
|
||||
--- Creates a new promise
|
||||
---@param executor fun(resolve: fun(value), reject: fun(reason))
|
||||
---@return trouble.Promise
|
||||
function P.new(executor)
|
||||
local self = setmetatable({}, P)
|
||||
self.state = "pending"
|
||||
self.value = nil
|
||||
self.queue = {}
|
||||
self.has_next = false
|
||||
|
||||
---@param state trouble.Promise.state
|
||||
local function transition(state, result)
|
||||
if self.state == "pending" then
|
||||
self.state = state
|
||||
self.value = result
|
||||
for _, cb in ipairs(self.queue) do
|
||||
cb()
|
||||
end
|
||||
if state == "rejected" and not self.has_next then
|
||||
local bt = debug.traceback()
|
||||
vim.schedule(function()
|
||||
if not self.has_next then
|
||||
Util.error("Unhandled promise rejection:\n```lua\n" .. tostring(result) .. "\n\n" .. bt .. "```")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.resolve = function(value)
|
||||
transition("fulfilled", value)
|
||||
end
|
||||
self.reject = function(reason)
|
||||
transition("rejected", reason)
|
||||
end
|
||||
|
||||
xpcall(function()
|
||||
executor(self.resolve, self.reject)
|
||||
end, function(err)
|
||||
self.reject(err)
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds fulfillment and rejection handlers to the promise
|
||||
---@param on_fulfilled? fun(value):any
|
||||
---@param on_rejected? fun(reason):any
|
||||
---@return trouble.Promise
|
||||
function P:next(on_fulfilled, on_rejected)
|
||||
local next = P.new(function() end)
|
||||
|
||||
local function handle()
|
||||
local callback = on_fulfilled
|
||||
if self.state == "rejected" then
|
||||
callback = on_rejected
|
||||
end
|
||||
if callback then
|
||||
local ok, ret = pcall(callback, self.value)
|
||||
if ok then
|
||||
if ret and type(ret) == "table" and getmetatable(ret) == P then
|
||||
ret:next(next.resolve, next.reject)
|
||||
else
|
||||
next.resolve(ret)
|
||||
end
|
||||
else
|
||||
next.reject(ret) -- reject the next promise with the error
|
||||
end
|
||||
else
|
||||
if self.state == "fulfilled" then
|
||||
next.resolve(self.value)
|
||||
else
|
||||
next.reject(self.value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.state ~= "pending" then
|
||||
vim.schedule(handle) -- ensure the callback is called in the next event loop tick
|
||||
else
|
||||
table.insert(self.queue, handle)
|
||||
end
|
||||
|
||||
self.has_next = true -- self.has_rejection_handler or (on_rejected ~= nil)
|
||||
return next
|
||||
end
|
||||
|
||||
function P:catch(on_rejected)
|
||||
return self:next(nil, on_rejected)
|
||||
end
|
||||
|
||||
function P:finally(on_finally)
|
||||
return self:next(function(value)
|
||||
return P.new(function(resolve)
|
||||
on_finally()
|
||||
resolve(value)
|
||||
end)
|
||||
end, function(reason)
|
||||
return P.new(function(_, reject)
|
||||
on_finally()
|
||||
reject(reason)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function P:is_pending()
|
||||
return self.state == "pending"
|
||||
end
|
||||
|
||||
function P:timeout(ms)
|
||||
return P.new(function(resolve, reject)
|
||||
local timer = (vim.uv or vim.loop).new_timer()
|
||||
timer:start(ms, 0, function()
|
||||
timer:close()
|
||||
vim.schedule(function()
|
||||
reject("timeout")
|
||||
end)
|
||||
end)
|
||||
self:next(resolve, reject)
|
||||
end)
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.resolve(value)
|
||||
return P.new(function(resolve)
|
||||
resolve(value)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.reject(reason)
|
||||
return P.new(function(_, reject)
|
||||
reject(reason)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param promises trouble.Promise[]
|
||||
function M.all(promises)
|
||||
return P.new(function(resolve, reject)
|
||||
local results = {}
|
||||
local pending = #promises
|
||||
if pending == 0 then
|
||||
return resolve(results)
|
||||
end
|
||||
for i, promise in ipairs(promises) do
|
||||
promise:next(function(value)
|
||||
results[i] = value
|
||||
pending = pending - 1
|
||||
if pending == 0 then
|
||||
resolve(results)
|
||||
end
|
||||
end, reject)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param promises trouble.Promise[]
|
||||
function M.all_settled(promises)
|
||||
return P.new(function(resolve)
|
||||
local results = {}
|
||||
local pending = #promises
|
||||
if pending == 0 then
|
||||
return resolve(results)
|
||||
end
|
||||
for i, promise in ipairs(promises) do
|
||||
promise:next(function(value)
|
||||
results[i] = { status = "fulfilled", value = value }
|
||||
pending = pending - 1
|
||||
if pending == 0 then
|
||||
resolve(results)
|
||||
end
|
||||
end, function(reason)
|
||||
results[i] = { status = "rejected", reason = reason }
|
||||
pending = pending - 1
|
||||
if pending == 0 then
|
||||
resolve(results)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
M.new = P.new
|
||||
|
||||
-- M.new(function() end):timeout(1000)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,87 @@
|
||||
local Filter = require("trouble.filter")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias trouble.Sort.ctx {opts:trouble.Config, main?:trouble.Main}
|
||||
|
||||
---@type table<string, trouble.SorterFn>
|
||||
M.sorters = {
|
||||
pos = function(obj)
|
||||
-- Use large multipliers for higher priority fields to ensure their precedence in sorting
|
||||
local primaryScore = obj.pos[1] * 1000000 + obj.pos[2] * 1000
|
||||
local secondaryScore = obj.end_pos[1] * 1000000 + obj.end_pos[2] * 1000
|
||||
|
||||
return primaryScore + secondaryScore
|
||||
end,
|
||||
}
|
||||
|
||||
---@param items trouble.Item[]
|
||||
---@param opts? trouble.Sort[]
|
||||
---@param ctx trouble.Sort.ctx
|
||||
function M.sort(items, opts, ctx)
|
||||
if not opts or #opts == 0 then
|
||||
return items
|
||||
end
|
||||
|
||||
local keys = {} ---@type table<trouble.Item, any[]>
|
||||
local desc = {} ---@type boolean[]
|
||||
|
||||
-- pre-compute fields
|
||||
local fields = {} ---@type trouble.Sort[]
|
||||
for f, field in ipairs(opts) do
|
||||
if field.field then
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
local sorter = ctx.opts.sorters and ctx.opts.sorters[field.field] or M.sorters[field.field]
|
||||
if sorter then
|
||||
fields[f] = { sorter = sorter }
|
||||
else
|
||||
fields[f] = { field = field.field }
|
||||
end
|
||||
else
|
||||
fields[f] = field
|
||||
end
|
||||
desc[f] = field.desc or false
|
||||
end
|
||||
|
||||
-- pre-compute keys
|
||||
for _, item in ipairs(items) do
|
||||
local item_keys = {} ---@type any[]
|
||||
for f, field in ipairs(fields) do
|
||||
local key = nil
|
||||
if field.sorter then
|
||||
key = field.sorter(item)
|
||||
elseif field.field then
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
key = item[field.field]
|
||||
elseif field.filter then
|
||||
key = Filter.is(item, field.filter, ctx)
|
||||
end
|
||||
if type(key) == "boolean" then
|
||||
key = key and 0 or 1
|
||||
end
|
||||
item_keys[f] = key
|
||||
end
|
||||
keys[item] = item_keys
|
||||
end
|
||||
|
||||
-- sort items
|
||||
table.sort(items, function(a, b)
|
||||
local ka = keys[a]
|
||||
local kb = keys[b]
|
||||
for i = 1, #ka do
|
||||
local fa = ka[i]
|
||||
local fb = kb[i]
|
||||
if fa ~= fb then
|
||||
if desc[i] then
|
||||
return fa > fb
|
||||
else
|
||||
return fa < fb
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end)
|
||||
return items
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,130 @@
|
||||
---@diagnostic disable: inject-field
|
||||
local Item = require("trouble.item")
|
||||
|
||||
---@class trouble.Source.diagnostics: trouble.Source
|
||||
local M = {}
|
||||
|
||||
M.highlights = {
|
||||
Message = "TroubleText",
|
||||
ItemSource = "Comment",
|
||||
Code = "Comment",
|
||||
}
|
||||
|
||||
M.config = {
|
||||
modes = {
|
||||
diagnostics = {
|
||||
desc = "diagnostics",
|
||||
events = { "DiagnosticChanged", "BufEnter" },
|
||||
-- Trouble classic for other buffers,
|
||||
-- but only if they are in the current directory
|
||||
source = "diagnostics",
|
||||
groups = {
|
||||
-- { format = "{hl:Special} {hl} {hl:Title}Diagnostics{hl} {count}" },
|
||||
-- { "severity", format = "{severity_icon} {severity} {count}" },
|
||||
-- { "dirname", format = "{hl:Special} {hl} {dirname} {count}" },
|
||||
{ "directory" },
|
||||
{ "filename", format = "{file_icon} {basename} {count}" },
|
||||
},
|
||||
sort = { "severity", "filename", "pos", "message" },
|
||||
format = "{severity_icon} {message:md} {item.source} {code} {pos}",
|
||||
-- filter = {
|
||||
-- ["not"] = {
|
||||
-- any = {
|
||||
-- { severity = vim.diagnostic.severity.ERROR },
|
||||
-- { buf = 0 },
|
||||
-- },
|
||||
-- },
|
||||
-- function(item)
|
||||
-- return item.filename:find((vim.loop or vim.uv).cwd(), 1, true)
|
||||
-- end,
|
||||
-- },
|
||||
},
|
||||
-- {
|
||||
-- -- error from all files
|
||||
-- source = "diagnostics",
|
||||
-- groups = { "severity", "code", "filename" },
|
||||
-- filter = {
|
||||
-- -- severity = 1,
|
||||
-- },
|
||||
-- sort = { "filename", "pos" },
|
||||
-- format = "sig {severity_sign} {severity} file: {filename} pos: {pos}",
|
||||
-- },
|
||||
-- {
|
||||
-- -- diagnostics from current buffer
|
||||
-- source = "diagnostics",
|
||||
-- groups = { "severity", "filename" },
|
||||
-- filter = {
|
||||
-- buf = 0,
|
||||
-- },
|
||||
-- sort = { "pos" },
|
||||
-- },
|
||||
},
|
||||
}
|
||||
|
||||
---@type table<number, trouble.Item[]>
|
||||
local cache = {}
|
||||
|
||||
function M.setup()
|
||||
vim.api.nvim_create_autocmd("DiagnosticChanged", {
|
||||
group = vim.api.nvim_create_augroup("trouble.diagnostics", { clear = true }),
|
||||
callback = function(event)
|
||||
-- NOTE: unfortunately, we can't use the event.data.diagnostics table here,
|
||||
-- since multiple namespaces exist and we can't tell which namespace the
|
||||
-- diagnostics are from.
|
||||
cache[event.buf] = vim.tbl_map(M.item, vim.diagnostic.get(event.buf))
|
||||
cache[0] = nil
|
||||
end,
|
||||
})
|
||||
for _, diag in ipairs(vim.diagnostic.get()) do
|
||||
local buf = diag.bufnr
|
||||
if buf and vim.api.nvim_buf_is_valid(buf) then
|
||||
cache[buf] = cache[buf] or {}
|
||||
table.insert(cache[buf], M.item(diag))
|
||||
Item.add_id(cache[buf], { "item.source", "severity", "code" })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param diag vim.Diagnostic
|
||||
function M.item(diag)
|
||||
return Item.new({
|
||||
source = "diagnostics",
|
||||
buf = diag.bufnr,
|
||||
pos = { diag.lnum + 1, diag.col },
|
||||
end_pos = { diag.end_lnum and (diag.end_lnum + 1) or nil, diag.end_col },
|
||||
item = diag,
|
||||
})
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param ctx trouble.Source.ctx)
|
||||
function M.get(cb, ctx)
|
||||
-- PERF: pre-filter when possible
|
||||
local buf = type(ctx.opts.filter) == "table" and ctx.opts.filter.buf or nil
|
||||
|
||||
if buf == 0 then
|
||||
buf = ctx.main.buf
|
||||
end
|
||||
|
||||
if buf then
|
||||
cb(cache[buf] or {})
|
||||
else
|
||||
if not cache[0] then
|
||||
cache[0] = {}
|
||||
for b, items in pairs(cache) do
|
||||
if b ~= 0 then
|
||||
if vim.api.nvim_buf_is_valid(b) then
|
||||
for _, item in ipairs(items) do
|
||||
table.insert(cache[0], item)
|
||||
end
|
||||
else
|
||||
cache[b] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
cb(cache[0])
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,128 @@
|
||||
---@diagnostic disable: inject-field
|
||||
local Item = require("trouble.item")
|
||||
|
||||
---Represents an item in a Neovim quickfix/loclist.
|
||||
---@class fzf.Item
|
||||
---@field stripped string the fzf item without any highlighting.
|
||||
---@field bufnr? number The buffer number of the item.
|
||||
---@field bufname? string
|
||||
---@field terminal? boolean
|
||||
---@field path string
|
||||
---@field uri? string
|
||||
---@field line number 1-indexed line number
|
||||
---@field col number 1-indexed column number
|
||||
|
||||
---@class fzf.Opts
|
||||
|
||||
---@class trouble.Source.fzf: trouble.Source
|
||||
local M = {}
|
||||
|
||||
---@type trouble.Item[]
|
||||
M.items = {}
|
||||
|
||||
M.config = {
|
||||
modes = {
|
||||
fzf = {
|
||||
desc = "FzfLua results previously opened with `require('trouble.sources.fzf').open()`.",
|
||||
source = "fzf",
|
||||
groups = {
|
||||
{ "cmd", format = "{hl:Title}fzf{hl} {cmd:Comment} {count}" },
|
||||
{ "filename", format = "{file_icon} {filename} {count}" },
|
||||
},
|
||||
sort = { "filename", "pos" },
|
||||
format = "{text:ts} {pos}",
|
||||
},
|
||||
fzf_files = {
|
||||
desc = "FzfLua results previously opened with `require('trouble.sources.fzf').open()`.",
|
||||
source = "fzf",
|
||||
groups = {
|
||||
{ "cmd", format = "{hl:Title}fzf{hl} {cmd:Comment} {count}" },
|
||||
},
|
||||
sort = { "filename", "pos" },
|
||||
format = "{file_icon} {filename}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@param item fzf.Item
|
||||
function M.item(item)
|
||||
item.text = item.stripped:match(":%d+:%d?%d?%d?%d?:?(.*)$")
|
||||
local word = item.text and item.text:sub(item.col):match("%S+")
|
||||
return Item.new({
|
||||
source = "fzf",
|
||||
buf = item.bufnr,
|
||||
filename = item.bufname or item.path or item.uri,
|
||||
pos = { item.line, item.col - 1 },
|
||||
end_pos = word and { item.line, item.col - 1 + #word } or nil,
|
||||
item = item,
|
||||
})
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param _ctx trouble.Source.ctx)
|
||||
function M.get(cb, _ctx)
|
||||
cb(M.items)
|
||||
end
|
||||
|
||||
-- Returns the mode based on the items.
|
||||
function M.mode()
|
||||
for _, item in ipairs(M.items) do
|
||||
if item.text then
|
||||
return "fzf"
|
||||
end
|
||||
end
|
||||
return "fzf_files"
|
||||
end
|
||||
|
||||
-- Append the current fzf buffer to the trouble list.
|
||||
---@param selected string[]
|
||||
---@param fzf_opts fzf.Opts
|
||||
---@param opts? trouble.Mode|string
|
||||
function M.add(selected, fzf_opts, opts)
|
||||
local cmd = fzf_opts.__INFO.cmd
|
||||
local path = require("fzf-lua.path")
|
||||
for _, line in ipairs(selected) do
|
||||
local item = M.item(path.entry_to_file(line, fzf_opts))
|
||||
item.item.cmd = cmd
|
||||
table.insert(M.items, item)
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
opts = opts or {}
|
||||
if type(opts) == "string" then
|
||||
opts = { mode = opts }
|
||||
end
|
||||
opts = vim.tbl_extend("force", { mode = M.mode() }, opts)
|
||||
require("trouble").open(opts)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Opens the current fzf buffer in the trouble list.
|
||||
-- This will clear the existing items.
|
||||
---@param selected string[]
|
||||
---@param fzf_opts fzf.Opts
|
||||
---@param opts? trouble.Mode|string
|
||||
function M.open(selected, fzf_opts, opts)
|
||||
M.items = {}
|
||||
M.add(selected, fzf_opts, opts)
|
||||
end
|
||||
|
||||
local smart_prefix = require("trouble.util").is_win() and "transform(IF %FZF_SELECT_COUNT% LEQ 0 (echo select-all))"
|
||||
or "transform([ $FZF_SELECT_COUNT -eq 0 ] && echo select-all)"
|
||||
|
||||
M.actions = {
|
||||
-- Open selected or all items in the trouble list.
|
||||
open = { fn = M.open, prefix = smart_prefix, desc = "smart-open-with-trouble" },
|
||||
-- Open selected items in the trouble list.
|
||||
open_selected = { fn = M.open, desc = "open-with-trouble" },
|
||||
-- Open all items in the trouble list.
|
||||
open_all = { fn = M.open, prefix = "select-all", desc = "open-all-with-trouble" },
|
||||
-- Add selected or all items to the trouble list.
|
||||
add = { fn = M.add, prefix = smart_prefix, desc = "smart-add-to-trouble" },
|
||||
-- Add selected items to the trouble list.
|
||||
add_selected = { fn = M.add, desc = "add-to-trouble" },
|
||||
-- Add all items to the trouble list.
|
||||
add_all = { fn = M.add, prefix = "select-all", desc = "add-all-to-trouble" },
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,63 @@
|
||||
local Config = require("trouble.config")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@class trouble.Source
|
||||
---@field highlights? table<string, string>
|
||||
---@field config? trouble.Config
|
||||
---@field setup? fun()
|
||||
---@field get trouble.Source.get|table<string, trouble.Source.get>
|
||||
|
||||
---@alias trouble.Source.ctx {main: trouble.Main, opts:trouble.Mode}
|
||||
---@alias trouble.Source.Callback fun(items:trouble.Item[])
|
||||
---@alias trouble.Source.get fun(cb:trouble.Source.Callback, ctx:trouble.Source.ctx)
|
||||
|
||||
local M = {}
|
||||
---@type table<string, trouble.Source>
|
||||
M.sources = {}
|
||||
|
||||
---@param name string
|
||||
---@param source? trouble.Source
|
||||
function M.register(name, source)
|
||||
if M.sources[name] then
|
||||
error("source already registered: " .. name)
|
||||
end
|
||||
source = source or require("trouble.sources." .. name)
|
||||
if source then
|
||||
if source.setup then
|
||||
source.setup()
|
||||
end
|
||||
require("trouble.config.highlights").source(name, source.highlights)
|
||||
if source.config then
|
||||
Config.defaults(source.config)
|
||||
end
|
||||
end
|
||||
M.sources[name] = source
|
||||
return source
|
||||
end
|
||||
|
||||
---@param source string
|
||||
function M.get(source)
|
||||
local parent, child = source:match("^(.-)%.(.*)$")
|
||||
source = parent or source
|
||||
local s = M.sources[source] or M.register(source)
|
||||
if child and type(s.get) ~= "table" then
|
||||
error("source does not support sub-sources: " .. source)
|
||||
elseif child and type(s.get[child]) ~= "function" then
|
||||
error("source does not support sub-source: " .. source .. "." .. child)
|
||||
end
|
||||
return child and s.get[child] or s.get
|
||||
end
|
||||
|
||||
function M.load()
|
||||
local rtp = vim.api.nvim_get_runtime_file("lua/trouble/sources/*.lua", true)
|
||||
for _, file in ipairs(rtp) do
|
||||
local name = file:match("lua[/\\]trouble[/\\]sources[/\\](.*)%.lua")
|
||||
if name and name ~= "init" and not M.sources[name] then
|
||||
Util.try(function()
|
||||
M.register(name)
|
||||
end, { msg = "Error loading source: " .. name })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,536 @@
|
||||
local Cache = require("trouble.cache")
|
||||
local Config = require("trouble.config")
|
||||
local Filter = require("trouble.filter")
|
||||
local Item = require("trouble.item")
|
||||
local Promise = require("trouble.promise")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@param line string line to be indexed
|
||||
---@param index integer UTF index
|
||||
---@param encoding string utf-8|utf-16|utf-32| defaults to utf-16
|
||||
---@return integer byte (utf-8) index of `encoding` index `index` in `line`
|
||||
local function get_line_col(line, index, encoding)
|
||||
local ok, ret = pcall(vim.lsp.util._str_byteindex_enc, line, index, encoding)
|
||||
return ok and ret or #line
|
||||
end
|
||||
|
||||
---@class trouble.Source.lsp: trouble.Source
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
vim.api.nvim_create_autocmd({ "LspAttach", "LspDetach" }, {
|
||||
group = vim.api.nvim_create_augroup("trouble.lsp.dattach", { clear = true }),
|
||||
callback = function()
|
||||
Cache.symbols:clear()
|
||||
Cache.locations:clear()
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd({ "BufDelete", "TextChanged", "TextChangedI" }, {
|
||||
group = vim.api.nvim_create_augroup("trouble.lsp.buf", { clear = true }),
|
||||
callback = function(ev)
|
||||
local buf = ev.buf
|
||||
Cache.symbols[buf] = nil
|
||||
if vim.api.nvim_buf_is_valid(ev.buf) and vim.api.nvim_buf_is_loaded(ev.buf) and vim.bo[ev.buf].buftype == "" then
|
||||
Cache.locations:clear()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
M.config = {
|
||||
modes = {
|
||||
lsp_document_symbols = {
|
||||
title = "{hl:Title}Document Symbols{hl} {count}",
|
||||
desc = "document symbols",
|
||||
events = {
|
||||
"BufEnter",
|
||||
-- symbols are cached on changedtick,
|
||||
-- so it's ok to refresh often
|
||||
{ event = "TextChanged", main = true },
|
||||
{ event = "CursorMoved", main = true },
|
||||
{ event = "LspAttach", main = true },
|
||||
},
|
||||
source = "lsp.document_symbols",
|
||||
groups = {
|
||||
{ "filename", format = "{file_icon} {filename} {count}" },
|
||||
},
|
||||
sort = { "filename", "pos", "text" },
|
||||
-- sort = { { buf = 0 }, { kind = "Function" }, "filename", "pos", "text" },
|
||||
format = "{kind_icon} {symbol.name} {text:Comment} {pos}",
|
||||
},
|
||||
lsp_base = {
|
||||
events = {
|
||||
"BufEnter",
|
||||
{ event = "CursorHold", main = true },
|
||||
{ event = "LspAttach", main = true },
|
||||
},
|
||||
groups = {
|
||||
{ "filename", format = "{file_icon} {filename} {count}" },
|
||||
},
|
||||
sort = { "filename", "pos", "text" },
|
||||
format = "{text:ts} ({item.client}) {pos}",
|
||||
},
|
||||
lsp = {
|
||||
desc = "LSP definitions, references, implementations, type definitions, and declarations",
|
||||
sections = {
|
||||
"lsp_definitions",
|
||||
"lsp_references",
|
||||
"lsp_implementations",
|
||||
"lsp_type_definitions",
|
||||
"lsp_declarations",
|
||||
"lsp_incoming_calls",
|
||||
"lsp_outgoing_calls",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, mode in ipairs({ "incoming_calls", "outgoing_calls" }) do
|
||||
M.config.modes["lsp_" .. mode] = {
|
||||
mode = "lsp_base",
|
||||
title = "{hl:Title}" .. Util.camel(mode, " ") .. "{hl} {count}",
|
||||
desc = Util.camel(mode, " "),
|
||||
source = "lsp." .. mode,
|
||||
format = "{kind_icon} {text:ts} {pos} {hl:Title}{item.client:Title}{hl}",
|
||||
}
|
||||
end
|
||||
|
||||
for _, mode in ipairs({ "definitions", "references", "implementations", "type_definitions", "declarations", "command" }) do
|
||||
M.config.modes["lsp_" .. mode] = {
|
||||
auto_jump = true,
|
||||
mode = "lsp_base",
|
||||
title = "{hl:Title}" .. Util.camel(mode, " ") .. "{hl} {count}",
|
||||
source = "lsp." .. mode,
|
||||
desc = Util.camel(mode, " "):lower(),
|
||||
}
|
||||
end
|
||||
|
||||
---@class trouble.lsp.Response<R,P>: {client: vim.lsp.Client, result: R, err: lsp.ResponseError, params: P}
|
||||
|
||||
---@param method string
|
||||
---@param params? table
|
||||
---@param opts? {client?:vim.lsp.Client}
|
||||
function M.request(method, params, opts)
|
||||
opts = opts or {}
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
---@type vim.lsp.Client[]
|
||||
local clients = {}
|
||||
|
||||
if opts.client then
|
||||
clients = { opts.client }
|
||||
else
|
||||
if vim.lsp.get_clients then
|
||||
clients = vim.lsp.get_clients({ method = method, bufnr = buf })
|
||||
else
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
clients = vim.lsp.get_active_clients({ bufnr = buf })
|
||||
---@param client vim.lsp.Client
|
||||
clients = vim.tbl_filter(function(client)
|
||||
return client.supports_method(method)
|
||||
end, clients)
|
||||
end
|
||||
end
|
||||
|
||||
---@param client vim.lsp.Client
|
||||
return Promise.all(vim.tbl_map(function(client)
|
||||
return Promise.new(function(resolve)
|
||||
client.request(method, params, function(err, result)
|
||||
resolve({ client = client, result = result, err = err, params = params })
|
||||
end, buf)
|
||||
end)
|
||||
end, clients)):next(function(results)
|
||||
---@param v trouble.lsp.Response<any,any>
|
||||
return vim.tbl_filter(function(v)
|
||||
return v.result
|
||||
end, results)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param method string
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param ctx trouble.Source.ctx
|
||||
---@param opts? {context?:any, params?:table<string,any>}
|
||||
function M.get_locations(method, cb, ctx, opts)
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local cursor = vim.api.nvim_win_get_cursor(win)
|
||||
local col = cursor[2]
|
||||
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
while col > 1 and vim.fn.strcharpart(line, col - 1, 1):match("^[a-zA-Z_]$") do
|
||||
col = col - 1
|
||||
end
|
||||
|
||||
opts = opts or {}
|
||||
---@type lsp.TextDocumentPositionParams
|
||||
local params = opts.params or vim.lsp.util.make_position_params(win)
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
params.context = params.context or opts.context or nil
|
||||
|
||||
local id = table.concat({ buf, cursor[1], col, method, vim.inspect(params) }, "-")
|
||||
if Cache.locations[id] then
|
||||
return cb(Cache.locations[id])
|
||||
end
|
||||
|
||||
M.request(method, params):next(
|
||||
---@param results trouble.lsp.Response<lsp.Loc>[]
|
||||
function(results)
|
||||
local items = {} ---@type trouble.Item[]
|
||||
for _, resp in ipairs(results) do
|
||||
vim.list_extend(items, M.get_items(resp.client, resp.result, ctx.opts.params))
|
||||
end
|
||||
Cache.locations[id] = items
|
||||
cb(items)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
M.get = {}
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
function M.get.document_symbols(cb)
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
---@type trouble.Item[]
|
||||
local ret = Cache.symbols[buf]
|
||||
|
||||
if ret then
|
||||
return cb(ret)
|
||||
end
|
||||
|
||||
---@type lsp.DocumentSymbolParams
|
||||
local params = { textDocument = vim.lsp.util.make_text_document_params() }
|
||||
|
||||
---@alias lsp.Symbol lsp.SymbolInformation|lsp.DocumentSymbol
|
||||
|
||||
M.request("textDocument/documentSymbol", params):next(
|
||||
---@param results trouble.lsp.Response<lsp.SymbolInformation[]|lsp.DocumentSymbol[]>[]
|
||||
function(results)
|
||||
if vim.tbl_isempty(results) then
|
||||
return cb({})
|
||||
end
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
return
|
||||
end
|
||||
local items = {} ---@type trouble.Item[]
|
||||
|
||||
for _, res in ipairs(results) do
|
||||
vim.list_extend(items, M.results_to_items(res.client, res.result, params.textDocument.uri))
|
||||
end
|
||||
Item.add_text(items, { mode = "after" })
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
Cache.symbols[buf] = items
|
||||
cb(items)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
function M.call_hierarchy(cb, incoming)
|
||||
---@type lsp.CallHierarchyPrepareParams
|
||||
local params = vim.lsp.util.make_position_params()
|
||||
|
||||
M.request("textDocument/prepareCallHierarchy", params)
|
||||
:next(
|
||||
---@param results trouble.lsp.Response<lsp.CallHierarchyItem[]>[]
|
||||
function(results)
|
||||
local requests = {} ---@type trouble.Promise[]
|
||||
for _, res in ipairs(results or {}) do
|
||||
for _, chi in ipairs(res.result) do
|
||||
requests[#requests + 1] = M.request(
|
||||
("callHierarchy/%sCalls"):format(incoming and "incoming" or "outgoing"),
|
||||
{ item = chi },
|
||||
{ client = res.client }
|
||||
)
|
||||
end
|
||||
end
|
||||
return Promise.all(requests)
|
||||
end
|
||||
)
|
||||
:next(
|
||||
---@param responses trouble.lsp.Response<(lsp.CallHierarchyIncomingCall|lsp.CallHierarchyOutgoingCall)[]>[][]
|
||||
function(responses)
|
||||
local items = {} ---@type trouble.Item[]
|
||||
|
||||
for _, results in ipairs(responses) do
|
||||
for _, res in ipairs(results) do
|
||||
local client = res.client
|
||||
local calls = res.result
|
||||
local todo = {} ---@type lsp.ResultItem[]
|
||||
|
||||
for _, call in ipairs(calls) do
|
||||
todo[#todo + 1] = call.to or call.from
|
||||
end
|
||||
vim.list_extend(items, M.results_to_items(client, todo))
|
||||
end
|
||||
end
|
||||
Item.add_text(items, { mode = "after" })
|
||||
|
||||
if incoming then
|
||||
-- for incoming calls, we actually want the call locations, not just the caller
|
||||
-- but we use the caller's item text as the call location text
|
||||
local texts = {} ---@type table<lsp.CallHierarchyItem, string>
|
||||
for _, item in ipairs(items) do
|
||||
texts[item.item.symbol] = item.item.text
|
||||
end
|
||||
|
||||
items = {}
|
||||
for _, results in ipairs(responses) do
|
||||
for _, res in ipairs(results) do
|
||||
local client = res.client
|
||||
local calls = res.result
|
||||
local todo = {} ---@type lsp.ResultItem[]
|
||||
|
||||
for _, call in ipairs(calls) do
|
||||
for _, r in ipairs(call.fromRanges or {}) do
|
||||
local t = vim.deepcopy(call.from) --[[@as lsp.ResultItem]]
|
||||
t.location = { range = r or call.from.selectionRange or call.from.range, uri = call.from.uri }
|
||||
t.text = texts[call.from]
|
||||
todo[#todo + 1] = t
|
||||
end
|
||||
end
|
||||
vim.list_extend(items, M.results_to_items(client, todo))
|
||||
end
|
||||
end
|
||||
end
|
||||
cb(items)
|
||||
end
|
||||
)
|
||||
-- :catch(Util.error)
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
function M.get.incoming_calls(cb)
|
||||
M.call_hierarchy(cb, true)
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
function M.get.outgoing_calls(cb)
|
||||
M.call_hierarchy(cb, false)
|
||||
end
|
||||
|
||||
---@param client vim.lsp.Client
|
||||
---@param locations? lsp.Location[]|lsp.LocationLink[]|lsp.Location
|
||||
---@param opts? {include_current?:boolean}
|
||||
function M.get_items(client, locations, opts)
|
||||
opts = opts or {}
|
||||
locations = locations or {}
|
||||
locations = Util.islist(locations) and locations or { locations }
|
||||
---@cast locations (lsp.Location|lsp.LocationLink)[]
|
||||
|
||||
locations = vim.list_slice(locations, 1, Config.max_items)
|
||||
|
||||
local items = M.locations_to_items(client, locations)
|
||||
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local fname = vim.api.nvim_buf_get_name(0)
|
||||
fname = vim.fs.normalize(fname)
|
||||
|
||||
if not opts.include_current then
|
||||
---@param item trouble.Item
|
||||
items = vim.tbl_filter(function(item)
|
||||
return not (item.filename == fname and Filter.overlaps(cursor, item, { lines = true }))
|
||||
end, items)
|
||||
end
|
||||
|
||||
-- Item.add_text(items, { mode = "full" })
|
||||
return items
|
||||
end
|
||||
|
||||
---@alias lsp.Loc lsp.Location|lsp.LocationLink
|
||||
---@param client vim.lsp.Client
|
||||
---@param locs lsp.Loc[]
|
||||
---@return trouble.Item[]
|
||||
function M.locations_to_items(client, locs)
|
||||
local ranges = M.locations_to_ranges(client, locs)
|
||||
---@param range trouble.Range.lsp
|
||||
return vim.tbl_map(function(range)
|
||||
return M.range_to_item(client, range)
|
||||
end, vim.tbl_values(ranges))
|
||||
end
|
||||
|
||||
---@param client vim.lsp.Client
|
||||
---@param range trouble.Range.lsp
|
||||
---@return trouble.Item
|
||||
function M.range_to_item(client, range)
|
||||
return Item.new({
|
||||
buf = range.buf,
|
||||
filename = range.filename,
|
||||
pos = range.pos,
|
||||
end_pos = range.end_pos,
|
||||
source = "lsp",
|
||||
item = {
|
||||
client_id = client.id,
|
||||
client = client.name,
|
||||
location = range.location,
|
||||
text = range.line and vim.trim(range.line) or nil,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
---@alias lsp.ResultItem lsp.Symbol|lsp.CallHierarchyItem|{text?:string}
|
||||
---@param client vim.lsp.Client
|
||||
---@param results lsp.ResultItem[]
|
||||
---@param default_uri? string
|
||||
function M.results_to_items(client, results, default_uri)
|
||||
local items = {} ---@type trouble.Item[]
|
||||
local locs = {} ---@type lsp.Loc[]
|
||||
local processed = {} ---@type table<lsp.ResultItem, {uri:string, loc:lsp.Loc, range?:lsp.Loc}>
|
||||
|
||||
---@param result lsp.ResultItem
|
||||
local function process(result)
|
||||
local uri = result.location and result.location.uri or result.uri or default_uri
|
||||
local loc = result.location or { range = result.selectionRange or result.range, uri = uri }
|
||||
loc.uri = loc.uri or uri
|
||||
assert(loc.uri, "missing uri in result:\n" .. vim.inspect(result))
|
||||
-- the range enclosing this symbol. Useful to get the symbol of the current cursor position
|
||||
---@type lsp.Location?
|
||||
local range = result.range and { range = result.range, uri = uri } or nil
|
||||
processed[result] = { uri = uri, loc = loc, range = range }
|
||||
locs[#locs + 1] = loc
|
||||
if range then
|
||||
locs[#locs + 1] = range
|
||||
end
|
||||
for _, child in ipairs(result.children or {}) do
|
||||
process(child)
|
||||
end
|
||||
end
|
||||
|
||||
for _, result in ipairs(results) do
|
||||
process(result)
|
||||
end
|
||||
|
||||
local ranges = M.locations_to_ranges(client, locs)
|
||||
|
||||
---@param result lsp.ResultItem
|
||||
local function add(result)
|
||||
local loc = processed[result].loc
|
||||
local range = processed[result].range
|
||||
|
||||
local item = M.range_to_item(client, ranges[loc])
|
||||
local id = { item.buf, item.pos[1], item.pos[2], item.end_pos[1], item.end_pos[2], item.kind }
|
||||
item.id = table.concat(id, "|")
|
||||
-- item.text = nil
|
||||
-- the range enclosing this symbol. Useful to get the symbol of the current cursor position
|
||||
item.range = range and ranges[range] or nil
|
||||
item.item.kind = vim.lsp.protocol.SymbolKind[result.kind] or tostring(result.kind)
|
||||
item.item.symbol = result
|
||||
item.item.text = result.text
|
||||
items[#items + 1] = item
|
||||
for _, child in ipairs(result.children or {}) do
|
||||
item:add_child(add(child))
|
||||
end
|
||||
result.children = nil
|
||||
return item
|
||||
end
|
||||
|
||||
for _, result in ipairs(results) do
|
||||
add(result)
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
---@class trouble.Range.lsp: trouble.Range
|
||||
---@field buf? number
|
||||
---@field filename string
|
||||
---@field location lsp.Loc
|
||||
---@field client vim.lsp.Client
|
||||
---@field line string
|
||||
|
||||
---@param client vim.lsp.Client
|
||||
---@param locs lsp.Loc[]
|
||||
function M.locations_to_ranges(client, locs)
|
||||
local todo = {} ---@type table<string, {locs:lsp.Loc[], rows:table<number,number>}>
|
||||
for _, d in ipairs(locs) do
|
||||
local uri = d.uri or d.targetUri
|
||||
local range = d.range or d.targetSelectionRange
|
||||
todo[uri] = todo[uri] or { locs = {}, rows = {} }
|
||||
table.insert(todo[uri].locs, d)
|
||||
local from = range.start.line + 1
|
||||
local to = range["end"].line + 1
|
||||
todo[uri].rows[from] = from
|
||||
todo[uri].rows[to] = to
|
||||
end
|
||||
|
||||
local ret = {} ---@type table<lsp.Loc,trouble.Range.lsp>
|
||||
|
||||
for uri, t in pairs(todo) do
|
||||
local buf = vim.uri_to_bufnr(uri)
|
||||
local filename = vim.uri_to_fname(uri)
|
||||
local lines = Util.get_lines({ rows = vim.tbl_keys(t.rows), buf = buf }) or {}
|
||||
for _, loc in ipairs(t.locs) do
|
||||
local range = loc.range or loc.targetSelectionRange
|
||||
local line = lines[range.start.line + 1] or ""
|
||||
local end_line = lines[range["end"].line + 1] or ""
|
||||
local pos = { range.start.line + 1, get_line_col(line, range.start.character, client.offset_encoding) }
|
||||
local end_pos = { range["end"].line + 1, get_line_col(end_line, range["end"].character, client.offset_encoding) }
|
||||
ret[loc] = {
|
||||
buf = buf,
|
||||
filename = filename,
|
||||
pos = pos,
|
||||
end_pos = end_pos,
|
||||
source = "lsp",
|
||||
client = client,
|
||||
location = loc,
|
||||
line = line,
|
||||
}
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param ctx trouble.Source.ctx
|
||||
function M.get.command(cb, ctx)
|
||||
local err = "Missing command params for `lsp_command`.\n"
|
||||
.. "You need to specify `opts.params = {command = 'the_command', arguments = {}}`"
|
||||
if not ctx.opts.params then
|
||||
return Util.error(err)
|
||||
end
|
||||
---@type lsp.ExecuteCommandParams
|
||||
local params = ctx.opts.params
|
||||
if not params.command then
|
||||
return Util.error(err)
|
||||
end
|
||||
M.get_locations("workspace/executeCommand", cb, ctx, { params = params })
|
||||
end
|
||||
|
||||
---@param ctx trouble.Source.ctx
|
||||
---@param cb trouble.Source.Callback
|
||||
function M.get.references(cb, ctx)
|
||||
local params = ctx.opts.params or {}
|
||||
M.get_locations("textDocument/references", cb, ctx, {
|
||||
context = {
|
||||
includeDeclaration = params.include_declaration ~= false,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param ctx trouble.Source.ctx
|
||||
function M.get.definitions(cb, ctx)
|
||||
M.get_locations("textDocument/definition", cb, ctx)
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param ctx trouble.Source.ctx
|
||||
function M.get.implementations(cb, ctx)
|
||||
M.get_locations("textDocument/implementation", cb, ctx)
|
||||
end
|
||||
|
||||
-- Type Definitions
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param ctx trouble.Source.ctx
|
||||
function M.get.type_definitions(cb, ctx)
|
||||
M.get_locations("textDocument/typeDefinition", cb, ctx)
|
||||
end
|
||||
|
||||
-- Declaration
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param ctx trouble.Source.ctx
|
||||
function M.get.declarations(cb, ctx)
|
||||
M.get_locations("textDocument/declaration", cb, ctx)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,106 @@
|
||||
---@diagnostic disable: inject-field
|
||||
local Item = require("trouble.item")
|
||||
|
||||
---Represents an item in a Neovim quickfix/loclist.
|
||||
---@class qf.item
|
||||
---@field bufnr? number The buffer number where the item originates.
|
||||
---@field filename? string
|
||||
---@field lnum number The start line number for the item.
|
||||
---@field end_lnum? number The end line number for the item.
|
||||
---@field pattern string A pattern related to the item. It can be a search pattern or any relevant string.
|
||||
---@field col? number The column number where the item starts.
|
||||
---@field end_col? number The column number where the item ends.
|
||||
---@field module? string Module information (if any) associated with the item.
|
||||
---@field nr? number A unique number or ID for the item.
|
||||
---@field text? string A description or message related to the item.
|
||||
---@field type? string The type of the item. E.g., "W" might stand for "Warning".
|
||||
---@field valid number A flag indicating if the item is valid (1) or not (0).
|
||||
---@field user_data? any Any user data associated with the item.
|
||||
---@field vcol? number Visual column number. Indicates if the column number is a visual column number (when set to 1) or a byte index (when set to 0).
|
||||
|
||||
---@class trouble.Source.qf: trouble.Source
|
||||
local M = {}
|
||||
|
||||
M.config = {
|
||||
modes = {
|
||||
qflist = {
|
||||
desc = "Quickfix List",
|
||||
events = {
|
||||
"QuickFixCmdPost",
|
||||
{ event = "TextChanged", main = true },
|
||||
},
|
||||
source = "qf.qflist",
|
||||
groups = {
|
||||
{ "filename", format = "{file_icon} {filename} {count}" },
|
||||
},
|
||||
sort = { "severity", "filename", "pos", "message" },
|
||||
format = "{severity_icon|item.type:DiagnosticSignWarn} {text:ts} {pos}",
|
||||
},
|
||||
loclist = {
|
||||
desc = "Location List",
|
||||
events = {
|
||||
"BufEnter",
|
||||
{ event = "TextChanged", main = true },
|
||||
},
|
||||
source = "qf.loclist",
|
||||
groups = {
|
||||
{ "filename", format = "{file_icon} {filename} {count}" },
|
||||
},
|
||||
sort = { "severity", "filename", "pos", "message" },
|
||||
format = "{severity_icon|item.type:DiagnosticSignWarn} {text:ts} {pos}",
|
||||
},
|
||||
},
|
||||
}
|
||||
M.config.modes.quickfix = M.config.modes.qflist
|
||||
|
||||
local severities = {
|
||||
E = vim.diagnostic.severity.ERROR,
|
||||
W = vim.diagnostic.severity.WARN,
|
||||
I = vim.diagnostic.severity.INFO,
|
||||
H = vim.diagnostic.severity.HINT,
|
||||
N = vim.diagnostic.severity.HINT,
|
||||
}
|
||||
|
||||
M.get = {
|
||||
qflist = function(cb)
|
||||
cb(M.get_list())
|
||||
end,
|
||||
loclist = function(cb)
|
||||
cb(M.get_list({ win = vim.api.nvim_get_current_win() }))
|
||||
end,
|
||||
}
|
||||
|
||||
---@param opts? {win:number}
|
||||
function M.get_list(opts)
|
||||
opts = opts or {}
|
||||
local list = opts.win == nil and vim.fn.getqflist({ all = true }) or vim.fn.getloclist(opts.win, { all = true })
|
||||
---@cast list {items?:qf.item[]}?
|
||||
|
||||
local ret = {} ---@type trouble.Item[]
|
||||
for _, item in pairs(list and list.items or {}) do
|
||||
local row = item.lnum == 0 and 1 or item.lnum
|
||||
local col = (item.col == 0 and 1 or item.col) - 1
|
||||
local end_row = item.end_lnum == 0 and row or item.end_lnum
|
||||
local end_col = item.end_col == 0 and col or (item.end_col - 1)
|
||||
|
||||
if item.valid == 1 then
|
||||
ret[#ret + 1] = Item.new({
|
||||
pos = { row, col },
|
||||
end_pos = { end_row, end_col },
|
||||
text = item.text,
|
||||
severity = severities[item.type] or 0,
|
||||
buf = item.bufnr,
|
||||
filename = item.filename,
|
||||
item = item,
|
||||
source = "qf",
|
||||
})
|
||||
elseif #ret > 0 and ret[#ret].item.text and item.text then
|
||||
ret[#ret].item.text = ret[#ret].item.text .. "\n" .. item.text
|
||||
end
|
||||
end
|
||||
Item.add_id(ret, { "severity" })
|
||||
Item.add_text(ret, { mode = "full" })
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,123 @@
|
||||
---@diagnostic disable: inject-field
|
||||
local Item = require("trouble.item")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---Represents an item in a Neovim quickfix/loclist.
|
||||
---@class telescope.Item
|
||||
---@field lnum? number The start line number for the item.
|
||||
---@field col? number The column number where the item starts.
|
||||
---@field bufnr? number The buffer number where the item originates.
|
||||
---@field filename? string The filename of the item.
|
||||
---@field text? string The text of the item.
|
||||
---@field cwd? string The current working directory of the item.
|
||||
---@field path? string The path of the item.
|
||||
|
||||
---@class trouble.Source.telescope: trouble.Source
|
||||
local M = {}
|
||||
|
||||
---@type trouble.Item[]
|
||||
M.items = {}
|
||||
|
||||
M.config = {
|
||||
modes = {
|
||||
telescope = {
|
||||
desc = "Telescope results previously opened with `require('trouble.sources.telescope').open()`.",
|
||||
source = "telescope",
|
||||
title = "{hl:Title}Telescope{hl} {count}",
|
||||
groups = {
|
||||
{ "filename", format = "{file_icon} {filename} {count}" },
|
||||
},
|
||||
sort = { "filename", "pos" },
|
||||
format = "{text:ts} {pos}",
|
||||
},
|
||||
telescope_files = {
|
||||
desc = "Telescope results previously opened with `require('trouble.sources.telescope').open()`.",
|
||||
source = "telescope",
|
||||
title = "{hl:Title}Telescope{hl} {count}",
|
||||
sort = { "filename", "pos" },
|
||||
format = "{file_icon} {filename}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@param item telescope.Item
|
||||
function M.item(item)
|
||||
---@type string
|
||||
local filename
|
||||
if item.path then
|
||||
filename = item.path
|
||||
else
|
||||
filename = item.filename
|
||||
if item.cwd then
|
||||
filename = item.cwd .. "/" .. filename
|
||||
end
|
||||
end
|
||||
local word = item.text and item.col and item.text:sub(item.col):match("%S+")
|
||||
local pos = item.lnum and { item.lnum, item.col and item.col - 1 or 0 } or nil
|
||||
return Item.new({
|
||||
source = "telescope",
|
||||
buf = item.bufnr,
|
||||
filename = filename,
|
||||
pos = pos,
|
||||
end_pos = word and pos and { pos[1], pos[2] + #word } or nil,
|
||||
item = item,
|
||||
})
|
||||
end
|
||||
|
||||
---@param cb trouble.Source.Callback
|
||||
---@param _ctx trouble.Source.ctx)
|
||||
function M.get(cb, _ctx)
|
||||
cb(M.items)
|
||||
end
|
||||
|
||||
-- Returns the mode based on the items.
|
||||
function M.mode()
|
||||
for _, item in ipairs(M.items) do
|
||||
if item.text then
|
||||
return "telescope"
|
||||
end
|
||||
end
|
||||
return "telescope_files"
|
||||
end
|
||||
|
||||
-- Append the current telescope buffer to the trouble list.
|
||||
---@param opts? trouble.Mode|string
|
||||
function M.add(prompt_bufnr, opts)
|
||||
local action_state = require("telescope.actions.state")
|
||||
---@type Picker
|
||||
local picker = action_state.get_current_picker(prompt_bufnr)
|
||||
if not picker then
|
||||
return Util.error("No Telescope picker found?")
|
||||
end
|
||||
|
||||
if #picker:get_multi_selection() > 0 then
|
||||
for _, item in ipairs(picker:get_multi_selection()) do
|
||||
table.insert(M.items, M.item(item))
|
||||
end
|
||||
else
|
||||
for item in picker.manager:iter() do
|
||||
table.insert(M.items, M.item(item))
|
||||
end
|
||||
end
|
||||
-- Item.add_text(M.items, { mode = "after" })
|
||||
|
||||
vim.schedule(function()
|
||||
require("telescope.actions").close(prompt_bufnr)
|
||||
opts = opts or {}
|
||||
if type(opts) == "string" then
|
||||
opts = { mode = opts }
|
||||
end
|
||||
opts = vim.tbl_extend("force", { mode = M.mode() }, opts)
|
||||
require("trouble").open(opts)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Opens the current telescope buffer in the trouble list.
|
||||
-- This will clear the existing items.
|
||||
---@param opts? trouble.Mode|string
|
||||
function M.open(prompt_bufnr, opts)
|
||||
M.items = {}
|
||||
M.add(prompt_bufnr, opts)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,194 @@
|
||||
local Config = require("trouble.config")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@alias trouble.SorterFn fun(item: trouble.Item): any?
|
||||
|
||||
---@alias trouble.Sort.spec string|trouble.SorterFn|(string|trouble.SorterFn|trouble.Filter.spec)[]
|
||||
---@alias trouble.Filter.spec table<string, any>|fun(items: trouble.Item[]): trouble.Item[]
|
||||
---@alias trouble.Group.spec string|string[]|{format?:string}
|
||||
|
||||
---@alias trouble.Sections.spec (trouble.Section.spec|string)[]
|
||||
|
||||
---@class trouble.Section.spec
|
||||
---@field source string
|
||||
---@field title? string|boolean
|
||||
---@field events? (string|trouble.Event)[]
|
||||
---@field groups? trouble.Group.spec[]|trouble.Group.spec
|
||||
---@field sort? trouble.Sort.spec
|
||||
---@field filter? trouble.Filter.spec
|
||||
---@field flatten? boolean when true, items with a natural hierarchy will be flattened
|
||||
---@field format? string
|
||||
---@field max_items? number
|
||||
---@field params? table<string, any>
|
||||
|
||||
---@alias trouble.Filter table<string, any>|fun(items: trouble.Item[]): trouble.Item[]
|
||||
|
||||
---@class trouble.Event
|
||||
---@field event string|string[]
|
||||
---@field pattern? string|string[]
|
||||
---@field main? boolean When true, this event will refresh only when it is the main window
|
||||
|
||||
---@class trouble.Sort
|
||||
---@field field? string
|
||||
---@field sorter? trouble.SorterFn
|
||||
---@field filter? trouble.Filter
|
||||
---@field desc? boolean
|
||||
|
||||
---@class trouble.Group
|
||||
---@field fields? string[]
|
||||
---@field format? string
|
||||
---@field directory? boolean
|
||||
|
||||
---@class trouble.Section.opts
|
||||
---@field source string
|
||||
---@field groups trouble.Group[]
|
||||
---@field format string
|
||||
---@field flatten? boolean when true, items with a natural hierarchy will be flattened
|
||||
---@field events trouble.Event[]
|
||||
---@field sort? trouble.Sort[]
|
||||
---@field filter? trouble.Filter
|
||||
---@field max_items? number
|
||||
---@field params? table<string, any>
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param spec trouble.Section.spec|string
|
||||
---@return trouble.Section.opts
|
||||
function M.section(spec)
|
||||
local groups = type(spec.groups) == "string" and { spec.groups } or spec.groups
|
||||
---@cast groups trouble.Group.spec[]
|
||||
local events = {} ---@type trouble.Event[]
|
||||
for _, e in ipairs(spec.events or {}) do
|
||||
if type(e) == "string" then
|
||||
local event, pattern = e:match("^(%w+)%s+(.*)$")
|
||||
event = event or e
|
||||
events[#events + 1] = { event = event, pattern = pattern }
|
||||
elseif type(e) == "table" and e.event then
|
||||
events[#events + 1] = e
|
||||
else
|
||||
error("invalid event: " .. vim.inspect(e))
|
||||
end
|
||||
end
|
||||
|
||||
local ret = {
|
||||
source = spec.source,
|
||||
groups = vim.tbl_map(M.group, groups or {}),
|
||||
sort = spec.sort and M.sort(spec.sort) or nil,
|
||||
filter = spec.filter,
|
||||
format = spec.format or "{filename} {pos}",
|
||||
events = events,
|
||||
flatten = spec.flatten,
|
||||
params = spec.params,
|
||||
}
|
||||
-- A title is just a group without fields
|
||||
if spec.title then
|
||||
table.insert(ret.groups, 1, { fields = {}, format = spec.title })
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param action trouble.Action.spec
|
||||
---@return trouble.Action
|
||||
function M.action(action)
|
||||
if type(action) == "string" then
|
||||
action = { action = action, desc = action:gsub("_", " ") }
|
||||
end
|
||||
if type(action) == "function" then
|
||||
action = { action = action }
|
||||
end
|
||||
if type(action.action) == "string" then
|
||||
local desc = action.action:gsub("_", " ")
|
||||
action.action = require("trouble.config.actions")[action.action]
|
||||
if type(action.action) == "table" then
|
||||
action = action.action
|
||||
end
|
||||
action.desc = action.desc or desc
|
||||
end
|
||||
---@cast action trouble.Action
|
||||
return action
|
||||
end
|
||||
|
||||
---@param mode trouble.Mode
|
||||
---@return trouble.Section.opts[]
|
||||
function M.sections(mode)
|
||||
local ret = {} ---@type trouble.Section.opts[]
|
||||
|
||||
if mode.sections then
|
||||
for _, s in ipairs(mode.sections) do
|
||||
ret[#ret + 1] = M.section(Config.get(mode, { sections = false }, s) --[[@as trouble.Mode]])
|
||||
end
|
||||
else
|
||||
local section = M.section(mode)
|
||||
section.max_items = section.max_items or mode.max_items
|
||||
ret[#ret + 1] = section
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param spec trouble.Sort.spec
|
||||
---@return trouble.Sort[]
|
||||
function M.sort(spec)
|
||||
spec = type(spec) == "table" and Util.islist(spec) and spec or { spec }
|
||||
---@cast spec (string|trouble.SorterFn|trouble.Filter.spec)[]
|
||||
local fields = {} ---@type trouble.Sort[]
|
||||
for f, field in ipairs(spec) do
|
||||
if type(field) == "function" then
|
||||
---@cast field trouble.SorterFn
|
||||
fields[f] = { sorter = field }
|
||||
elseif type(field) == "table" and field.field then
|
||||
---@cast field {field:string, desc?:boolean}
|
||||
fields[f] = field
|
||||
elseif type(field) == "table" then
|
||||
fields[f] = { filter = field }
|
||||
elseif type(field) == "string" then
|
||||
local desc = field:sub(1, 1) == "-"
|
||||
fields[f] = {
|
||||
field = desc and field:sub(2) or field,
|
||||
desc = desc and true or nil,
|
||||
}
|
||||
else
|
||||
error("invalid sort field: " .. vim.inspect(field))
|
||||
end
|
||||
end
|
||||
return fields
|
||||
end
|
||||
|
||||
---@param spec trouble.Group.spec
|
||||
---@return trouble.Group
|
||||
function M.group(spec)
|
||||
spec = type(spec) == "string" and { spec } or spec
|
||||
---@cast spec string[]|{format?:string}
|
||||
---@type trouble.Group
|
||||
local ret = { fields = {}, format = "" }
|
||||
for k, v in pairs(spec) do
|
||||
if type(k) == "number" then
|
||||
---@cast v string
|
||||
ret.fields[#ret.fields + 1] = v
|
||||
elseif k == "format" then
|
||||
---@cast v string
|
||||
ret[k] = v
|
||||
else
|
||||
error("invalid `group` key: " .. k)
|
||||
end
|
||||
end
|
||||
if vim.tbl_contains(ret.fields, "directory") then
|
||||
ret.directory = true
|
||||
ret.format = ret.format == "" and "{directory_icon} {directory} {count}" or ret.format
|
||||
if #ret.fields > 1 then
|
||||
error("group: cannot specify other fields with `directory`")
|
||||
end
|
||||
ret.fields = nil
|
||||
end
|
||||
if ret.format == "" then
|
||||
ret.format = table.concat(
|
||||
---@param f string
|
||||
vim.tbl_map(function(f)
|
||||
return "{" .. f .. "}"
|
||||
end, ret.fields),
|
||||
" "
|
||||
)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,308 @@
|
||||
local Item = require("trouble.item")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@class trouble.Node
|
||||
---@field id string
|
||||
---@field parent? trouble.Node
|
||||
---@field item? trouble.Item
|
||||
---@field index? table<string, trouble.Node>
|
||||
---@field group? trouble.Group
|
||||
---@field folded? boolean
|
||||
---@field children? trouble.Node[]
|
||||
---@field private _depth number
|
||||
---@field private _count? number
|
||||
---@field private _degree? number
|
||||
local M = {}
|
||||
|
||||
---@alias trouble.GroupFn fun(item: trouble.Item, parent: trouble.Node, group: trouble.Group): trouble.Node
|
||||
|
||||
---@param opts {id: string, item?: trouble.Item}
|
||||
function M.new(opts)
|
||||
local self = setmetatable(opts, { __index = M })
|
||||
self.id = self.id or self.item and self.item.id or nil
|
||||
self.children = {}
|
||||
self.index = {}
|
||||
return self
|
||||
end
|
||||
|
||||
function M:delete()
|
||||
local parent = self.parent
|
||||
if not parent then
|
||||
return
|
||||
end
|
||||
if parent.children then
|
||||
parent.children = vim.tbl_filter(function(c)
|
||||
return c ~= self
|
||||
end, parent.children)
|
||||
end
|
||||
if parent.index and self.id then
|
||||
parent.index[self.id] = nil
|
||||
end
|
||||
parent._count = nil
|
||||
parent._degree = nil
|
||||
if parent:count() == 0 then
|
||||
parent:delete()
|
||||
end
|
||||
end
|
||||
|
||||
-- Max depth of the tree
|
||||
function M:degree()
|
||||
if not self._degree then
|
||||
self._degree = 0
|
||||
for _, child in ipairs(self.children or {}) do
|
||||
self._degree = math.max(self._degree, child:degree())
|
||||
end
|
||||
self._degree = self._degree + 1
|
||||
end
|
||||
return self._degree
|
||||
end
|
||||
|
||||
-- Depth of this node
|
||||
function M:depth()
|
||||
if not self._depth then
|
||||
self._depth = self.parent and (self.parent:depth() + 1) or 0
|
||||
end
|
||||
return self._depth
|
||||
end
|
||||
|
||||
-- Number of actual items in the tree
|
||||
-- This excludes internal group nodes
|
||||
function M:count()
|
||||
if not self._count then
|
||||
self._count = 0
|
||||
for _, child in ipairs(self.children or {}) do
|
||||
self._count = self._count + child:count()
|
||||
end
|
||||
if not self.group and self.item then
|
||||
self._count = self._count + 1
|
||||
end
|
||||
end
|
||||
return self._count
|
||||
end
|
||||
|
||||
--- Gets all the items in the tree, recursively.
|
||||
---@param ret trouble.Item[]?
|
||||
function M:flatten(ret)
|
||||
ret = ret or {}
|
||||
for _, child in ipairs(self.children or {}) do
|
||||
child:flatten(ret)
|
||||
end
|
||||
if not self.group and self.item then
|
||||
ret[#ret + 1] = self.item
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param idx number|string
|
||||
---@return trouble.Node?
|
||||
function M:get(idx)
|
||||
return type(idx) == "number" and self.children[idx] or self.index[idx]
|
||||
end
|
||||
|
||||
-- Source of the item of this node
|
||||
function M:source()
|
||||
return self.item and self.item.source
|
||||
end
|
||||
|
||||
-- Width of the node (number of children)
|
||||
function M:width()
|
||||
return self.children and #self.children or 0
|
||||
end
|
||||
|
||||
-- Item of the parent node
|
||||
function M:parent_item()
|
||||
return self.parent and self.parent.item
|
||||
end
|
||||
|
||||
function M:add(node)
|
||||
if node.id then
|
||||
if self.index[node.id] then
|
||||
Util.debug("node already exists:\n" .. node.id)
|
||||
node.id = node.id .. "_"
|
||||
end
|
||||
self.index[node.id] = node
|
||||
end
|
||||
node.parent = self
|
||||
table.insert(self.children, node)
|
||||
return node
|
||||
end
|
||||
|
||||
function M:is_leaf()
|
||||
return self.children == nil or #self.children == 0
|
||||
end
|
||||
|
||||
---@param other? trouble.Node
|
||||
function M:is(other)
|
||||
if not other then
|
||||
return false
|
||||
end
|
||||
|
||||
if self == other then
|
||||
return true
|
||||
end
|
||||
|
||||
if self.id ~= other.id then
|
||||
return false
|
||||
end
|
||||
|
||||
if self.group ~= other.group then
|
||||
return false
|
||||
end
|
||||
|
||||
if self.group then
|
||||
return true
|
||||
end
|
||||
assert(self.item, "missing item")
|
||||
|
||||
if not other.item then
|
||||
return false
|
||||
end
|
||||
|
||||
if self.item == other.item then
|
||||
return true
|
||||
end
|
||||
|
||||
return self.item.id and (self.item.id == other.item.id)
|
||||
end
|
||||
|
||||
--- Build a tree from a list of items and a section.
|
||||
---@param items trouble.Item[]
|
||||
---@param section trouble.Section.opts
|
||||
function M.build(items, section)
|
||||
local root = M.new({ id = "$root" })
|
||||
local node_items = {} ---@type table<trouble.Node, trouble.Item[]>
|
||||
|
||||
-- create the group nodes
|
||||
for i, item in ipairs(items) do
|
||||
if section.max_items and i > section.max_items then
|
||||
break
|
||||
end
|
||||
local node = root
|
||||
for _, group in ipairs(section.groups) do
|
||||
local builder = M.builders[group.directory and "directory" or "fields"]
|
||||
assert(builder, "unknown group type: " .. vim.inspect(group))
|
||||
node = builder.group(item, node, group)
|
||||
end
|
||||
node_items[node] = node_items[node] or {}
|
||||
table.insert(node_items[node], item)
|
||||
end
|
||||
|
||||
-- add the items to the nodes.
|
||||
-- this will structure items by their parent node unless flatten is true
|
||||
for node, nitems in pairs(node_items) do
|
||||
M.add_items(node, nitems, { flatten = section.flatten })
|
||||
end
|
||||
|
||||
-- post process the tree
|
||||
for _, group in ipairs(section.groups) do
|
||||
local builder = M.builders[group.directory and "directory" or "fields"]
|
||||
if builder.post then
|
||||
root = builder.post(root) or root
|
||||
end
|
||||
end
|
||||
return root
|
||||
end
|
||||
|
||||
--- This will add all the items to the root node,
|
||||
--- structured by their parent item, unless flatten is true.
|
||||
---@param root trouble.Node
|
||||
---@param items trouble.Item[]
|
||||
---@param opts? {flatten?: boolean}
|
||||
function M.add_items(root, items, opts)
|
||||
opts = opts or {}
|
||||
local item_nodes = {} ---@type table<trouble.Item, trouble.Node>
|
||||
for _, item in ipairs(items) do
|
||||
item_nodes[item] = M.new({ item = item })
|
||||
end
|
||||
for _, item in ipairs(items) do
|
||||
local node = item_nodes[item]
|
||||
local parent_node = root
|
||||
if not opts.flatten then
|
||||
local parent = item.parent
|
||||
while parent do
|
||||
if item_nodes[parent] then
|
||||
parent_node = item_nodes[parent]
|
||||
break
|
||||
end
|
||||
parent = parent.parent
|
||||
end
|
||||
end
|
||||
parent_node:add(node)
|
||||
end
|
||||
end
|
||||
|
||||
---@alias trouble.Group.builder {group:trouble.GroupFn, post?:(fun(node: trouble.Node):trouble.Node?)}
|
||||
---@type table<"directory"|"fields", trouble.Group.builder>
|
||||
M.builders = {
|
||||
fields = {
|
||||
group = function(item, parent, group)
|
||||
-- id is based on the parent id and the group fields
|
||||
local id = group.format
|
||||
if #group.fields > 0 then
|
||||
local values = {} ---@type string[]
|
||||
for i = 1, #group.fields do
|
||||
values[#values + 1] = tostring(item[group.fields[i]])
|
||||
end
|
||||
id = table.concat(values, "|")
|
||||
end
|
||||
id = parent.id .. "#" .. id
|
||||
local child = parent:get(id)
|
||||
if not child then
|
||||
child = M.new({ id = id, item = item, group = group })
|
||||
parent:add(child)
|
||||
end
|
||||
return child
|
||||
end,
|
||||
},
|
||||
|
||||
directory = {
|
||||
group = function(item, root, group)
|
||||
if not item.dirname then
|
||||
return root
|
||||
end
|
||||
local directory = ""
|
||||
local parent = root
|
||||
for _, part in Util.split(item.dirname, "/") do
|
||||
directory = directory .. part .. "/"
|
||||
local id = (root.id or "") .. "#" .. directory
|
||||
local child = parent:get(id)
|
||||
if not child then
|
||||
local dir = Item.new({
|
||||
filename = directory,
|
||||
source = "fs",
|
||||
id = id,
|
||||
pos = { 1, 0 },
|
||||
end_pos = { 1, 0 },
|
||||
dirname = directory,
|
||||
item = { directory = directory, type = "directory" },
|
||||
})
|
||||
child = M.new({ id = id, item = dir, group = group })
|
||||
parent:add(child)
|
||||
end
|
||||
parent = child
|
||||
end
|
||||
return parent
|
||||
end,
|
||||
post = function(root)
|
||||
---@param node trouble.Node
|
||||
local function collapse(node)
|
||||
if node:source() == "fs" then
|
||||
if node:width() == 1 then
|
||||
local child = node.children[1]
|
||||
if child:source() == "fs" and child.item.type == "directory" then
|
||||
child.parent = node.parent
|
||||
return collapse(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
for c, child in ipairs(node.children or {}) do
|
||||
node.children[c] = collapse(child)
|
||||
end
|
||||
return node
|
||||
end
|
||||
return collapse(root)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,80 @@
|
||||
local Config = require("trouble.config")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@alias trouble.Indent.type "top"|"middle"|"last"|"fold_open"|"fold_closed"|"ws"
|
||||
---@alias trouble.Indent.symbols table<trouble.Indent.type, string|string[]>
|
||||
|
||||
---@class SymbolSegment: TextSegment
|
||||
---@field type trouble.Indent.type
|
||||
|
||||
---@class trouble.Indent: {[number]: SymbolSegment}
|
||||
---@field symbols table<string, SymbolSegment>
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
---@param symbols? trouble.Indent.symbols
|
||||
function M.new(symbols)
|
||||
local self = setmetatable({}, M)
|
||||
symbols = vim.tbl_deep_extend("force", Config.icons.indent, symbols or {})
|
||||
self.symbols = {}
|
||||
for k, v in pairs(symbols) do
|
||||
local symbol = v
|
||||
self.symbols[k] = type(symbol) == "string" and { str = symbol } or { str = symbol[1], hl = symbol[2] }
|
||||
self.symbols[k].type = k
|
||||
self.symbols[k].hl = self.symbols[k].hl or ("TroubleIndent" .. Util.camel(k))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---@return number
|
||||
---@param opts? {display:boolean}
|
||||
function M:width(opts)
|
||||
local ret = 0
|
||||
for _, segment in ipairs(self) do
|
||||
ret = ret + (opts and opts.display and vim.fn.strdisplaywidth(segment.str) or #segment.str)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
-- Returns a new indent with all the symbols replaced with whitespace
|
||||
function M:indent()
|
||||
local new = setmetatable({}, M)
|
||||
for k, v in pairs(self) do
|
||||
new[k] = type(k) == "number" and self.symbols.ws or v
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
function M:clone()
|
||||
local new = setmetatable({}, M)
|
||||
for k, v in pairs(self) do
|
||||
new[k] = v
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
---@param other trouble.Indent.type
|
||||
function M:add(other)
|
||||
table.insert(self, self.symbols[other])
|
||||
return self
|
||||
end
|
||||
|
||||
---@return SymbolSegment?
|
||||
function M:del()
|
||||
return table.remove(self)
|
||||
end
|
||||
|
||||
---@param other trouble.Indent.type
|
||||
function M:extend(other)
|
||||
return self:clone():add(other)
|
||||
end
|
||||
|
||||
function M:multi_line()
|
||||
local last = self:del()
|
||||
if last then
|
||||
self:add(last.type == "middle" and "top" or last.type == "last" and "ws" or last.type)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,742 @@
|
||||
local Format = require("trouble.format")
|
||||
local Main = require("trouble.view.main")
|
||||
local Preview = require("trouble.view.preview")
|
||||
local Promise = require("trouble.promise")
|
||||
local Render = require("trouble.view.render")
|
||||
local Section = require("trouble.view.section")
|
||||
local Spec = require("trouble.spec")
|
||||
local Text = require("trouble.view.text")
|
||||
local Util = require("trouble.util")
|
||||
local Window = require("trouble.view.window")
|
||||
|
||||
---@class trouble.View
|
||||
---@field win trouble.Window
|
||||
---@field preview_win? trouble.Window
|
||||
---@field opts trouble.Mode
|
||||
---@field sections trouble.Section[]
|
||||
---@field renderer trouble.Render
|
||||
---@field first_render trouble.Promise
|
||||
---@field first_update trouble.Promise
|
||||
---@field moving uv_timer_t
|
||||
---@field clicked uv_timer_t
|
||||
---@field state table<string,any>
|
||||
---@field _filters table<string, trouble.ViewFilter>
|
||||
---@field private _main? trouble.Main
|
||||
local M = {}
|
||||
M.__index = M
|
||||
local _idx = 0
|
||||
---@type table<trouble.View, number>
|
||||
M._views = setmetatable({}, { __mode = "k" })
|
||||
|
||||
---@type trouble.View[]
|
||||
M._auto = {}
|
||||
|
||||
---@type table<string, trouble.Render.Location>
|
||||
M._last = {}
|
||||
|
||||
M.MOVING_DELAY = 4000
|
||||
|
||||
local uv = vim.loop or vim.uv
|
||||
|
||||
---@param opts trouble.Mode
|
||||
function M.new(opts)
|
||||
local self = setmetatable({}, M)
|
||||
_idx = _idx + 1
|
||||
M._views[self] = _idx
|
||||
self.state = {}
|
||||
self.opts = opts or {}
|
||||
self._filters = {}
|
||||
self.first_render = Promise.new(function() end)
|
||||
self.first_update = Promise.new(function() end)
|
||||
self.opts.win = vim.tbl_deep_extend("force", self.opts.win or {}, Window.FOLDS)
|
||||
self.opts.win.on_mount = function()
|
||||
self:on_mount()
|
||||
end
|
||||
self.opts.win.on_close = function()
|
||||
if not self.opts.auto_open then
|
||||
for _, section in ipairs(self.sections) do
|
||||
section:stop()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.sections = {}
|
||||
for _, s in ipairs(Spec.sections(self.opts)) do
|
||||
local section = Section.new(s, self.opts)
|
||||
section.on_update = function()
|
||||
self:update()
|
||||
end
|
||||
table.insert(self.sections, section)
|
||||
end
|
||||
|
||||
self.win = Window.new(self.opts.win)
|
||||
self.opts.win = self.win.opts
|
||||
|
||||
self.preview_win = Window.new(self.opts.preview) or nil
|
||||
|
||||
self.renderer = Render.new(self.opts, {
|
||||
padding = vim.tbl_get(self.opts.win, "padding", "left") or 0,
|
||||
multiline = self.opts.multiline,
|
||||
})
|
||||
self.update = Util.throttle(M.update, Util.throttle_opts(self.opts.throttle.update, { ms = 10 }))
|
||||
self.render = Util.throttle(M.render, Util.throttle_opts(self.opts.throttle.render, { ms = 10 }))
|
||||
self.follow = Util.throttle(M.follow, Util.throttle_opts(self.opts.throttle.follow, { ms = 100 }))
|
||||
|
||||
if self.opts.auto_open then
|
||||
-- add to a table, so that the view doesn't gc
|
||||
table.insert(M._auto, self)
|
||||
self:listen()
|
||||
self:refresh()
|
||||
end
|
||||
self.moving = uv.new_timer()
|
||||
self.clicked = uv.new_timer()
|
||||
return self
|
||||
end
|
||||
|
||||
---@alias trouble.View.filter {debug?: boolean, open?:boolean, mode?: string}
|
||||
|
||||
---@param filter? trouble.View.filter
|
||||
function M.get(filter)
|
||||
filter = filter or {}
|
||||
---@type {idx:number, mode?: string, view: trouble.View, is_open: boolean}[]
|
||||
local ret = {}
|
||||
for view, idx in pairs(M._views) do
|
||||
local is_open = view.win:valid()
|
||||
local opening = view.first_update:is_pending()
|
||||
local ok = is_open or view.opts.auto_open or opening
|
||||
ok = ok and (not filter.mode or filter.mode == view.opts.mode)
|
||||
ok = ok and (not filter.open or is_open or opening)
|
||||
if ok then
|
||||
ret[#ret + 1] = {
|
||||
idx = idx,
|
||||
mode = view.opts.mode,
|
||||
view = not filter.debug and view or {},
|
||||
is_open = is_open,
|
||||
}
|
||||
end
|
||||
end
|
||||
table.sort(ret, function(a, b)
|
||||
return a.idx < b.idx
|
||||
end)
|
||||
return ret
|
||||
end
|
||||
|
||||
function M:on_mount()
|
||||
vim.w[self.win.win].trouble = {
|
||||
mode = self.opts.mode,
|
||||
type = self.opts.win.type,
|
||||
relative = self.opts.win.relative,
|
||||
position = self.opts.win.position,
|
||||
}
|
||||
|
||||
self:listen()
|
||||
self.win:on("WinLeave", function()
|
||||
if self.opts.preview.type == "main" and self.clicked:is_active() and Preview.is_open() then
|
||||
local main = self.preview_win.opts.win
|
||||
local preview = self.preview_win.win
|
||||
if main and preview and vim.api.nvim_win_is_valid(main) and vim.api.nvim_win_is_valid(preview) then
|
||||
local view = vim.api.nvim_win_call(preview, vim.fn.winsaveview)
|
||||
vim.api.nvim_win_call(main, function()
|
||||
vim.fn.winrestview(view)
|
||||
end)
|
||||
vim.api.nvim_set_current_win(main)
|
||||
end
|
||||
end
|
||||
Preview.close()
|
||||
end)
|
||||
|
||||
local _self = Util.weak(self)
|
||||
|
||||
local preview = Util.throttle(
|
||||
M.preview,
|
||||
Util.throttle_opts(self.opts.throttle.preview, {
|
||||
ms = 100,
|
||||
debounce = true,
|
||||
})
|
||||
)
|
||||
|
||||
self.win:on("CursorMoved", function()
|
||||
local this = _self()
|
||||
if not this then
|
||||
return true
|
||||
end
|
||||
M._last[self.opts.mode or ""] = self:at()
|
||||
if this.opts.auto_preview then
|
||||
local loc = this:at()
|
||||
if loc and loc.item then
|
||||
preview(this, loc.item)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if self.opts.follow then
|
||||
-- tracking of the current item
|
||||
self.win:on("CursorMoved", function()
|
||||
local this = _self()
|
||||
if not this then
|
||||
return true
|
||||
end
|
||||
if this.win:valid() then
|
||||
this:follow()
|
||||
end
|
||||
end, { buffer = false })
|
||||
end
|
||||
|
||||
self.win:on("OptionSet", function()
|
||||
local this = _self()
|
||||
if not this then
|
||||
return true
|
||||
end
|
||||
if this.win:valid() then
|
||||
local foldlevel = vim.wo[this.win.win].foldlevel
|
||||
if foldlevel ~= this.renderer.foldlevel then
|
||||
this:fold_level({ level = foldlevel })
|
||||
end
|
||||
end
|
||||
end, { pattern = "foldlevel", buffer = false })
|
||||
|
||||
for k, v in pairs(self.opts.keys) do
|
||||
if v ~= false then
|
||||
self:map(k, v)
|
||||
end
|
||||
end
|
||||
|
||||
self.win:map("<leftmouse>", function()
|
||||
self.clicked:start(100, 0, function() end)
|
||||
return "<leftmouse>"
|
||||
end, { remap = false, expr = true })
|
||||
end
|
||||
|
||||
---@param node? trouble.Node
|
||||
function M:delete(node)
|
||||
local selection = node and { node } or self:selection()
|
||||
if #selection == 0 then
|
||||
return
|
||||
end
|
||||
for _, n in ipairs(selection) do
|
||||
n:delete()
|
||||
end
|
||||
self.opts.auto_refresh = false
|
||||
self:render()
|
||||
end
|
||||
|
||||
---@param node? trouble.Node
|
||||
---@param opts? trouble.Render.fold_opts
|
||||
function M:fold(node, opts)
|
||||
node = node or self:at().node
|
||||
if node then
|
||||
self.renderer:fold(node, opts)
|
||||
self:render()
|
||||
end
|
||||
end
|
||||
|
||||
---@param opts {level?:number, add?:number}
|
||||
function M:fold_level(opts)
|
||||
self.renderer:fold_level(opts)
|
||||
self:render()
|
||||
end
|
||||
|
||||
---@param item? trouble.Item
|
||||
---@param opts? {split?: boolean, vsplit?:boolean}
|
||||
function M:jump(item, opts)
|
||||
opts = opts or {}
|
||||
item = item or self:at().item
|
||||
vim.schedule(function()
|
||||
Preview.close()
|
||||
end)
|
||||
if not item then
|
||||
return Util.warn("No item to jump to")
|
||||
end
|
||||
|
||||
if not (item.buf or item.filename) then
|
||||
Util.warn("No buffer or filename for item")
|
||||
return
|
||||
end
|
||||
|
||||
item.buf = item.buf or vim.fn.bufadd(item.filename)
|
||||
|
||||
if not vim.api.nvim_buf_is_loaded(item.buf) then
|
||||
vim.fn.bufload(item.buf)
|
||||
end
|
||||
if not vim.bo[item.buf].buflisted then
|
||||
vim.bo[item.buf].buflisted = true
|
||||
end
|
||||
local main = self:main()
|
||||
local win = main and main.win or 0
|
||||
|
||||
vim.api.nvim_win_call(win, function()
|
||||
-- save position in jump list
|
||||
vim.cmd("normal! m'")
|
||||
end)
|
||||
|
||||
if opts.split then
|
||||
vim.api.nvim_win_call(win, function()
|
||||
vim.cmd("split")
|
||||
win = vim.api.nvim_get_current_win()
|
||||
end)
|
||||
elseif opts.vsplit then
|
||||
vim.api.nvim_win_call(win, function()
|
||||
vim.cmd("vsplit")
|
||||
win = vim.api.nvim_get_current_win()
|
||||
end)
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_buf(win, item.buf)
|
||||
-- order of the below seems important with splitkeep=screen
|
||||
vim.api.nvim_set_current_win(win)
|
||||
vim.api.nvim_win_set_cursor(win, item.pos)
|
||||
vim.api.nvim_win_call(win, function()
|
||||
vim.cmd("norm! zzzv")
|
||||
end)
|
||||
return item
|
||||
end
|
||||
|
||||
function M:wait(fn)
|
||||
self.first_render:next(fn)
|
||||
end
|
||||
|
||||
---@param item? trouble.Item
|
||||
function M:preview(item)
|
||||
item = item or self:at().item
|
||||
if not item then
|
||||
return Util.warn("No item to preview")
|
||||
end
|
||||
|
||||
return Preview.open(self, item, { scratch = self.opts.preview.scratch })
|
||||
end
|
||||
|
||||
function M:main()
|
||||
self._main = Main.get(self.opts.pinned and self._main or nil)
|
||||
return self._main
|
||||
end
|
||||
|
||||
function M:goto_main()
|
||||
local main = self:main()
|
||||
if main then
|
||||
vim.api.nvim_set_current_win(main.win)
|
||||
end
|
||||
end
|
||||
|
||||
function M:listen()
|
||||
self:main()
|
||||
|
||||
for _, section in ipairs(self.sections) do
|
||||
section:listen()
|
||||
end
|
||||
end
|
||||
|
||||
---@param cursor? number[]
|
||||
function M:at(cursor)
|
||||
if not vim.api.nvim_buf_is_valid(self.win.buf) then
|
||||
return {}
|
||||
end
|
||||
cursor = cursor or vim.api.nvim_win_get_cursor(self.win.win)
|
||||
return self.renderer:at(cursor[1])
|
||||
end
|
||||
|
||||
function M:selection()
|
||||
if not vim.fn.mode():lower():find("v") then
|
||||
local ret = self:at()
|
||||
return ret.node and { ret.node } or {}
|
||||
end
|
||||
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<esc>", true, false, true), "x", false)
|
||||
|
||||
local from = vim.api.nvim_buf_get_mark(self.win.buf, "<")[1]
|
||||
local to = vim.api.nvim_buf_get_mark(self.win.buf, ">")[1]
|
||||
---@type trouble.Node[]
|
||||
local ret = {}
|
||||
for row = from, to do
|
||||
local node = self.renderer:at(row).node
|
||||
if not vim.tbl_contains(ret, node) then
|
||||
ret[#ret + 1] = node
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param key string
|
||||
---@param action trouble.Action.spec
|
||||
function M:map(key, action)
|
||||
action = Spec.action(action)
|
||||
local _self = Util.weak(self)
|
||||
self.win:map(key, function()
|
||||
local this = _self()
|
||||
if this then
|
||||
this:action(action)
|
||||
end
|
||||
end, { desc = action.desc, mode = action.mode })
|
||||
end
|
||||
|
||||
---@param opts? {idx?: number, up?:number, down?:number, jump?:boolean}
|
||||
function M:move(opts)
|
||||
-- start the moving timer. Will stop any previous timers,
|
||||
-- so this acts as a debounce.
|
||||
-- This is needed to prevent `follow` from being called
|
||||
self.moving:start(M.MOVING_DELAY, 0, function() end)
|
||||
|
||||
opts = opts or {}
|
||||
local cursor = vim.api.nvim_win_get_cursor(self.win.win)
|
||||
local from = 1
|
||||
local to = vim.api.nvim_buf_line_count(self.win.buf)
|
||||
local todo = opts.idx or opts.up or opts.down or 0
|
||||
|
||||
if opts.idx and opts.idx < 0 then
|
||||
from, to = to, 1
|
||||
todo = math.abs(todo)
|
||||
elseif opts.down then
|
||||
from = cursor[1] + 1
|
||||
elseif opts.up then
|
||||
from = cursor[1] - 1
|
||||
to = 1
|
||||
end
|
||||
|
||||
for row = from, to, from > to and -1 or 1 do
|
||||
local info = self.renderer:at(row)
|
||||
if info.item and info.first_line then
|
||||
todo = todo - 1
|
||||
if todo == 0 then
|
||||
vim.api.nvim_win_set_cursor(self.win.win, { row, cursor[2] })
|
||||
if opts.jump then
|
||||
self:jump(info.item)
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param action trouble.Action.spec
|
||||
---@param opts? table
|
||||
function M:action(action, opts)
|
||||
action = Spec.action(action)
|
||||
self:wait(function()
|
||||
local at = self:at() or {}
|
||||
action.action(self, {
|
||||
item = at.item,
|
||||
node = at.node,
|
||||
opts = type(opts) == "table" and opts or {},
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
---@param opts? {update?: boolean, opening?: boolean}
|
||||
function M:refresh(opts)
|
||||
opts = opts or {}
|
||||
if not (opts.opening or self.win:valid() or self.opts.auto_open) then
|
||||
return
|
||||
end
|
||||
---@param section trouble.Section
|
||||
return Promise.all(vim.tbl_map(function(section)
|
||||
return section:refresh(opts)
|
||||
end, self.sections))
|
||||
end
|
||||
|
||||
function M:help()
|
||||
local text = Text.new({ padding = 1 })
|
||||
|
||||
text:nl():append("# Help ", "Title"):nl()
|
||||
text:append("Press ", "Comment"):append("<q>", "Special"):append(" to close", "Comment"):nl():nl()
|
||||
text:append("# Keymaps ", "Title"):nl():nl()
|
||||
---@type string[]
|
||||
local keys = vim.tbl_keys(self.win.keys)
|
||||
table.sort(keys, function(a, b)
|
||||
local lowa = string.lower(a)
|
||||
local lowb = string.lower(b)
|
||||
if lowa == lowb then
|
||||
return a > b -- Preserve original order for equal strings
|
||||
else
|
||||
return lowa < lowb
|
||||
end
|
||||
end)
|
||||
for _, key in ipairs(keys) do
|
||||
local desc = self.win.keys[key]
|
||||
text:append(" - ", "@punctuation.special.markdown")
|
||||
text:append(key, "Special"):append(" "):append(desc):nl()
|
||||
end
|
||||
text:trim()
|
||||
|
||||
local win = Window.new({
|
||||
type = "float",
|
||||
size = { width = text:width(), height = text:height() },
|
||||
border = "rounded",
|
||||
wo = { cursorline = false },
|
||||
})
|
||||
win:open():focus()
|
||||
text:render(win.buf)
|
||||
vim.bo[win.buf].modifiable = false
|
||||
|
||||
win:map("<esc>", win.close)
|
||||
win:map("q", win.close)
|
||||
end
|
||||
|
||||
function M:is_open()
|
||||
return self.win:valid()
|
||||
end
|
||||
|
||||
function M:open()
|
||||
if self.win:valid() then
|
||||
return self
|
||||
end
|
||||
self
|
||||
:refresh({ update = false, opening = true })
|
||||
:next(function()
|
||||
local count = self:count()
|
||||
if count == 0 then
|
||||
if not self.opts.open_no_results then
|
||||
if self.opts.warn_no_results then
|
||||
Util.warn({
|
||||
"No results for **" .. self.opts.mode .. "**",
|
||||
"Buffer: " .. vim.api.nvim_buf_get_name(self:main().buf),
|
||||
})
|
||||
end
|
||||
return
|
||||
end
|
||||
elseif count == 1 and self.opts.auto_jump then
|
||||
self:jump(self:flatten()[1])
|
||||
return self:close()
|
||||
end
|
||||
self.win:open()
|
||||
self:update()
|
||||
end)
|
||||
:next(self.first_update.resolve)
|
||||
return self
|
||||
end
|
||||
|
||||
function M:close()
|
||||
if vim.api.nvim_get_current_win() == self.win.win then
|
||||
self:goto_main()
|
||||
end
|
||||
Preview.close()
|
||||
self.win:close()
|
||||
return self
|
||||
end
|
||||
|
||||
function M:count()
|
||||
local count = 0
|
||||
for _, section in ipairs(self.sections) do
|
||||
if section.node then
|
||||
count = count + section.node:count()
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function M:flatten()
|
||||
local ret = {}
|
||||
for _, section in ipairs(self.sections) do
|
||||
section.node:flatten(ret)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
-- called when results are updated
|
||||
function M:update()
|
||||
local is_open = self.win:valid()
|
||||
local count = self:count()
|
||||
|
||||
if count == 0 and is_open and self.opts.auto_close then
|
||||
return self:close()
|
||||
end
|
||||
|
||||
if self.opts.auto_open and not is_open and count > 0 then
|
||||
self.win:open()
|
||||
is_open = true
|
||||
end
|
||||
|
||||
if not is_open then
|
||||
return
|
||||
end
|
||||
|
||||
self:render()
|
||||
end
|
||||
|
||||
---@param filter trouble.Filter
|
||||
---@param opts? trouble.ViewFilter.opts
|
||||
function M:filter(filter, opts)
|
||||
opts = opts or {}
|
||||
|
||||
---@type trouble.ViewFilter
|
||||
local view_filter = vim.tbl_deep_extend("force", {
|
||||
id = vim.inspect(filter),
|
||||
filter = filter,
|
||||
data = opts.data,
|
||||
template = opts.template,
|
||||
}, opts)
|
||||
|
||||
if opts.del or (opts.toggle and self._filters[view_filter.id]) then
|
||||
self._filters[view_filter.id] = nil
|
||||
else
|
||||
self._filters[view_filter.id] = view_filter
|
||||
end
|
||||
|
||||
local filters = vim.tbl_count(self._filters) > 0
|
||||
and vim.tbl_map(function(f)
|
||||
return f.filter
|
||||
end, vim.tbl_values(self._filters))
|
||||
or nil
|
||||
|
||||
for _, section in ipairs(self.sections) do
|
||||
section.filter = filters
|
||||
end
|
||||
self:refresh()
|
||||
end
|
||||
|
||||
function M:header()
|
||||
local ret = {} ---@type trouble.Format[][]
|
||||
for _, filter in pairs(self._filters) do
|
||||
local data = vim.tbl_deep_extend("force", {
|
||||
filter = filter.filter,
|
||||
}, type(filter.filter) == "table" and filter.filter or {}, filter.data or {})
|
||||
local template = filter.template or "{hl:Title}Filter:{hl} {filter}"
|
||||
ret[#ret + 1] = self:format(template, data)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param id string
|
||||
function M:get_filter(id)
|
||||
return self._filters[id]
|
||||
end
|
||||
|
||||
---@param template string
|
||||
---@param data table<string,any>
|
||||
function M:format(template, data)
|
||||
data.source = "view"
|
||||
assert(self.opts, "opts is nil")
|
||||
return Format.format(template, { item = data, opts = self.opts })
|
||||
end
|
||||
|
||||
-- render the results
|
||||
function M:render()
|
||||
if not self.win:valid() then
|
||||
return
|
||||
end
|
||||
|
||||
local loc = self:at()
|
||||
local restore_loc = self.opts.restore and self.first_render:is_pending() and M._last[self.opts.mode or ""]
|
||||
if restore_loc then
|
||||
loc = restore_loc
|
||||
end
|
||||
|
||||
-- render sections
|
||||
self.renderer:clear()
|
||||
self.renderer:nl()
|
||||
for _ = 1, vim.tbl_get(self.opts.win, "padding", "top") or 0 do
|
||||
self.renderer:nl()
|
||||
end
|
||||
|
||||
local header = self:header()
|
||||
for _, h in ipairs(header) do
|
||||
for _, ff in ipairs(h) do
|
||||
self.renderer:append(ff.text, ff)
|
||||
end
|
||||
self.renderer:nl()
|
||||
end
|
||||
|
||||
self.renderer:sections(self.sections)
|
||||
self.renderer:trim()
|
||||
|
||||
-- calculate initial folds
|
||||
if self.renderer.foldlevel == nil then
|
||||
local level = vim.wo[self.win.win].foldlevel
|
||||
if level < self.renderer.max_depth then
|
||||
self.renderer:fold_level({ level = level })
|
||||
-- render again to apply folds
|
||||
return self:render()
|
||||
end
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
self.first_render.resolve()
|
||||
end)
|
||||
|
||||
-- render extmarks and restore window view
|
||||
local view = vim.api.nvim_win_call(self.win.win, vim.fn.winsaveview)
|
||||
self.renderer:render(self.win.buf)
|
||||
vim.api.nvim_win_call(self.win.win, function()
|
||||
vim.fn.winrestview(view)
|
||||
end)
|
||||
|
||||
if self.opts.follow and self:follow() then
|
||||
return
|
||||
end
|
||||
|
||||
-- when window is at top, dont move cursor
|
||||
if not restore_loc and view.topline == 1 then
|
||||
return
|
||||
end
|
||||
|
||||
-- fast exit when cursor is already on the right item
|
||||
local new_loc = self:at()
|
||||
if new_loc.node and loc.node and new_loc.node.id == loc.node.id then
|
||||
return
|
||||
end
|
||||
|
||||
-- Move cursor to the same item
|
||||
local cursor = vim.api.nvim_win_get_cursor(self.win.win)
|
||||
local item_row ---@type number?
|
||||
if loc.node then
|
||||
for row, l in pairs(self.renderer._locations) do
|
||||
if loc.node:is(l.node) then
|
||||
item_row = row
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Move cursor to the actual item when found
|
||||
if item_row and item_row ~= cursor[1] then
|
||||
vim.api.nvim_win_set_cursor(self.win.win, { item_row, cursor[2] })
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- When not in the trouble window, try to show the range
|
||||
function M:follow()
|
||||
if not self.win:valid() then -- trouble is closed
|
||||
return
|
||||
end
|
||||
if self.moving:is_active() then -- dont follow when moving
|
||||
return
|
||||
end
|
||||
local current_win = vim.api.nvim_get_current_win()
|
||||
if current_win == self.win.win then -- inside the trouble window
|
||||
return
|
||||
end
|
||||
local Filter = require("trouble.filter")
|
||||
local ctx = { opts = self.opts, main = self:main() }
|
||||
local fname = vim.api.nvim_buf_get_name(ctx.main.buf or 0)
|
||||
local loc = self:at()
|
||||
|
||||
-- check if we're already in the file group
|
||||
local in_group = loc.node and loc.node.item and loc.node.item.filename == fname
|
||||
|
||||
---@type number[]|nil
|
||||
local cursor_item = nil
|
||||
local cursor_group = cursor_item
|
||||
|
||||
for row, l in pairs(self.renderer._locations) do
|
||||
-- only return the group if we're not yet in the group
|
||||
-- and the group's filename matches the current file
|
||||
local is_group = not in_group and l.node and l.node.group and l.node.item and l.node.item.filename == fname
|
||||
if is_group then
|
||||
cursor_group = { row, 1 }
|
||||
end
|
||||
|
||||
-- prefer a full match
|
||||
local is_current = l.item and Filter.is(l.item, { range = true }, ctx)
|
||||
if is_current then
|
||||
cursor_item = { row, 1 }
|
||||
end
|
||||
end
|
||||
|
||||
local cursor = cursor_item or cursor_group
|
||||
if cursor then
|
||||
-- make sure the cursorline is visible
|
||||
vim.wo[self.win.win].cursorline = true
|
||||
vim.api.nvim_win_set_cursor(self.win.win, cursor)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,94 @@
|
||||
local Preview = require("trouble.view.preview")
|
||||
|
||||
---@class trouble.Main
|
||||
---@field win number
|
||||
---@field buf number
|
||||
---@field filename string
|
||||
---@field cursor trouble.Pos
|
||||
|
||||
local M = {}
|
||||
M._main = nil ---@type trouble.Main?
|
||||
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup("trouble.main", { clear = true })
|
||||
vim.api.nvim_create_autocmd({ "BufEnter", "WinEnter" }, {
|
||||
group = group,
|
||||
callback = function()
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
if M._valid(win, buf) then
|
||||
M.set(M._info(win))
|
||||
end
|
||||
end,
|
||||
})
|
||||
M.set(M._find())
|
||||
end
|
||||
|
||||
---@param main trouble.Main
|
||||
function M.set(main)
|
||||
M._main = main
|
||||
end
|
||||
|
||||
function M._valid(win, buf)
|
||||
if not win or not buf then
|
||||
return false
|
||||
end
|
||||
if not vim.api.nvim_win_is_valid(win) or not vim.api.nvim_buf_is_valid(buf) then
|
||||
return false
|
||||
end
|
||||
if vim.api.nvim_win_get_buf(win) ~= buf then
|
||||
return false
|
||||
end
|
||||
if Preview.is_win(win) or vim.w[win].trouble then
|
||||
return false
|
||||
end
|
||||
if vim.api.nvim_win_get_config(win).relative ~= "" then
|
||||
return false
|
||||
end
|
||||
if vim.bo[buf].buftype ~= "" then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@private
|
||||
function M._find()
|
||||
local wins = vim.api.nvim_list_wins()
|
||||
table.insert(wins, 1, vim.api.nvim_get_current_win())
|
||||
for _, win in ipairs(wins) do
|
||||
local b = vim.api.nvim_win_get_buf(win)
|
||||
if M._valid(win, b) then
|
||||
return M._info(win)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@return trouble.Main
|
||||
function M._info(win)
|
||||
local b = vim.api.nvim_win_get_buf(win)
|
||||
return {
|
||||
win = win,
|
||||
buf = b,
|
||||
filename = vim.fs.normalize(vim.api.nvim_buf_get_name(b)),
|
||||
cursor = vim.api.nvim_win_get_cursor(win),
|
||||
}
|
||||
end
|
||||
|
||||
---@param main? trouble.Main
|
||||
---@return trouble.Main?
|
||||
function M.get(main)
|
||||
main = main or M._main
|
||||
|
||||
local valid = main and M._valid(main.win, main.buf)
|
||||
|
||||
if not valid then
|
||||
main = M._find()
|
||||
end
|
||||
-- Always return a main window even if it is not valid
|
||||
main = main or M._info(vim.api.nvim_get_current_win())
|
||||
main.cursor = vim.api.nvim_win_get_cursor(main.win)
|
||||
return main
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,167 @@
|
||||
local Render = require("trouble.view.render")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
local M = {}
|
||||
M.preview = nil ---@type {item:trouble.Item, win:number, buf: number, close:fun()}?
|
||||
|
||||
function M.is_open()
|
||||
return M.preview ~= nil
|
||||
end
|
||||
|
||||
function M.is_win(win)
|
||||
return M.preview and M.preview.win == win
|
||||
end
|
||||
|
||||
function M.item()
|
||||
return M.preview and M.preview.item
|
||||
end
|
||||
|
||||
function M.close()
|
||||
local preview = M.preview
|
||||
M.preview = nil
|
||||
if not preview then
|
||||
return
|
||||
end
|
||||
Render.reset(preview.buf)
|
||||
preview.close()
|
||||
end
|
||||
|
||||
--- Create a preview buffer for an item.
|
||||
--- If the item has a loaded buffer, use that,
|
||||
--- otherwise create a new buffer.
|
||||
---@param item trouble.Item
|
||||
---@param opts? {scratch?:boolean}
|
||||
function M.create(item, opts)
|
||||
opts = opts or {}
|
||||
|
||||
local buf = item.buf or vim.fn.bufnr(item.filename)
|
||||
|
||||
if item.filename and vim.fn.isdirectory(item.filename) == 1 then
|
||||
return
|
||||
end
|
||||
|
||||
-- create a scratch preview buffer when needed
|
||||
if not (buf and vim.api.nvim_buf_is_loaded(buf)) then
|
||||
if opts.scratch then
|
||||
buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
local lines = Util.get_lines({ path = item.filename, buf = item.buf })
|
||||
if not lines then
|
||||
return
|
||||
end
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
local ft = item:get_ft(buf)
|
||||
if ft then
|
||||
local lang = vim.treesitter.language.get_lang(ft)
|
||||
if not pcall(vim.treesitter.start, buf, lang) then
|
||||
vim.bo[buf].syntax = ft
|
||||
end
|
||||
end
|
||||
else
|
||||
item.buf = vim.fn.bufadd(item.filename)
|
||||
buf = item.buf
|
||||
|
||||
if not vim.api.nvim_buf_is_loaded(item.buf) then
|
||||
vim.fn.bufload(item.buf)
|
||||
end
|
||||
if not vim.bo[item.buf].buflisted then
|
||||
vim.bo[item.buf].buflisted = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param view trouble.View
|
||||
---@param item trouble.Item
|
||||
---@param opts? {scratch?:boolean}
|
||||
function M.open(view, item, opts)
|
||||
if M.item() == item then
|
||||
return
|
||||
end
|
||||
if M.preview and M.preview.item.filename ~= item.filename then
|
||||
M.close()
|
||||
end
|
||||
|
||||
if not M.preview then
|
||||
local buf = M.create(item, opts)
|
||||
if not buf then
|
||||
return
|
||||
end
|
||||
|
||||
M.preview = M.preview_win(buf, view)
|
||||
|
||||
M.preview.buf = buf
|
||||
end
|
||||
M.preview.item = item
|
||||
|
||||
Render.reset(M.preview.buf)
|
||||
|
||||
-- make sure we highlight at least one character
|
||||
local end_pos = { item.end_pos[1], item.end_pos[2] }
|
||||
if end_pos[1] == item.pos[1] and end_pos[2] == item.pos[2] then
|
||||
end_pos[2] = end_pos[2] + 1
|
||||
end
|
||||
|
||||
-- highlight the line
|
||||
Util.set_extmark(M.preview.buf, Render.ns, item.pos[1] - 1, 0, {
|
||||
end_row = end_pos[1],
|
||||
hl_group = "CursorLine",
|
||||
hl_eol = true,
|
||||
strict = false,
|
||||
priority = 150,
|
||||
})
|
||||
|
||||
-- highlight the range
|
||||
Util.set_extmark(M.preview.buf, Render.ns, item.pos[1] - 1, item.pos[2], {
|
||||
end_row = end_pos[1] - 1,
|
||||
end_col = end_pos[2],
|
||||
hl_group = "TroublePreview",
|
||||
strict = false,
|
||||
priority = 160,
|
||||
})
|
||||
|
||||
-- no autocmds should be triggered. So LSP's etc won't try to attach in the preview
|
||||
Util.noautocmd(function()
|
||||
if pcall(vim.api.nvim_win_set_cursor, M.preview.win, item.pos) then
|
||||
vim.api.nvim_win_call(M.preview.win, function()
|
||||
vim.cmd("norm! zzzv")
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
return item
|
||||
end
|
||||
|
||||
---@param buf number
|
||||
---@param view trouble.View
|
||||
function M.preview_win(buf, view)
|
||||
if view.opts.preview.type == "main" then
|
||||
local main = view:main()
|
||||
if not main then
|
||||
Util.debug("No main window")
|
||||
return
|
||||
end
|
||||
view.preview_win.opts.win = main.win
|
||||
else
|
||||
view.preview_win.opts.win = view.win.win
|
||||
end
|
||||
|
||||
view.preview_win:open()
|
||||
Util.noautocmd(function()
|
||||
view.preview_win:set_buf(buf)
|
||||
view.preview_win:set_options("win")
|
||||
vim.w[view.preview_win.win].trouble_preview = true
|
||||
end)
|
||||
|
||||
return {
|
||||
win = view.preview_win.win,
|
||||
close = function()
|
||||
view.preview_win:close()
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,264 @@
|
||||
local Cache = require("trouble.cache")
|
||||
local Format = require("trouble.format")
|
||||
local Indent = require("trouble.view.indent")
|
||||
local Text = require("trouble.view.text")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@alias trouble.Render.Location {item?: trouble.Item, node?: trouble.Node, first_line?:boolean}
|
||||
---@class trouble.Render: trouble.Text
|
||||
---@field _locations trouble.Render.Location[] Maps line numbers to items.
|
||||
---@field _folded table<string, true>
|
||||
---@field root_nodes trouble.Node[]
|
||||
---@field foldlevel? number
|
||||
---@field foldenable boolean
|
||||
---@field max_depth number
|
||||
---@field view trouble.View
|
||||
---@field opts trouble.Config
|
||||
local M = setmetatable({}, Text)
|
||||
M.__index = M
|
||||
|
||||
---@class trouble.Render.opts: trouble.Text.opts
|
||||
---@field indent? trouble.Indent.symbols
|
||||
---@field formatters? table<string, trouble.Formatter>
|
||||
|
||||
---@param text_opts trouble.Text.opts
|
||||
---@param opts trouble.Config
|
||||
function M.new(opts, text_opts)
|
||||
local text = Text.new(text_opts)
|
||||
---@type trouble.Render
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
local self = setmetatable(text, M)
|
||||
self.opts = opts
|
||||
self._folded = {}
|
||||
self.foldenable = true
|
||||
self:clear()
|
||||
return self
|
||||
end
|
||||
|
||||
---@alias trouble.Render.fold_opts {action?: "open"|"close"|"toggle", recursive?: boolean}
|
||||
---@param node trouble.Node
|
||||
---@param opts? trouble.Render.fold_opts
|
||||
function M:fold(node, opts)
|
||||
self.foldenable = true
|
||||
opts = opts or {}
|
||||
local action = opts.action or "toggle"
|
||||
if node:is_leaf() and node.parent then
|
||||
node = node.parent
|
||||
end
|
||||
local id = node.id
|
||||
if action == "toggle" then
|
||||
if self._folded[id] then
|
||||
action = "open"
|
||||
else
|
||||
action = "close"
|
||||
end
|
||||
end
|
||||
local stack = { node }
|
||||
while #stack > 0 do
|
||||
local n = table.remove(stack) --[[@as trouble.Node]]
|
||||
if not n:is_leaf() then
|
||||
if action == "open" then
|
||||
self._folded[n.id] = nil
|
||||
local parent = n.parent
|
||||
while parent do
|
||||
self._folded[parent.id] = nil
|
||||
parent = parent.parent
|
||||
end
|
||||
else
|
||||
self._folded[n.id] = true
|
||||
end
|
||||
if opts.recursive then
|
||||
for _, c in ipairs(n.children or {}) do
|
||||
table.insert(stack, c)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param opts {level?:number, add?:number}
|
||||
function M:fold_level(opts)
|
||||
self.foldenable = true
|
||||
self.foldlevel = self.foldlevel or (self.max_depth - 1) or 0
|
||||
if opts.level then
|
||||
self.foldlevel = opts.level
|
||||
end
|
||||
if opts.add then
|
||||
self.foldlevel = self.foldlevel + opts.add
|
||||
end
|
||||
self.foldlevel = math.min(self.max_depth - 1, self.foldlevel)
|
||||
self.foldlevel = math.max(0, self.foldlevel)
|
||||
local stack = {}
|
||||
for _, node in ipairs(self.root_nodes) do
|
||||
table.insert(stack, node)
|
||||
end
|
||||
while #stack > 0 do
|
||||
---@type trouble.Node
|
||||
local node = table.remove(stack)
|
||||
if not node:is_leaf() then
|
||||
if node:depth() > self.foldlevel then
|
||||
self._folded[node.id] = true
|
||||
else
|
||||
self._folded[node.id] = nil
|
||||
end
|
||||
for _, c in ipairs(node.children or {}) do
|
||||
table.insert(stack, c)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M:clear()
|
||||
Cache.langs:clear()
|
||||
self.max_depth = 0
|
||||
self._lines = {}
|
||||
self.ts_regions = {}
|
||||
self._locations = {}
|
||||
self.root_nodes = {}
|
||||
end
|
||||
|
||||
---@param sections trouble.Section[]
|
||||
function M:sections(sections)
|
||||
for _, section in ipairs(sections) do
|
||||
local nodes = section.node and section.node.children
|
||||
if nodes and #nodes > 0 then
|
||||
self:section(section.section, nodes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param section trouble.Section.opts
|
||||
---@param nodes trouble.Node[]
|
||||
function M:section(section, nodes)
|
||||
for n, node in ipairs(nodes) do
|
||||
table.insert(self.root_nodes, node)
|
||||
self.max_depth = math.max(self.max_depth, node:degree())
|
||||
self:node(node, section, Indent.new(self.opts.icons.indent), n == #nodes)
|
||||
end
|
||||
end
|
||||
|
||||
function M:is_folded(node)
|
||||
return self.foldenable and self._folded[node.id]
|
||||
end
|
||||
|
||||
---@param node trouble.Node
|
||||
---@param section trouble.Section.opts
|
||||
---@param indent trouble.Indent
|
||||
---@param is_last boolean
|
||||
function M:node(node, section, indent, is_last)
|
||||
node.folded = self:is_folded(node)
|
||||
if node.item then
|
||||
---@type trouble.Indent.type
|
||||
local symbol = self:is_folded(node) and "fold_closed"
|
||||
or node:depth() == 1 and "fold_open"
|
||||
or is_last and "last"
|
||||
or "middle"
|
||||
symbol = node:depth() == 1 and node:is_leaf() and "ws" or symbol
|
||||
indent:add(symbol)
|
||||
-- self:item(node.item, node, section.groups[node.depth].format, true, indent)
|
||||
self:item(node, section, indent)
|
||||
indent:del()
|
||||
end
|
||||
|
||||
if self:is_folded(node) then
|
||||
return -- don't render children
|
||||
end
|
||||
|
||||
indent:add((is_last or node:depth() == 1) and "ws" or "top")
|
||||
|
||||
for i, n in ipairs(node.children or {}) do
|
||||
self:node(n, section, indent, i == #node.children)
|
||||
end
|
||||
|
||||
indent:del()
|
||||
end
|
||||
|
||||
--- Returns the item and node at the given row.
|
||||
--- For a group, only the node is returned.
|
||||
--- To get the group item used for formatting, use `node.items[1]`.
|
||||
---@param row number
|
||||
function M:at(row)
|
||||
return self._locations[row] or {}
|
||||
end
|
||||
|
||||
---@param node trouble.Node
|
||||
---@param section trouble.Section.opts
|
||||
---@param indent trouble.Indent
|
||||
function M:item(node, section, indent)
|
||||
local item = node.item
|
||||
if not item then
|
||||
return
|
||||
end
|
||||
local is_group = node.group ~= nil
|
||||
local row = self:row()
|
||||
local format_string = node.group and node.group.format or section.format
|
||||
local cache_key = "render:" .. node:depth() .. format_string
|
||||
|
||||
---@type TextSegment[]?
|
||||
local segments = not is_group and item.cache[cache_key]
|
||||
|
||||
if not self.opts.indent_guides then
|
||||
indent = indent:indent()
|
||||
end
|
||||
if self._opts.indent ~= false then
|
||||
self:append(indent)
|
||||
end
|
||||
if segments then
|
||||
self:append(segments)
|
||||
else
|
||||
local format = Format.format(format_string, { item = item, node = node, opts = self.opts })
|
||||
indent:multi_line()
|
||||
for _, ff in ipairs(format) do
|
||||
local text = self._opts.multiline and ff.text or ff.text:gsub("[\n\r]+", " ")
|
||||
local offset ---@type number? start column of the first line
|
||||
local first ---@type string? first line
|
||||
if ff.hl == "ts" then
|
||||
local lang = item:get_lang()
|
||||
if lang then
|
||||
ff.hl = "ts." .. lang
|
||||
else
|
||||
ff.hl = nil
|
||||
end
|
||||
end
|
||||
for l, line in Util.lines(text) do
|
||||
if l == 1 then
|
||||
first = line
|
||||
else
|
||||
-- PERF: most items are single line, so do heavy lifting only when more than one line
|
||||
offset = offset or (self:col({ display = true }) - vim.fn.strdisplaywidth(first or ""))
|
||||
self:nl()
|
||||
self:append(indent)
|
||||
local indent_width = indent:width({ display = true })
|
||||
-- align to item column
|
||||
if offset > indent_width then
|
||||
self:append((" "):rep(offset - indent_width))
|
||||
end
|
||||
end
|
||||
self:append(line, {
|
||||
hl = ff.hl,
|
||||
line = l,
|
||||
})
|
||||
end
|
||||
end
|
||||
-- NOTE:
|
||||
-- * don't cache groups, since they can contain aggregates.
|
||||
-- * don't cache multi-line items
|
||||
-- * don't cache the indent part of the line
|
||||
if not is_group and self:row() == row then
|
||||
item.cache[cache_key] =
|
||||
vim.list_slice(self._lines[#self._lines], self._opts.indent == false and 1 or (#indent + 1))
|
||||
end
|
||||
end
|
||||
|
||||
for r = row, self:row() do
|
||||
self._locations[r] = {
|
||||
first_line = r == row,
|
||||
item = not is_group and item or nil,
|
||||
node = node,
|
||||
}
|
||||
end
|
||||
|
||||
self:nl()
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,174 @@
|
||||
local Filter = require("trouble.filter")
|
||||
local Main = require("trouble.view.main")
|
||||
local Preview = require("trouble.view.preview")
|
||||
local Promise = require("trouble.promise")
|
||||
local Sort = require("trouble.sort")
|
||||
local Sources = require("trouble.sources")
|
||||
local Tree = require("trouble.tree")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@class trouble.Section
|
||||
---@field section trouble.Section.opts
|
||||
---@field finder trouble.Source.get
|
||||
---@field private _main? trouble.Main
|
||||
---@field opts trouble.Config
|
||||
---@field items trouble.Item[]
|
||||
---@field node? trouble.Node
|
||||
---@field fetching boolean
|
||||
---@field filter? trouble.Filter
|
||||
---@field id number
|
||||
---@field on_update? fun(self: trouble.Section)
|
||||
---@field _refresh fun()
|
||||
local M = {}
|
||||
M._id = 0
|
||||
|
||||
---@param section trouble.Section.opts
|
||||
---@param opts trouble.Config
|
||||
function M.new(section, opts)
|
||||
local self = setmetatable({}, { __index = M })
|
||||
self.section = section
|
||||
self.opts = opts
|
||||
M._id = M._id + 1
|
||||
self.id = M._id
|
||||
self.finder = Sources.get(section.source)
|
||||
self.items = {}
|
||||
self:main()
|
||||
|
||||
local _self = Util.weak(self)
|
||||
|
||||
self._refresh = Util.throttle(
|
||||
M.refresh,
|
||||
Util.throttle_opts(opts.throttle.refresh, {
|
||||
ms = 20,
|
||||
is_running = function()
|
||||
local this = _self()
|
||||
return this and this.fetching
|
||||
end,
|
||||
})
|
||||
)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@param opts? {update?: boolean}
|
||||
function M:refresh(opts)
|
||||
-- if self.section.source ~= "lsp.document_symbols" then
|
||||
-- Util.debug("Section Refresh", {
|
||||
-- id = self.id,
|
||||
-- source = self.section.source,
|
||||
-- })
|
||||
-- end
|
||||
self.fetching = true
|
||||
return Promise.new(function(resolve)
|
||||
self:main_call(function(main)
|
||||
local ctx = { opts = self.opts, main = main }
|
||||
self.finder(function(items)
|
||||
items = Filter.filter(items, self.section.filter, ctx)
|
||||
if self.filter then
|
||||
items = Filter.filter(items, self.filter, ctx)
|
||||
end
|
||||
items = Sort.sort(items, self.section.sort, ctx)
|
||||
self.items = items
|
||||
self.node = Tree.build(items, self.section)
|
||||
if not (opts and opts.update == false) then
|
||||
self:update()
|
||||
end
|
||||
resolve(self)
|
||||
end, ctx)
|
||||
end)
|
||||
end)
|
||||
:catch(Util.error)
|
||||
:timeout(2000)
|
||||
:catch(function() end)
|
||||
:finally(function()
|
||||
self.fetching = false
|
||||
end)
|
||||
end
|
||||
|
||||
---@param fn fun(main: trouble.Main)
|
||||
function M:main_call(fn)
|
||||
local main = self:main()
|
||||
|
||||
if not main then
|
||||
return
|
||||
end
|
||||
|
||||
if Preview.is_win(main.win) then
|
||||
return
|
||||
end
|
||||
|
||||
local current = {
|
||||
win = vim.api.nvim_get_current_win(),
|
||||
buf = vim.api.nvim_get_current_buf(),
|
||||
cursor = vim.api.nvim_win_get_cursor(0),
|
||||
}
|
||||
|
||||
if vim.deep_equal(current, main) then
|
||||
fn(main)
|
||||
elseif vim.api.nvim_win_get_buf(main.win) == main.buf then
|
||||
vim.api.nvim_win_call(main.win, function()
|
||||
fn(main)
|
||||
end)
|
||||
else
|
||||
Util.debug({
|
||||
"Main window switched buffers",
|
||||
"Main: " .. vim.api.nvim_buf_get_name(main.buf),
|
||||
"Current: " .. vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(main.win)),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function M:update()
|
||||
if self.on_update then
|
||||
self:on_update()
|
||||
end
|
||||
end
|
||||
|
||||
function M:main()
|
||||
self._main = Main.get(self.opts.pinned and self._main or nil)
|
||||
return self._main
|
||||
end
|
||||
|
||||
function M:augroup()
|
||||
return "trouble.section." .. self.section.source .. "." .. self.id
|
||||
end
|
||||
|
||||
function M:stop()
|
||||
pcall(vim.api.nvim_del_augroup_by_name, self:augroup())
|
||||
end
|
||||
|
||||
function M:listen()
|
||||
local _self = Util.weak(self)
|
||||
|
||||
local group = vim.api.nvim_create_augroup(self:augroup(), { clear = true })
|
||||
for _, event in ipairs(self.section.events or {}) do
|
||||
vim.api.nvim_create_autocmd(event.event, {
|
||||
group = group,
|
||||
pattern = event.pattern,
|
||||
callback = function(e)
|
||||
local this = _self()
|
||||
if not this then
|
||||
return true
|
||||
end
|
||||
if not this.opts.auto_refresh then
|
||||
return
|
||||
end
|
||||
if not vim.api.nvim_buf_is_valid(e.buf) then
|
||||
return
|
||||
end
|
||||
if event.main then
|
||||
local main = this:main()
|
||||
if main and main.buf ~= e.buf then
|
||||
return
|
||||
end
|
||||
end
|
||||
if e.event == "BufEnter" and vim.bo[e.buf].buftype ~= "" then
|
||||
return
|
||||
end
|
||||
this:_refresh()
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,213 @@
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@class TextSegment
|
||||
---@field str string Text
|
||||
---@field hl? string Extmark hl group
|
||||
---@field ts? string TreeSitter language
|
||||
---@field line? number line number in a multiline segment
|
||||
---@field width? number
|
||||
|
||||
---@alias Extmark {hl_group?:string, col?:number, row?:number, end_col?:number}
|
||||
|
||||
---@class trouble.Text.opts
|
||||
---@field padding? number
|
||||
---@field multiline? boolean
|
||||
---@field indent? boolean
|
||||
|
||||
---@class trouble.Text
|
||||
---@field _lines TextSegment[][]
|
||||
---@field _col number
|
||||
---@field _indents string[]
|
||||
---@field _opts trouble.Text.opts
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
M.ns = vim.api.nvim_create_namespace("trouble.text")
|
||||
|
||||
function M.reset(buf)
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
|
||||
end
|
||||
end
|
||||
|
||||
---@param opts? trouble.Text.opts
|
||||
function M.new(opts)
|
||||
local self = setmetatable({}, M)
|
||||
self._lines = {}
|
||||
self._col = 0
|
||||
self._opts = opts or {}
|
||||
self._opts.padding = self._opts.padding or 0
|
||||
self._indents = {}
|
||||
for i = 0, 100, 1 do
|
||||
self._indents[i] = (" "):rep(i)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function M:height()
|
||||
return #self._lines
|
||||
end
|
||||
|
||||
function M:width()
|
||||
local width = 0
|
||||
for _, line in ipairs(self._lines) do
|
||||
local w = 0
|
||||
for _, segment in ipairs(line) do
|
||||
w = w + vim.fn.strdisplaywidth(segment.str)
|
||||
end
|
||||
width = math.max(width, w)
|
||||
end
|
||||
return width + ((self._opts.padding or 0) * 2)
|
||||
end
|
||||
|
||||
---@param text string|TextSegment[]
|
||||
---@param opts? string|{ts?:string, hl?:string, line?:number}
|
||||
function M:append(text, opts)
|
||||
opts = opts or {}
|
||||
if #self._lines == 0 then
|
||||
self:nl()
|
||||
end
|
||||
|
||||
if type(text) == "table" then
|
||||
for _, s in ipairs(text) do
|
||||
s.width = s.width or #s.str
|
||||
self._col = self._col + s.width
|
||||
table.insert(self._lines[#self._lines], s)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
opts = type(opts) == "string" and { hl = opts } or opts
|
||||
if opts.hl == "md" then
|
||||
opts.ts = "markdown"
|
||||
elseif opts.hl and opts.hl:sub(1, 3) == "ts." then
|
||||
opts.ts = opts.hl:sub(4)
|
||||
end
|
||||
|
||||
for l, line in Util.lines(text) do
|
||||
local width = #line
|
||||
self._col = self._col + width
|
||||
table.insert(self._lines[#self._lines], {
|
||||
str = line,
|
||||
width = width,
|
||||
hl = opts.hl,
|
||||
ts = opts.ts,
|
||||
line = opts.line or l,
|
||||
})
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function M:nl()
|
||||
table.insert(self._lines, {})
|
||||
self._col = 0
|
||||
return self
|
||||
end
|
||||
|
||||
---@param opts? {sep?:string}
|
||||
function M:statusline(opts)
|
||||
local sep = opts and opts.sep or " "
|
||||
local lines = {} ---@type string[]
|
||||
for _, line in ipairs(self._lines) do
|
||||
local parts = {}
|
||||
for _, segment in ipairs(line) do
|
||||
local str = segment.str:gsub("%%", "%%%%")
|
||||
if segment.hl then
|
||||
str = ("%%#%s#%s%%*"):format(segment.hl, str)
|
||||
end
|
||||
parts[#parts + 1] = str
|
||||
end
|
||||
table.insert(lines, table.concat(parts, ""))
|
||||
end
|
||||
return table.concat(lines, sep)
|
||||
end
|
||||
|
||||
function M:render(buf)
|
||||
local lines = {}
|
||||
|
||||
local padding = (" "):rep(self._opts.padding)
|
||||
for _, line in ipairs(self._lines) do
|
||||
local parts = { padding }
|
||||
for _, segment in ipairs(line) do
|
||||
parts[#parts + 1] = segment.str
|
||||
end
|
||||
table.insert(lines, table.concat(parts, ""))
|
||||
end
|
||||
|
||||
vim.bo[buf].modifiable = true
|
||||
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
|
||||
local regions = {} ---@type trouble.LangRegions
|
||||
|
||||
for l, line in ipairs(self._lines) do
|
||||
local col = self._opts.padding or 0
|
||||
local row = l - 1
|
||||
|
||||
for _, segment in ipairs(line) do
|
||||
local width = segment.width
|
||||
if segment.ts then
|
||||
regions[segment.ts or ""] = regions[segment.ts] or {}
|
||||
local ts_regions = regions[segment.ts or ""]
|
||||
local last_region = ts_regions[#ts_regions]
|
||||
if not last_region then
|
||||
last_region = {}
|
||||
table.insert(ts_regions, last_region)
|
||||
end
|
||||
-- combine multiline item segments in one region
|
||||
if segment.line and #last_region ~= segment.line - 1 then
|
||||
last_region = {}
|
||||
table.insert(ts_regions, last_region)
|
||||
end
|
||||
table.insert(last_region, {
|
||||
row,
|
||||
col,
|
||||
row,
|
||||
col + width + 1,
|
||||
})
|
||||
elseif segment.hl then
|
||||
Util.set_extmark(buf, M.ns, row, col, {
|
||||
hl_group = segment.hl,
|
||||
end_col = col + width,
|
||||
})
|
||||
end
|
||||
col = col + width
|
||||
end
|
||||
end
|
||||
vim.bo[buf].modifiable = false
|
||||
local changetick = vim.b[buf].changetick
|
||||
|
||||
vim.schedule(function()
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
return
|
||||
end
|
||||
if vim.b[buf].changetick ~= changetick then
|
||||
return
|
||||
end
|
||||
require("trouble.view.treesitter").attach(buf, regions)
|
||||
end)
|
||||
end
|
||||
|
||||
function M:trim()
|
||||
while #self._lines > 0 and #self._lines[#self._lines] == 0 do
|
||||
table.remove(self._lines)
|
||||
end
|
||||
end
|
||||
|
||||
function M:row()
|
||||
return #self._lines == 0 and 1 or #self._lines
|
||||
end
|
||||
|
||||
---@param opts? {display:boolean}
|
||||
function M:col(opts)
|
||||
if opts and opts.display then
|
||||
local ret = 0
|
||||
for _, segment in ipairs(self._lines[#self._lines] or {}) do
|
||||
ret = ret + vim.fn.strdisplaywidth(segment.str)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
return self._col
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,86 @@
|
||||
---@alias trouble.LangRegions table<string, number[][][]>
|
||||
|
||||
local M = {}
|
||||
|
||||
M.cache = {} ---@type table<number, table<string,{parser: vim.treesitter.LanguageTree, highlighter:vim.treesitter.highlighter, enabled:boolean}>>
|
||||
local ns = vim.api.nvim_create_namespace("trouble.treesitter")
|
||||
|
||||
local TSHighlighter = vim.treesitter.highlighter
|
||||
|
||||
local function wrap(name)
|
||||
return function(_, win, buf, ...)
|
||||
if not M.cache[buf] then
|
||||
return false
|
||||
end
|
||||
for _, hl in pairs(M.cache[buf] or {}) do
|
||||
if hl.enabled then
|
||||
TSHighlighter.active[buf] = hl.highlighter
|
||||
TSHighlighter[name](_, win, buf, ...)
|
||||
end
|
||||
end
|
||||
TSHighlighter.active[buf] = nil
|
||||
end
|
||||
end
|
||||
|
||||
M.did_setup = false
|
||||
function M.setup()
|
||||
if M.did_setup then
|
||||
return
|
||||
end
|
||||
M.did_setup = true
|
||||
|
||||
vim.api.nvim_set_decoration_provider(ns, {
|
||||
on_win = wrap("_on_win"),
|
||||
on_line = wrap("_on_line"),
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
group = vim.api.nvim_create_augroup("trouble.treesitter.hl", { clear = true }),
|
||||
callback = function(ev)
|
||||
M.cache[ev.buf] = nil
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param buf number
|
||||
---@param regions trouble.LangRegions
|
||||
function M.attach(buf, regions)
|
||||
M.setup()
|
||||
M.cache[buf] = M.cache[buf] or {}
|
||||
for lang in pairs(M.cache[buf]) do
|
||||
M.cache[buf][lang].enabled = regions[lang] ~= nil
|
||||
end
|
||||
|
||||
for lang in pairs(regions) do
|
||||
M._attach_lang(buf, lang, regions[lang])
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf number
|
||||
---@param lang? string
|
||||
function M._attach_lang(buf, lang, regions)
|
||||
lang = lang or "markdown"
|
||||
lang = lang == "markdown" and "markdown_inline" or lang
|
||||
|
||||
M.cache[buf] = M.cache[buf] or {}
|
||||
|
||||
if not M.cache[buf][lang] then
|
||||
local ok, parser = pcall(vim.treesitter.get_parser, buf, lang)
|
||||
if not ok then
|
||||
local msg = "nvim-treesitter parser missing `" .. lang .. "`"
|
||||
vim.notify_once(msg, vim.log.levels.WARN, { title = "trouble.nvim" })
|
||||
return
|
||||
end
|
||||
|
||||
M.cache[buf][lang] = {
|
||||
parser = parser,
|
||||
highlighter = TSHighlighter.new(parser),
|
||||
}
|
||||
end
|
||||
M.cache[buf][lang].enabled = true
|
||||
local parser = M.cache[buf][lang].parser
|
||||
|
||||
parser:set_included_regions(regions)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,389 @@
|
||||
local Main = require("trouble.view.main")
|
||||
local Util = require("trouble.util")
|
||||
|
||||
---@class trouble.Window.split
|
||||
---@field type "split"
|
||||
---@field relative "editor" | "win" cursor is only valid for float
|
||||
---@field size number | {width:number, height:number} when a table is provided, either the width or height is used based on the position
|
||||
---@field position "top" | "bottom" | "left" | "right"
|
||||
|
||||
---@class trouble.Window.float
|
||||
---@field type "float"
|
||||
---@field relative "editor" | "win" | "cursor" cursor is only valid for float
|
||||
---@field size {width: number, height: number}
|
||||
---@field position {[1]: number, [2]: number}
|
||||
---@field anchor? string
|
||||
---@field focusable? boolean
|
||||
---@field zindex? integer
|
||||
---@field border? any
|
||||
---@field title? string|{[1]: string, [2]: string}
|
||||
---@field title_pos? string
|
||||
---@field footer? string|{[1]: string, [2]: string}
|
||||
---@field footer_pos? string
|
||||
---@field fixed? boolean
|
||||
|
||||
---@class trouble.Window.main
|
||||
---@field type "main"
|
||||
|
||||
---@class trouble.Window.base
|
||||
---@field padding? {top?:number, left?:number}
|
||||
---@field wo? vim.wo
|
||||
---@field bo? vim.bo
|
||||
---@field minimal? boolean (defaults to true)
|
||||
---@field win? number
|
||||
---@field on_mount? fun(self: trouble.Window)
|
||||
---@field on_close? fun(self: trouble.Window)
|
||||
|
||||
---@alias trouble.Window.opts trouble.Window.base|trouble.Window.split|trouble.Window.float|trouble.Window.main
|
||||
|
||||
---@class trouble.Window
|
||||
---@field opts trouble.Window.opts
|
||||
---@field win? number
|
||||
---@field buf number
|
||||
---@field id number
|
||||
---@field keys table<string, string>
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
local _id = 0
|
||||
local function next_id()
|
||||
_id = _id + 1
|
||||
return _id
|
||||
end
|
||||
|
||||
local split_commands = {
|
||||
editor = {
|
||||
top = "topleft",
|
||||
right = "vertical botright",
|
||||
bottom = "botright",
|
||||
left = "vertical topleft",
|
||||
},
|
||||
win = {
|
||||
top = "aboveleft",
|
||||
right = "vertical rightbelow",
|
||||
bottom = "belowright",
|
||||
left = "vertical leftabove",
|
||||
},
|
||||
}
|
||||
|
||||
local float_options = {
|
||||
"anchor",
|
||||
"border",
|
||||
"bufpos",
|
||||
"col",
|
||||
"external",
|
||||
"fixed",
|
||||
"focusable",
|
||||
"footer",
|
||||
"footer_pos",
|
||||
"height",
|
||||
"hide",
|
||||
"noautocmd",
|
||||
"relative",
|
||||
"row",
|
||||
"style",
|
||||
"title",
|
||||
"title_pos",
|
||||
"width",
|
||||
"win",
|
||||
"zindex",
|
||||
}
|
||||
|
||||
---@type trouble.Window.opts
|
||||
local defaults = {
|
||||
padding = { top = 0, left = 1 },
|
||||
bo = {
|
||||
bufhidden = "wipe",
|
||||
filetype = "trouble",
|
||||
buftype = "nofile",
|
||||
},
|
||||
wo = {
|
||||
winbar = "",
|
||||
winblend = 0,
|
||||
},
|
||||
}
|
||||
|
||||
M.FOLDS = {
|
||||
wo = {
|
||||
foldcolumn = "0",
|
||||
foldenable = false,
|
||||
foldlevel = 99,
|
||||
foldmethod = "manual",
|
||||
},
|
||||
}
|
||||
|
||||
---@type trouble.Window.opts
|
||||
local minimal = {
|
||||
wo = {
|
||||
cursorcolumn = false,
|
||||
cursorline = true,
|
||||
cursorlineopt = "both",
|
||||
fillchars = "eob: ",
|
||||
list = false,
|
||||
number = false,
|
||||
relativenumber = false,
|
||||
signcolumn = "no",
|
||||
spell = false,
|
||||
winbar = "",
|
||||
statuscolumn = "",
|
||||
winfixheight = true,
|
||||
winfixwidth = true,
|
||||
winhighlight = "Normal:TroubleNormal,NormalNC:TroubleNormalNC,EndOfBuffer:TroubleNormal",
|
||||
wrap = false,
|
||||
},
|
||||
}
|
||||
|
||||
---@param opts? trouble.Window.opts
|
||||
function M.new(opts)
|
||||
local self = setmetatable({}, M)
|
||||
self.id = next_id()
|
||||
opts = opts or {}
|
||||
|
||||
if opts.minimal == nil then
|
||||
opts.minimal = opts.type ~= "main"
|
||||
end
|
||||
|
||||
opts = vim.tbl_deep_extend("force", {}, defaults, opts.minimal and minimal or {}, opts or {})
|
||||
opts.type = opts.type or "split"
|
||||
if opts.type == "split" then
|
||||
opts.relative = opts.relative or "editor"
|
||||
opts.position = opts.position or "bottom"
|
||||
opts.size = opts.size or (opts.position == "bottom" or opts.position == "top") and 10 or 30
|
||||
opts.win = opts.win or vim.api.nvim_get_current_win()
|
||||
elseif opts.type == "float" then
|
||||
opts.relative = opts.relative or "editor"
|
||||
opts.size = opts.size or { width = 0.8, height = 0.8 }
|
||||
opts.position = type(opts.position) == "table" and opts.position or { 0.5, 0.5 }
|
||||
elseif opts.type == "main" then
|
||||
opts.type = "float"
|
||||
opts.relative = "win"
|
||||
opts.position = { 0, 0 }
|
||||
opts.size = { width = 1, height = 1 }
|
||||
opts.wo.winhighlight = "Normal:Normal"
|
||||
end
|
||||
self.opts = opts
|
||||
return self
|
||||
end
|
||||
|
||||
---@param clear? boolean
|
||||
function M:augroup(clear)
|
||||
return vim.api.nvim_create_augroup("trouble.window." .. self.id, { clear = clear == true })
|
||||
end
|
||||
|
||||
function M:parent_size()
|
||||
if self.opts.relative == "editor" or self.opts.relative == "cursor" then
|
||||
return { width = vim.o.columns, height = vim.o.lines }
|
||||
end
|
||||
local ret = {
|
||||
width = vim.api.nvim_win_get_width(self.opts.win),
|
||||
height = vim.api.nvim_win_get_height(self.opts.win),
|
||||
}
|
||||
-- account for winbar
|
||||
if vim.wo[self.opts.win].winbar ~= "" then
|
||||
ret.height = ret.height - 1
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param type "win" | "buf"
|
||||
function M:set_options(type)
|
||||
local opts = type == "win" and self.opts.wo or self.opts.bo
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
for k, v in pairs(opts or {}) do
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
local ok, err = pcall(vim.api.nvim_set_option_value, k, v, type == "win" and {
|
||||
scope = "local",
|
||||
win = self.win,
|
||||
} or { buf = self.buf })
|
||||
if not ok then
|
||||
Util.error("Error setting option `" .. k .. "=" .. v .. "`\n\n" .. err)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M:mount()
|
||||
self.keys = {}
|
||||
self.buf = vim.api.nvim_create_buf(false, true)
|
||||
self:set_options("buf")
|
||||
if self.opts.type == "split" then
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
self:mount_split(self.opts)
|
||||
else
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
self:mount_float(self.opts)
|
||||
end
|
||||
|
||||
self:set_options("win")
|
||||
|
||||
self:on({ "BufWinLeave" }, vim.schedule_wrap(self.check_alien))
|
||||
|
||||
self:on("WinClosed", function()
|
||||
if self.opts.on_close then
|
||||
self.opts.on_close(self)
|
||||
end
|
||||
self:augroup(true)
|
||||
end, { win = true })
|
||||
|
||||
if self.opts.on_mount then
|
||||
self.opts.on_mount(self)
|
||||
end
|
||||
end
|
||||
|
||||
function M:set_buf(buf)
|
||||
self.buf = buf
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_set_buf(self.win, buf)
|
||||
end
|
||||
end
|
||||
|
||||
function M:check_alien()
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
local buf = vim.api.nvim_win_get_buf(self.win)
|
||||
if buf ~= self.buf then
|
||||
-- move the alien buffer to another window
|
||||
local main = Main:get()
|
||||
if main then
|
||||
vim.api.nvim_win_set_buf(main.win, buf)
|
||||
-- restore the trouble window
|
||||
self:close()
|
||||
self:open()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M:close()
|
||||
pcall(vim.api.nvim_win_close, self.win, true)
|
||||
self:augroup(true)
|
||||
self.win = nil
|
||||
end
|
||||
|
||||
function M:open()
|
||||
if self:valid() then
|
||||
return self
|
||||
end
|
||||
self:close()
|
||||
self:mount()
|
||||
return self
|
||||
end
|
||||
|
||||
function M:valid()
|
||||
return self.win
|
||||
and vim.api.nvim_win_is_valid(self.win)
|
||||
and self.buf
|
||||
and vim.api.nvim_buf_is_valid(self.buf)
|
||||
and vim.api.nvim_win_get_buf(self.win) == self.buf
|
||||
end
|
||||
|
||||
---@param opts trouble.Window.split|trouble.Window.base
|
||||
function M:mount_split(opts)
|
||||
if self.opts.win and not vim.api.nvim_win_is_valid(self.opts.win) then
|
||||
self.opts.win = 0
|
||||
end
|
||||
local parent_size = self:parent_size()
|
||||
local size = opts.size
|
||||
if type(size) == "table" then
|
||||
size = opts.position == "left" or opts.position == "right" and size.width or size.height
|
||||
end
|
||||
if size <= 1 then
|
||||
local vertical = opts.position == "left" or opts.position == "right"
|
||||
size = math.floor(parent_size[vertical and "width" or "height"] * size)
|
||||
end
|
||||
local cmd = split_commands[opts.relative][opts.position]
|
||||
Util.noautocmd(function()
|
||||
vim.api.nvim_win_call(opts.win, function()
|
||||
vim.cmd("silent noswapfile " .. cmd .. " " .. size .. "split")
|
||||
vim.api.nvim_win_set_buf(0, self.buf)
|
||||
self.win = vim.api.nvim_get_current_win()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param opts trouble.Window.float|trouble.Window.base
|
||||
function M:mount_float(opts)
|
||||
local parent_size = self:parent_size()
|
||||
---@type vim.api.keyset.win_config
|
||||
local config = {}
|
||||
for _, v in ipairs(float_options) do
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
config[v] = opts[v]
|
||||
end
|
||||
config.focusable = true
|
||||
config.height = opts.size.height <= 1 and math.floor(parent_size.height * opts.size.height) or opts.size.height
|
||||
config.width = opts.size.width <= 1 and math.floor(parent_size.width * opts.size.width) or opts.size.width
|
||||
|
||||
config.row = math.abs(opts.position[1]) <= 1 and math.floor((parent_size.height - config.height) * opts.position[1])
|
||||
or opts.position[1]
|
||||
config.row = config.row < 0 and (parent_size.height + config.row) or config.row
|
||||
|
||||
config.col = math.abs(opts.position[2]) <= 1 and math.floor((parent_size.width - config.width) * opts.position[2])
|
||||
or opts.position[2]
|
||||
config.col = config.col < 0 and (parent_size.width + config.col) or config.col
|
||||
if config.relative ~= "win" then
|
||||
config.win = nil
|
||||
end
|
||||
|
||||
self.win = vim.api.nvim_open_win(self.buf, false, config)
|
||||
end
|
||||
|
||||
function M:focus()
|
||||
if self:valid() then
|
||||
vim.api.nvim_set_current_win(self.win)
|
||||
end
|
||||
end
|
||||
|
||||
---@param events string|string[]
|
||||
---@param fn fun(self:trouble.Window, event:{buf:number}):boolean?
|
||||
---@param opts? vim.api.keyset.create_autocmd | {buffer: false, win?:boolean}
|
||||
function M:on(events, fn, opts)
|
||||
opts = opts or {}
|
||||
if opts.win then
|
||||
opts.pattern = self.win .. ""
|
||||
opts.win = nil
|
||||
elseif opts.buffer == nil then
|
||||
opts.buffer = self.buf
|
||||
elseif opts.buffer == false then
|
||||
opts.buffer = nil
|
||||
end
|
||||
if opts.pattern then
|
||||
opts.buffer = nil
|
||||
end
|
||||
local _self = Util.weak(self)
|
||||
opts.callback = function(e)
|
||||
local this = _self()
|
||||
if not this then
|
||||
-- delete the autocmd
|
||||
return true
|
||||
end
|
||||
return fn(this, e)
|
||||
end
|
||||
opts.group = self:augroup()
|
||||
vim.api.nvim_create_autocmd(events, opts)
|
||||
end
|
||||
|
||||
---@param key string
|
||||
---@param fn fun(self: trouble.Window):any
|
||||
---@param opts? string|vim.keymap.set.Opts|{mode?:string}
|
||||
function M:map(key, fn, opts)
|
||||
opts = vim.tbl_deep_extend("force", {
|
||||
buffer = self.buf,
|
||||
nowait = true,
|
||||
mode = "n",
|
||||
}, type(opts) == "string" and { desc = opts } or opts or {})
|
||||
local mode = opts.mode
|
||||
opts.mode = nil
|
||||
---@cast opts vim.keymap.set.Opts
|
||||
if not self:valid() then
|
||||
error("Cannot create a keymap for an invalid window")
|
||||
end
|
||||
|
||||
self.keys[key] = opts.desc or key
|
||||
local weak_self = Util.weak(self)
|
||||
vim.keymap.set(mode, key, function()
|
||||
if weak_self() then
|
||||
return fn(weak_self())
|
||||
end
|
||||
end, opts)
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user