1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,34 @@
local util = require("trouble.util")
local M = {}
local links = {
TextError = "TroubleText",
TextWarning = "TroubleText",
TextInformation = "TroubleText",
TextHint = "TroubleText",
Text = "Normal",
File = "Directory",
Source = "Comment",
Code = "Comment",
Location = "LineNr",
FoldIcon = "CursorLineNr",
Normal = "Normal",
Count = "TabLineSel",
Preview = "Search",
Indent = "LineNr",
SignOther = "TroubleSignInformation",
}
function M.setup()
for k, v in pairs(links) do
vim.api.nvim_command("hi def link Trouble" .. k .. " " .. v)
end
for _, severity in pairs(util.severity) do
vim.api.nvim_command("hi def link Trouble" .. severity .. " " .. util.get_severity_label(severity))
vim.api.nvim_command("hi def link TroubleSign" .. severity .. " " .. util.get_severity_label(severity, "Sign"))
end
end
return M

View File

@ -0,0 +1,83 @@
local M = {}
M.namespace = vim.api.nvim_create_namespace("Trouble")
---@class TroubleOptions
---@field buf number|nil
---@field win number|nil
---@field severity lsp.DiagnosticSeverity|nil
-- TODO: make some options configurable per mode
-- TODO: make it possible to have multiple trouble lists open at the same time
local defaults = {
debug = false,
cmd_options = {},
group = true, -- group results by file
padding = true, -- add an extra new line on top of the list
position = "bottom", -- position of the list can be: bottom, top, left, right
height = 10, -- height of the trouble list when position is top or bottom
width = 50, -- width of the list when position is left or right
icons = true, -- use devicons for filenames
mode = "workspace_diagnostics", -- "workspace_diagnostics", "document_diagnostics", "quickfix", "lsp_references", "loclist"
severity = nil, -- nil (ALL) or vim.diagnostic.severity.ERROR | WARN | INFO | HINT
fold_open = "", -- icon used for open folds
fold_closed = "", -- icon used for closed folds
cycle_results = true, -- cycle item list when reaching beginning or end of list
action_keys = { -- key mappings for actions in the trouble list
close = "q", -- close the list
cancel = "<esc>", -- cancel the preview and get back to your last window / buffer / cursor
refresh = "r", -- manually refresh
jump = { "<cr>", "<tab>", "<2-leftmouse>" }, -- jump to the diagnostic or open / close folds
open_split = { "<c-x>" }, -- open buffer in new split
open_vsplit = { "<c-v>" }, -- open buffer in new vsplit
open_tab = { "<c-t>" }, -- open buffer in new tab
jump_close = { "o" }, -- jump to the diagnostic and close the list
toggle_mode = "m", -- toggle between "workspace" and "document" mode
switch_severity = "s", -- switch "diagnostics" severity filter level to ALL / HINT / INFO / WARN / ERROR
toggle_preview = "P", -- toggle auto_preview
hover = "K", -- opens a small popup with the full multiline message
preview = "p", -- preview the diagnostic location
open_code_href = "c", -- if present, open a URI with more information about the diagnostic error
close_folds = { "zM", "zm" }, -- close all folds
open_folds = { "zR", "zr" }, -- open all folds
toggle_fold = { "zA", "za" }, -- toggle fold of current file
previous = "k", -- preview item
next = "j", -- next item
help = "?", -- help menu
},
multiline = true, -- render multi-line messages
indent_lines = true, -- add an indent guide below the fold icons
win_config = { border = "single" }, -- window configuration for floating windows. See |nvim_open_win()|.
auto_open = false, -- automatically open the list when you have diagnostics
auto_close = false, -- automatically close the list when you have no diagnostics
auto_preview = true, -- automatically preview the location of the diagnostic. <esc> to close preview and go back to last window
auto_fold = false, -- automatically fold a file trouble list at creation
auto_jump = { "lsp_definitions" }, -- for the given modes, automatically jump if there is only a single result
include_declaration = { "lsp_references", "lsp_implementations", "lsp_definitions" }, -- for the given modes, include the declaration of the current symbol in the results
signs = {
-- icons / text used for a diagnostic
error = "",
warning = "",
hint = "",
information = "",
other = "",
},
use_diagnostic_signs = false, -- enabling this will use the signs defined in your lsp client
sort_keys = {
"severity",
"filename",
"lnum",
"col",
},
}
---@type TroubleOptions
M.options = {}
---@return TroubleOptions
function M.setup(options)
M.options = vim.tbl_deep_extend("force", {}, defaults, options or {})
end
M.setup()
return M

View File

@ -0,0 +1,24 @@
local config = require("trouble.config")
local M = {}
M.folded = {}
function M.is_folded(filename)
local fold = M.folded[filename]
return (fold == nil and config.options.auto_fold == true) or (fold == true)
end
function M.toggle(filename)
M.folded[filename] = not M.is_folded(filename)
end
function M.close(filename)
M.folded[filename] = true
end
function M.open(filename)
M.folded[filename] = false
end
return M

View File

@ -0,0 +1,300 @@
local View = require("trouble.view")
local config = require("trouble.config")
local colors = require("trouble.colors")
local util = require("trouble.util")
colors.setup()
local Trouble = {}
local view
function Trouble.is_open()
return view and view:is_valid() or false
end
function Trouble.setup(options)
if vim.fn.has("nvim-0.7.2") == 0 then
util.error("Trouble needs Neovim >= 0.7.2")
return
end
config.setup(options)
util.fix_mode(config.options)
colors.setup()
end
function Trouble.close()
if Trouble.is_open() then
view:close()
end
end
local function get_opts(...)
local args = { ... }
if (vim.islist or vim.tbl_islist)(args) and #args == 1 and type(args[1]) == "table" then
args = args[1]
end
local opts = {}
for key, value in pairs(args) do
if type(key) == "number" then
local k, v = value:match("^(.*)=(.*)$")
if k then
opts[k] = v
elseif opts.mode then
util.error("unknown option " .. value)
else
opts.mode = value
end
else
opts[key] = value
end
end
opts = opts or {}
util.fix_mode(opts)
config.options.cmd_options = opts
return opts
end
function Trouble.open(...)
local opts = get_opts(...)
if opts.mode and (opts.mode ~= config.options.mode) then
config.options.mode = opts.mode
end
if opts.severity and (opts.severity ~= config.options.severity) then
config.options.severity = opts.severity
end
opts.focus = (opts.focus == nil and not opts.auto) and true or opts.focus
opts.on_open = true
if Trouble.is_open() then
Trouble.refresh(opts)
elseif not opts.auto and vim.tbl_contains(config.options.auto_jump, opts.mode) then
require("trouble.providers").get(vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf(), function(results)
if #results == 1 then
util.jump_to_item(opts.win, opts.precmd, results[1])
elseif #results > 0 then
view = View.create(opts)
end
end, config.options)
else
view = View.create(opts)
end
end
function Trouble.toggle(...)
local opts = get_opts(...)
if opts.mode and (opts.mode ~= config.options.mode) then
config.options.mode = opts.mode
Trouble.open(...)
return
end
if Trouble.is_open() then
Trouble.close()
else
Trouble.open(...)
end
end
function Trouble.help()
local lines = { "# Key Bindings" }
local height = 1
for command, key in pairs(config.options.action_keys) do
if type(key) == "table" then
key = table.concat(key, " | ")
end
table.insert(lines, " * **" .. key .. "** " .. command:gsub("_", " "))
height = height + 1
end
-- help
vim.lsp.util.open_floating_preview(lines, "markdown", config.options.win_config)
end
local updater = util.debounce(100, function()
-- buff might have been closed during the debounce
if not Trouble.is_open() then
util.debug("refresh: not open anymore")
return
end
util.debug("refresh: auto")
view:update({ auto = true })
end)
function Trouble.refresh(opts)
opts = opts or {}
-- dont do an update if this is an automated refresh from a different provider
if opts.auto then
if opts.provider == "diagnostics" and config.options.mode == "document_diagnostics" then
opts.provider = "document_diagnostics"
elseif opts.provider == "diagnostics" and config.options.mode == "workspace_diagnostics" then
opts.provider = "workspace_diagnostics"
elseif opts.provider == "qf" and config.options.mode == "quickfix" then
opts.provider = "quickfix"
elseif opts.provider == "qf" and config.options.mode == "loclist" then
opts.provider = "loclist"
end
if opts.provider ~= config.options.mode then
return
end
end
if Trouble.is_open() then
if opts.auto then
updater()
else
util.debug("refresh")
view:update(opts)
end
elseif opts.auto and config.options.auto_open and opts.provider == config.options.mode then
require("trouble.providers").get(vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf(), function(results)
if #results > 0 then
Trouble.open(opts)
end
end, config.options)
end
end
function Trouble.action(action)
if action == "toggle_mode" then
if config.options.mode == "document_diagnostics" then
config.options.mode = "workspace_diagnostics"
elseif config.options.mode == "workspace_diagnostics" then
config.options.mode = "document_diagnostics"
end
action = "refresh"
end
if action == "switch_severity" then
if config.options.severity == nil then
config.options.severity = vim.diagnostic.severity.ERROR
elseif config.options.severity < 4 then
config.options.severity = config.options.severity + 1
else
config.options.severity = nil
end
action = "refresh"
end
if view and action == "on_win_enter" then
view:on_win_enter()
end
if not Trouble.is_open() then
return Trouble
end
if action == "hover" then
view:hover()
end
if action == "jump" then
view:jump()
elseif action == "open_split" then
view:jump({ precmd = "split" })
elseif action == "open_vsplit" then
view:jump({ precmd = "vsplit" })
elseif action == "open_tab" then
view:jump({ precmd = "tabe" })
end
if action == "jump_close" then
view:jump()
Trouble.close()
end
if action == "open_folds" then
Trouble.refresh({ open_folds = true })
end
if action == "close_folds" then
Trouble.refresh({ close_folds = true })
end
if action == "toggle_fold" then
view:toggle_fold()
end
if action == "on_enter" then
view:on_enter()
end
if action == "on_leave" then
view:on_leave()
end
if action == "cancel" then
view:switch_to_parent()
end
if action == "next" then
view:next_item()
return Trouble
end
if action == "previous" then
view:previous_item()
return Trouble
end
if action == "first" then
view:first_item()
return Trouble
end
if action == "last" then
view:last_item()
return Trouble
end
if action == "toggle_preview" then
config.options.auto_preview = not config.options.auto_preview
if not config.options.auto_preview then
view:close_preview()
else
action = "preview"
end
end
if action == "auto_preview" and config.options.auto_preview then
action = "preview"
end
if action == "preview" then
view:preview()
end
if action == "open_code_href" then
view:open_code_href()
end
if Trouble[action] then
Trouble[action]()
end
return Trouble
end
function Trouble.next(opts)
util.fix_mode(opts)
if view then
view:next_item(opts)
end
end
function Trouble.previous(opts)
util.fix_mode(opts)
if view then
view:previous_item(opts)
end
end
function Trouble.first(opts)
util.fix_mode(opts)
if view then
view:first_item(opts)
end
end
function Trouble.last(opts)
util.fix_mode(opts)
if view then
view:last_item(opts)
end
end
function Trouble.get_items()
if view ~= nil then
return view.items
else
return {}
end
end
return Trouble

View File

@ -0,0 +1,59 @@
local util = require("trouble.util")
---@class Lsp
local M = {}
local severity = {
[1] = "ERROR",
[2] = "WARN",
[3] = "INFO",
[4] = "HINT",
}
---@param options TroubleOptions
---@return Item[]
function M.diagnostics(_, buf, cb, options)
if options.mode == "workspace_diagnostics" then
buf = nil
end
local items = {}
if vim.diagnostic then
local diags = vim.diagnostic.get(buf, { severity = options.severity })
for _, item in ipairs(diags) do
table.insert(items, util.process_item(item))
end
else
---@diagnostic disable-next-line: deprecated
local diags = buf and { [buf] = vim.lsp.diagnostic.get(buf) } or vim.lsp.diagnostic.get_all()
items = util.locations_to_items(diags, 1)
end
local messages = {}
if severity[options.severity] then
table.insert(messages, { text = "filter:", group = "Information" })
table.insert(messages, { text = severity[options.severity], group = "Sign" .. util.severity[options.severity] })
end
cb(items, messages)
end
function M.get_signs()
local signs = {}
for _, v in pairs(util.severity) do
if v ~= "Other" then
-- pcall to catch entirely unbound or cleared out sign hl group
local status, sign = pcall(function()
return vim.trim(vim.fn.sign_getdefined(util.get_severity_label(v, "Sign"))[1].text)
end)
if not status then
sign = v:sub(1, 1)
end
signs[string.lower(v)] = sign
end
end
return signs
end
return M

View File

@ -0,0 +1,94 @@
local util = require("trouble.util")
local qf = require("trouble.providers.qf")
local telescope = require("trouble.providers.telescope")
local lsp = require("trouble.providers.lsp")
local diagnostic = require("trouble.providers.diagnostic")
local M = {}
M.providers = {
workspace_diagnostics = diagnostic.diagnostics,
document_diagnostics = diagnostic.diagnostics,
lsp_references = lsp.references,
lsp_implementations = lsp.implementations,
lsp_definitions = lsp.definitions,
lsp_type_definitions = lsp.type_definitions,
quickfix = qf.qflist,
loclist = qf.loclist,
telescope = telescope.telescope,
}
---@param options TroubleOptions
function M.get(win, buf, cb, options)
local name = options.mode
local provider = M.providers[name]
if not provider then
local ok, mod = pcall(require, "trouble.providers." .. name)
if ok then
M.providers[name] = mod
provider = mod
end
end
if not provider then
util.error(("invalid provider %q"):format(name))
return {}
end
local sort_keys = vim.list_extend({
function(item)
local cwd = vim.loop.fs_realpath(vim.fn.getcwd())
local path = vim.loop.fs_realpath(item.filename)
if not path then
return 200
end
local ret = string.find(path, cwd, 1, true) == 1 and 10 or 100
-- prefer non-hidden files
if string.find(path, ".") then
ret = ret + 1
end
return ret
end,
}, options.sort_keys)
provider(win, buf, function(items, messages)
table.sort(items, function(a, b)
for _, key in ipairs(sort_keys) do
local ak = type(key) == "string" and a[key] or key(a)
local bk = type(key) == "string" and b[key] or key(b)
if ak ~= bk then
return ak < bk
end
end
end)
cb(items, messages)
end, options)
end
---@param items Item[]
---@return table<string, Item[]>
function M.group(items)
local keys = {}
local keyid = 0
local groups = {}
for _, item in ipairs(items) do
if groups[item.filename] == nil then
groups[item.filename] = { filename = item.filename, items = {} }
keys[item.filename] = keyid
keyid = keyid + 1
end
table.insert(groups[item.filename].items, item)
end
local ret = {}
for _, group in pairs(groups) do
table.insert(ret, group)
end
table.sort(ret, function(a, b)
return keys[a.filename] < keys[b.filename]
end)
return ret
end
return M

View File

@ -0,0 +1,93 @@
local lsp = require("vim.lsp")
local util = require("trouble.util")
---@class Lsp
local M = {}
local function lsp_buf_request(buf, method, params, handler)
lsp.buf_request(buf, method, params, function(err, m, result)
handler(err, method == m and result or m)
end)
end
---@return Item[]
function M.references(win, buf, cb, options)
local method = "textDocument/references"
local params = util.make_position_params(win, buf)
params.context = { includeDeclaration = vim.tbl_contains(options.include_declaration, options.mode) }
lsp_buf_request(buf, method, params, function(err, result)
if err then
util.error("an error happened getting references: " .. err.message)
return cb({})
end
if result == nil or #result == 0 then
return cb({})
end
local ret = util.locations_to_items({ result }, 0)
cb(ret)
end)
end
---@return Item[]
function M.implementations(win, buf, cb, options)
local method = "textDocument/implementation"
local params = util.make_position_params(win, buf)
params.context = { includeDeclaration = vim.tbl_contains(options.include_declaration, options.mode) }
lsp_buf_request(buf, method, params, function(err, result)
if err then
util.error("an error happened getting implementation: " .. err.message)
return cb({})
end
if result == nil or #result == 0 then
return cb({})
end
local ret = util.locations_to_items({ result }, 0)
cb(ret)
end)
end
---@return Item[]
function M.definitions(win, buf, cb, options)
local method = "textDocument/definition"
local params = util.make_position_params(win, buf)
params.context = { includeDeclaration = vim.tbl_contains(options.include_declaration, options.mode) }
lsp_buf_request(buf, method, params, function(err, result)
if err then
util.error("an error happened getting definitions: " .. err.message)
return cb({})
end
if result == nil or #result == 0 then
return cb({})
end
for _, value in ipairs(result) do
value.uri = value.targetUri or value.uri
value.range = value.targetSelectionRange or value.range
end
local ret = util.locations_to_items({ result }, 0)
cb(ret)
end)
end
---@return Item[]
function M.type_definitions(win, buf, cb, _options)
local method = "textDocument/typeDefinition"
local params = util.make_position_params(win, buf)
lsp_buf_request(buf, method, params, function(err, result)
if err then
util.error("an error happened getting type definitions: " .. err.message)
return cb({})
end
if result == nil or #result == 0 then
return cb({})
end
for _, value in ipairs(result) do
value.uri = value.targetUri or value.uri
value.range = value.targetSelectionRange or value.range
end
local ret = util.locations_to_items({ result }, 0)
cb(ret)
end)
end
return M

View File

@ -0,0 +1,47 @@
local util = require("trouble.util")
local M = {}
local severities = { E = 1, W = 2, I = 3, H = 4 }
function M.get_list(winid)
local list = winid == nil and vim.fn.getqflist({ all = true }) or vim.fn.getloclist(winid, { all = true })
local ret = {}
for _, item in pairs(list.items) do
local row = (item.lnum == 0 and 1 or item.lnum) - 1
local col = (item.col == 0 and 1 or item.col) - 1
if item.valid == 1 then
ret[#ret + 1] = {
row = row,
col = col,
message = item.text,
severity = severities[item.type] or 0,
bufnr = item.bufnr,
range = {
start = { line = row, character = col },
["end"] = { line = row, character = -1 },
},
}
elseif #ret > 0 then
ret[#ret].message = ret[#ret].message .. "\n" .. item.text
end
end
for i, item in ipairs(ret) do
ret[i] = util.process_item(item)
end
return ret
end
function M.loclist(win, _buf, cb, _options)
return cb(M.get_list(win))
end
function M.qflist(_win, _buf, cb, _options)
return cb(M.get_list())
end
return M

View File

@ -0,0 +1,86 @@
local util = require("trouble.util")
local M = {}
M.results = {}
--- Turns a Telescope item into a Trouble item.
local function item_to_result(item)
local row = (item.lnum or 1) - 1
local col = (item.col or 1) - 1
if not item.bufnr then
local fname = vim.fn.fnamemodify(item.filename, ":p")
if vim.fn.filereadable(fname) == 0 and item.cwd then
fname = vim.fn.fnamemodify(item.cwd .. "/" .. item.filename, ":p")
end
item.bufnr = vim.fn.bufnr(fname, true)
end
local pitem = {
row = row,
col = col,
message = item.text,
severity = 0,
range = {
start = { line = row, character = col },
["end"] = { line = row, character = -1 },
},
}
return util.process_item(pitem, item.bufnr)
end
--- Shows all Telescope results in Trouble.
function M.open_with_trouble(prompt_bufnr, _mode)
local action_state = require("telescope.actions.state")
local actions = require("telescope.actions")
local picker = action_state.get_current_picker(prompt_bufnr)
local manager = picker.manager
M.results = {}
for item in manager:iter() do
table.insert(M.results, item_to_result(item))
end
actions.close(prompt_bufnr)
require("trouble").open("telescope")
end
--- Shows the selected Telescope results in Trouble.
function M.open_selected_with_trouble(prompt_bufnr, _mode)
local action_state = require("telescope.actions.state")
local actions = require("telescope.actions")
local picker = action_state.get_current_picker(prompt_bufnr)
M.results = {}
for _, item in ipairs(picker:get_multi_selection()) do
table.insert(M.results, item_to_result(item))
end
actions.close(prompt_bufnr)
require("trouble").open("telescope")
end
--- Shows the selected Telescope results in Trouble.
--- If no results are currently selected, shows all of them.
function M.smart_open_with_trouble(prompt_bufnr, _mode)
local action_state = require("telescope.actions.state")
local picker = action_state.get_current_picker(prompt_bufnr)
if #picker:get_multi_selection() > 0 then
M.open_selected_with_trouble(prompt_bufnr, _mode)
else
M.open_with_trouble(prompt_bufnr, _mode)
end
end
function M.telescope(_win, _buf, cb, _options)
if #M.results == 0 then
util.warn(
"No Telescope results found. Open Telescope and send results to Trouble first. Refer to the documentation for more info."
)
end
cb(M.results)
end
return M

View File

@ -0,0 +1,173 @@
local providers = require("trouble.providers")
local util = require("trouble.util")
local config = require("trouble.config")
local Text = require("trouble.text")
local folds = require("trouble.folds")
---@class Renderer
local renderer = {}
renderer.signs = {}
local function get_icon(file)
local ok, icons = pcall(require, "nvim-web-devicons")
if not ok then
util.warn(
"'nvim-web-devicons' is not installed. Install it, or set icons=false in your configuration to disable this message"
)
return ""
end
local fname = vim.fn.fnamemodify(file, ":t")
local ext = vim.fn.fnamemodify(file, ":e")
return icons.get_icon(fname, ext, { default = true })
end
local function update_signs()
renderer.signs = config.options.signs
if config.options.use_diagnostic_signs then
local lsp_signs = require("trouble.providers.diagnostic").get_signs()
renderer.signs = vim.tbl_deep_extend("force", {}, renderer.signs, lsp_signs)
end
end
---@param view TroubleView
function renderer.render(view, opts)
opts = opts or {}
local buf = vim.api.nvim_win_get_buf(view.parent)
providers.get(view.parent, buf, function(items, messages)
local auto_jump = vim.tbl_contains(config.options.auto_jump, opts.mode)
if opts.on_open and #items == 1 and auto_jump and not opts.auto then
view:close()
util.jump_to_item(opts.win, opts.precmd, items[1])
return
end
local grouped = providers.group(items)
local count = util.count(grouped)
-- check for auto close
if opts.auto and config.options.auto_close then
if count == 0 then
view:close()
return
end
end
-- Update lsp signs
update_signs()
local text = Text:new()
view.items = {}
if config.options.padding then
if messages ~= nil then
for _, msg in ipairs(messages) do
text:render(" " .. msg.text, msg.group, { append = " " })
end
end
text:nl()
end
-- render file groups
for _, group in ipairs(grouped) do
if opts.open_folds then
folds.open(group.filename)
end
if opts.close_folds then
folds.close(group.filename)
end
renderer.render_file(view, text, group.filename, group.items)
end
view:render(text)
if opts.focus then
view:focus()
end
end, config.options)
end
---@param view TroubleView
---@param text Text
---@param items Item[]
---@param filename string
function renderer.render_file(view, text, filename, items)
view.items[text.lineNr + 1] = { filename = filename, is_file = true }
if view.group == true then
local count = util.count(items)
text:render(" ")
if folds.is_folded(filename) then
text:render(config.options.fold_closed, "FoldIcon", " ")
else
text:render(config.options.fold_open, "FoldIcon", " ")
end
if config.options.icons then
local icon, icon_hl = get_icon(filename)
text:render(icon, icon_hl, { exact = true, append = " " })
end
text:render(vim.fn.fnamemodify(filename, ":p:."), "File", " ")
text:render(" " .. count .. " ", "Count")
text:nl()
end
if not folds.is_folded(filename) then
renderer.render_diagnostics(view, text, items)
end
end
---@param view TroubleView
---@param text Text
---@param items Item[]
function renderer.render_diagnostics(view, text, items)
for _, diag in ipairs(items) do
view.items[text.lineNr + 1] = diag
local sign = diag.sign or renderer.signs[string.lower(diag.type)]
if not sign then
sign = diag.type
end
local indent = " "
if config.options.indent_lines then
indent = ""
elseif config.options.group == false then
indent = " "
end
local sign_hl = diag.sign_hl or ("TroubleSign" .. diag.type)
text:render(indent, "Indent")
text:render(sign .. " ", sign_hl, { exact = true })
local lines = config.options.multiline and vim.split(diag.full_text, "\n") or { diag.text }
text:render(lines[1], "Text" .. diag.type, " ")
if diag.source then
text:render(diag.source, "Source")
end
if diag.code and diag.code ~= vim.NIL then
text:render(" (" .. diag.code .. ")", "Code")
end
text:render(" ")
text:render("[" .. diag.lnum .. ", " .. diag.col .. "]", "Location")
for l = 2, #lines do
local str = lines[l]
text:nl()
view.items[text.lineNr + 1] = diag
text:render(indent .. " ", "Indent")
text:render(str, "Text" .. diag.type, " ")
end
text:nl()
end
end
return renderer

View File

@ -0,0 +1,52 @@
---@class Text
---@field lines string[]
---@field hl Highlight[]
---@field lineNr number
---@field current string
local Text = {}
Text.__index = Text
function Text:new()
local this = { lines = {}, hl = {}, lineNr = 0, current = "" }
setmetatable(this, self)
return this
end
function Text:nl()
table.insert(self.lines, self.current)
self.current = ""
self.lineNr = self.lineNr + 1
end
function Text:render(str, group, opts)
str = str:gsub("[\n]", " ")
if type(opts) == "string" then
opts = { append = opts }
end
opts = opts or {}
if group then
if opts.exact ~= true then
group = "Trouble" .. group
end
local from = string.len(self.current)
---@class Highlight
local hl
hl = {
line = self.lineNr,
from = from,
to = from + string.len(str),
group = group,
}
table.insert(self.hl, hl)
end
self.current = self.current .. str
if opts.append then
self.current = self.current .. opts.append
end
if opts.nl then
self:nl()
end
end
return Text

View File

@ -0,0 +1,242 @@
local config = require("trouble.config")
local uv = vim.loop
local M = {}
function M.jump_to_item(win, precmd, item)
-- requiring here, as otherwise we run into a circular dependency
local View = require("trouble.view")
-- save position in jump list
vim.cmd("normal! m'")
View.switch_to(win)
if precmd then
vim.cmd(precmd)
end
if not vim.bo[item.bufnr].buflisted then
vim.bo[item.bufnr].buflisted = true
end
if not vim.api.nvim_buf_is_loaded(item.bufnr) then
vim.fn.bufload(item.bufnr)
end
vim.api.nvim_set_current_buf(item.bufnr)
vim.api.nvim_win_set_cursor(win or 0, { item.start.line + 1, item.start.character })
vim.api.nvim_exec_autocmds("User", { pattern = "TroubleJump", modeline = false })
end
function M.fix_mode(opts)
if opts.use_lsp_diagnostic_signs then
opts.use_diagnostic_signs = opts.use_lsp_diagnostic_signs
M.warn("The Trouble option use_lsp_diagnostic_signs has been renamed to use_diagnostic_signs")
end
local replace = {
lsp_workspace_diagnostics = "workspace_diagnostics",
lsp_document_diagnostics = "document_diagnostics",
workspace = "workspace_diagnostics",
document = "document_diagnostics",
}
for old, new in pairs(replace) do
if opts.mode == old then
opts.mode = new
M.warn("Using " .. old .. " for Trouble is deprecated. Please use " .. new .. " instead.")
end
end
end
---@return number
function M.count(tab)
local count = 0
for _ in pairs(tab) do
count = count + 1
end
return count
end
function M.warn(msg)
vim.notify(msg, vim.log.levels.WARN, { title = "Trouble" })
end
function M.error(msg)
vim.notify(msg, vim.log.levels.ERROR, { title = "Trouble" })
end
function M.debug(msg)
if config.options.debug then
vim.notify(msg, vim.log.levels.DEBUG, { title = "Trouble" })
end
end
function M.debounce(ms, fn)
local timer = vim.loop.new_timer()
return function(...)
local argv = { ... }
timer:start(ms, 0, function()
timer:stop()
vim.schedule_wrap(fn)(unpack(argv))
end)
end
end
function M.throttle(ms, fn)
local timer = vim.loop.new_timer()
local running = false
return function(...)
if not running then
local argv = { ... }
local argc = select("#", ...)
timer:start(ms, 0, function()
running = false
pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc))
end)
running = true
end
end
end
M.severity = {
[0] = "Other",
[1] = "Error",
[2] = "Warning",
[3] = "Information",
[4] = "Hint",
}
-- returns a hl or sign label for the givin severity and type
-- correctly handles new names introduced in vim.diagnostic
function M.get_severity_label(severity, type)
local label = severity
local prefix = "LspDiagnostics" .. (type or "Default")
if vim.diagnostic then
prefix = type and ("Diagnostic" .. type) or "Diagnostic"
label = ({
Warning = "Warn",
Information = "Info",
})[severity] or severity
end
return prefix .. label
end
-- based on the Telescope diagnostics code
-- see https://github.com/nvim-telescope/telescope.nvim/blob/0d6cd47990781ea760dd3db578015c140c7b9fa7/lua/telescope/utils.lua#L85
function M.process_item(item, bufnr)
bufnr = bufnr or item.bufnr
local filename = vim.api.nvim_buf_get_name(bufnr)
local range = item.range or item.targetSelectionRange
local start = {
line = range and vim.tbl_get(range, "start", "line") or item.lnum,
character = range and vim.tbl_get(range, "start", "character") or item.col,
}
local finish = {
line = range and vim.tbl_get(range, "end", "line") or item.end_lnum,
character = range and vim.tbl_get(range, "end", "character") or item.end_col,
}
if start.character == nil or start.line == nil then
M.error("Found an item for Trouble without start range " .. vim.inspect(start))
end
if finish.character == nil or finish.line == nil then
M.error("Found an item for Trouble without finish range " .. vim.inspect(finish))
end
local row = start.line ---@type number
local col = start.character ---@type number
if not item.message and filename then
-- check if the filename is a uri
if string.match(filename, "^%w+://") ~= nil then
if not vim.api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
local lines = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)
item.message = lines[1] or ""
else
local fd = assert(uv.fs_open(filename, "r", 438))
local stat = assert(uv.fs_fstat(fd))
local data = assert(uv.fs_read(fd, stat.size, 0))
assert(uv.fs_close(fd))
item.message = vim.split(data, "\n", { plain = true })[row + 1] or ""
end
end
---@class Item
---@field is_file boolean
---@field fixed boolean
local ret = {
bufnr = bufnr,
filename = filename,
lnum = row + 1,
col = col + 1,
start = start,
finish = finish,
sign = item.sign, ---@type string?
sign_hl = item.sign_hl, ---@type string?
-- remove line break to avoid display issues
text = vim.trim(item.message:gsub("[\n]+", "")):sub(0, vim.o.columns),
full_text = vim.trim(item.message),
type = M.severity[item.severity] or M.severity[0],
code = item.code or (item.user_data and item.user_data.lsp and item.user_data.lsp.code), ---@type string?
code_href = (item.codeDescription and item.codeDescription.href)
or (
item.user_data
and item.user_data.lsp
and item.user_data.lsp.codeDescription
and item.user_data.lsp.codeDescription.href
), ---@type string?
source = item.source, ---@type string?
severity = item.severity or 0,
}
return ret
end
-- takes either a table indexed by bufnr, or an lsp result with uri
---@return Item[]
function M.locations_to_items(results, default_severity)
default_severity = default_severity or 0
local ret = {}
for bufnr, locs in pairs(results or {}) do
for _, loc in pairs(locs.result or locs) do
if not vim.tbl_isempty(loc) then
local uri = loc.uri or loc.targetUri
local buf = uri and vim.uri_to_bufnr(uri) or bufnr
loc.severity = loc.severity or default_severity
table.insert(ret, M.process_item(loc, buf))
end
end
end
return ret
end
-- @private
local function make_position_param(win, buf)
local row, col = unpack(vim.api.nvim_win_get_cursor(win))
row = row - 1
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
return { line = 0, character = 0 }
end
col = vim.str_utfindex(line, col)
return { line = row, character = col }
end
function M.make_text_document_params(buf)
return { uri = vim.uri_from_bufnr(buf) }
end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
-- @returns `TextDocumentPositionParams` object
-- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
function M.make_position_params(win, buf)
return {
textDocument = M.make_text_document_params(buf),
position = make_position_param(win, buf),
}
end
return M

View File

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