1

Update generated neovim config

This commit is contained in:
2024-08-15 14:28:54 +02:00
parent 07409c223d
commit 25cfcf2941
3809 changed files with 351157 additions and 0 deletions

View File

@ -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,
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
})

View File

@ -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

View File

@ -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,
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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