aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua')
-rw-r--r--runtime/lua/vim/F.lua7
-rw-r--r--runtime/lua/vim/_meta.lua631
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua1
-rw-r--r--runtime/lua/vim/lsp/handlers.lua51
-rw-r--r--runtime/lua/vim/lsp/util.lua152
5 files changed, 772 insertions, 70 deletions
diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua
index 5887e978b9..7925ff6e44 100644
--- a/runtime/lua/vim/F.lua
+++ b/runtime/lua/vim/F.lua
@@ -20,5 +20,12 @@ function F.npcall(fn, ...)
return F.ok_or_nil(pcall(fn, ...))
end
+--- Wrap a function to return nil if it fails, otherwise the value
+function F.nil_wrap(fn)
+ return function(...)
+ return F.npcall(fn, ...)
+ end
+end
+
return F
diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua
new file mode 100644
index 0000000000..02d1154df4
--- /dev/null
+++ b/runtime/lua/vim/_meta.lua
@@ -0,0 +1,631 @@
+-- prevents luacheck from making lints for setting things on vim
+local vim = assert(vim)
+
+local a = vim.api
+local validate = vim.validate
+
+local SET_TYPES = setmetatable({
+ SET = 0,
+ LOCAL = 1,
+ GLOBAL = 2,
+}, { __index = error })
+
+local options_info = {}
+for _, v in pairs(a.nvim_get_all_options_info()) do
+ options_info[v.name] = v
+ if v.shortname ~= "" then options_info[v.shortname] = v end
+end
+
+local is_global_option = function(info) return info.scope == "global" end
+local is_buffer_option = function(info) return info.scope == "buf" end
+local is_window_option = function(info) return info.scope == "win" end
+
+local get_scoped_options = function(scope)
+ local result = {}
+ for name, option_info in pairs(options_info) do
+ if option_info.scope == scope then
+ result[name] = true
+ end
+ end
+
+ return result
+end
+
+local buf_options = get_scoped_options("buf")
+local glb_options = get_scoped_options("global")
+local win_options = get_scoped_options("win")
+
+local function make_meta_accessor(get, set, del, validator)
+ validator = validator or function() return true end
+
+ validate {
+ get = {get, 'f'};
+ set = {set, 'f'};
+ del = {del, 'f', true};
+ validator = {validator, 'f'};
+ }
+
+ local mt = {}
+ function mt:__newindex(k, v)
+ if not validator(k) then
+ return
+ end
+
+ if del and v == nil then
+ return del(k)
+ end
+ return set(k, v)
+ end
+ function mt:__index(k)
+ if not validator(k) then
+ return
+ end
+
+ return get(k)
+ end
+ return setmetatable({}, mt)
+end
+
+vim.env = make_meta_accessor(function(k)
+ local v = vim.fn.getenv(k)
+ if v == vim.NIL then
+ return nil
+ end
+ return v
+end, vim.fn.setenv)
+
+do -- buffer option accessor
+ local function new_buf_opt_accessor(bufnr)
+ local function get(k)
+ if bufnr == nil and type(k) == "number" then
+ return new_buf_opt_accessor(k)
+ end
+
+ return a.nvim_buf_get_option(bufnr or 0, k)
+ end
+
+ local function set(k, v)
+ return a.nvim_buf_set_option(bufnr or 0, k, v)
+ end
+
+ return make_meta_accessor(get, set, nil, function(k)
+ if type(k) == 'string' then
+ if win_options[k] then
+ error(string.format([['%s' is a window option, not a buffer option. See ":help %s"]], k, k))
+ elseif glb_options[k] then
+ error(string.format([['%s' is a global option, not a buffer option. See ":help %s"]], k, k))
+ end
+ end
+
+ return true
+ end)
+ end
+
+ vim.bo = new_buf_opt_accessor(nil)
+end
+
+do -- window option accessor
+ local function new_win_opt_accessor(winnr)
+ local function get(k)
+ if winnr == nil and type(k) == "number" then
+ return new_win_opt_accessor(k)
+ end
+ return a.nvim_win_get_option(winnr or 0, k)
+ end
+
+ local function set(k, v)
+ return a.nvim_win_set_option(winnr or 0, k, v)
+ end
+
+ return make_meta_accessor(get, set, nil, function(k)
+ if type(k) == 'string' then
+ if buf_options[k] then
+ error(string.format([['%s' is a buffer option, not a window option. See ":help %s"]], k, k))
+ elseif glb_options[k] then
+ error(string.format([['%s' is a global option, not a window option. See ":help %s"]], k, k))
+ end
+ end
+
+ return true
+ end)
+ end
+
+ vim.wo = new_win_opt_accessor(nil)
+end
+
+--[[
+Local window setter
+
+buffer options: does not get copied when split
+ nvim_set_option(buf_opt, value) -> sets the default for NEW buffers
+ this sets the hidden global default for buffer options
+
+ nvim_buf_set_option(...) -> sets the local value for the buffer
+
+ set opt=value, does BOTH global default AND buffer local value
+ setlocal opt=value, does ONLY buffer local value
+
+window options: gets copied
+ does not need to call nvim_set_option because nobody knows what the heck this does⸮
+ We call it anyway for more readable code.
+
+
+ Command global value local value
+ :set option=value set set
+ :setlocal option=value - set
+:setglobal option=value set -
+--]]
+local function set_scoped_option(k, v, set_type)
+ local info = options_info[k]
+
+ -- Don't let people do setlocal with global options.
+ -- That is a feature that doesn't make sense.
+ if set_type == SET_TYPES.LOCAL and is_global_option(info) then
+ error(string.format("Unable to setlocal option: '%s', which is a global option.", k))
+ end
+
+ -- Only `setlocal` skips setting the default/global value
+ -- This will more-or-less noop for window options, but that's OK
+ if set_type ~= SET_TYPES.LOCAL then
+ a.nvim_set_option(k, v)
+ end
+
+ if is_window_option(info) then
+ if set_type ~= SET_TYPES.GLOBAL then
+ a.nvim_win_set_option(0, k, v)
+ end
+ elseif is_buffer_option(info) then
+ if set_type == SET_TYPES.LOCAL
+ or (set_type == SET_TYPES.SET and not info.global_local) then
+ a.nvim_buf_set_option(0, k, v)
+ end
+ end
+end
+
+--[[
+Local window getter
+
+ Command global value local value
+ :set option? - display
+ :setlocal option? - display
+:setglobal option? display -
+--]]
+local function get_scoped_option(k, set_type)
+ local info = assert(options_info[k], "Must be a valid option: " .. tostring(k))
+
+ if set_type == SET_TYPES.GLOBAL or is_global_option(info) then
+ return a.nvim_get_option(k)
+ end
+
+ if is_buffer_option(info) then
+ local was_set, value = pcall(a.nvim_buf_get_option, 0, k)
+ if was_set then return value end
+
+ if info.global_local then
+ return a.nvim_get_option(k)
+ end
+
+ error("buf_get: This should not be able to happen, given my understanding of options // " .. k)
+ end
+
+ if is_window_option(info) then
+ if vim.api.nvim_get_option_info(k).was_set then
+ local was_set, value = pcall(a.nvim_win_get_option, 0, k)
+ if was_set then return value end
+ end
+
+ return a.nvim_get_option(k)
+ end
+
+ error("This fallback case should not be possible. " .. k)
+end
+
+-- vim global option
+-- this ONLY sets the global option. like `setglobal`
+vim.go = make_meta_accessor(a.nvim_get_option, a.nvim_set_option)
+
+-- vim `set` style options.
+-- it has no additional metamethod magic.
+vim.o = make_meta_accessor(
+ function(k) return get_scoped_option(k, SET_TYPES.SET) end,
+ function(k, v) return set_scoped_option(k, v, SET_TYPES.SET) end
+)
+
+---@brief [[
+--- vim.opt, vim.opt_local and vim.opt_global implementation
+---
+--- To be used as helpers for working with options within neovim.
+--- For information on how to use, see :help vim.opt
+---
+---@brief ]]
+
+--- Preserves the order and does not mutate the original list
+local remove_duplicate_values = function(t)
+ local result, seen = {}, {}
+ if type(t) == "function" then error(debug.traceback("asdf")) end
+ for _, v in ipairs(t) do
+ if not seen[v] then
+ table.insert(result, v)
+ end
+
+ seen[v] = true
+ end
+
+ return result
+end
+
+-- TODO(tjdevries): Improve option metadata so that this doesn't have to be hardcoded.
+-- Can be done in a separate PR.
+local key_value_options = {
+ fillchars = true,
+ listchars = true,
+ winhl = true,
+}
+
+---@class OptionType
+--- Option Type Enum
+local OptionTypes = setmetatable({
+ BOOLEAN = 0,
+ NUMBER = 1,
+ STRING = 2,
+ ARRAY = 3,
+ MAP = 4,
+ SET = 5,
+}, {
+ __index = function(_, k) error("Not a valid OptionType: " .. k) end,
+ __newindex = function(_, k) error("Cannot set a new OptionType: " .. k) end,
+})
+
+--- Convert a vimoption_T style dictionary to the correct OptionType associated with it.
+---@return OptionType
+local get_option_type = function(name, info)
+ if info.type == "boolean" then
+ return OptionTypes.BOOLEAN
+ elseif info.type == "number" then
+ return OptionTypes.NUMBER
+ elseif info.type == "string" then
+ if not info.commalist and not info.flaglist then
+ return OptionTypes.STRING
+ end
+
+ if key_value_options[name] then
+ assert(info.commalist, "Must be a comma list to use key:value style")
+ return OptionTypes.MAP
+ end
+
+ if info.flaglist then
+ return OptionTypes.SET
+ elseif info.commalist then
+ return OptionTypes.ARRAY
+ end
+
+ error("Fallthrough in OptionTypes")
+ else
+ error("Not a known info.type:" .. info.type)
+ end
+end
+
+
+--- Convert a lua value to a vimoption_T value
+local convert_value_to_vim = (function()
+ -- Map of functions to take a Lua style value and convert to vimoption_T style value.
+ -- Each function takes (info, lua_value) -> vim_value
+ local to_vim_value = {
+ [OptionTypes.BOOLEAN] = function(_, value) return value end,
+ [OptionTypes.NUMBER] = function(_, value) return value end,
+ [OptionTypes.STRING] = function(_, value) return value end,
+
+ [OptionTypes.SET] = function(_, value)
+ if type(value) == "string" then return value end
+ local result = ''
+ for k in pairs(value) do
+ result = result .. k
+ end
+
+ return result
+ end,
+
+ [OptionTypes.ARRAY] = function(_, value)
+ if type(value) == "string" then return value end
+ return table.concat(remove_duplicate_values(value), ",")
+ end,
+
+ [OptionTypes.MAP] = function(_, value)
+ if type(value) == "string" then return value end
+ if type(value) == "function" then error(debug.traceback("asdf")) end
+
+ local result = {}
+ for opt_key, opt_value in pairs(value) do
+ table.insert(result, string.format("%s:%s", opt_key, opt_value))
+ end
+
+ table.sort(result)
+ return table.concat(result, ",")
+ end,
+ }
+
+ return function(name, info, value)
+ return to_vim_value[get_option_type(name, info)](info, value)
+ end
+end)()
+
+--- Converts a vimoption_T style value to a Lua value
+local convert_value_to_lua = (function()
+ -- Map of OptionType to functions that take vimoption_T values and conver to lua values.
+ -- Each function takes (info, vim_value) -> lua_value
+ local to_lua_value = {
+ [OptionTypes.BOOLEAN] = function(_, value) return value end,
+ [OptionTypes.NUMBER] = function(_, value) return value end,
+ [OptionTypes.STRING] = function(_, value) return value end,
+
+ [OptionTypes.ARRAY] = function(_, value)
+ if type(value) == "table" then
+ value = remove_duplicate_values(value)
+ return value
+ end
+
+ return vim.split(value, ",")
+ end,
+
+ [OptionTypes.SET] = function(info, value)
+ if type(value) == "table" then return value end
+
+ assert(info.flaglist, "That is the only one I know how to handle")
+
+ local result = {}
+ for i = 1, #value do
+ result[value:sub(i, i)] = true
+ end
+
+ return result
+ end,
+
+ [OptionTypes.MAP] = function(info, raw_value)
+ if type(raw_value) == "table" then return raw_value end
+
+ assert(info.commalist, "Only commas are supported currently")
+
+ local result = {}
+
+ local comma_split = vim.split(raw_value, ",")
+ for _, key_value_str in ipairs(comma_split) do
+ local key, value = unpack(vim.split(key_value_str, ":"))
+ key = vim.trim(key)
+ value = vim.trim(value)
+
+ result[key] = value
+ end
+
+ return result
+ end,
+ }
+
+ return function(name, info, option_value)
+ return to_lua_value[get_option_type(name, info)](info, option_value)
+ end
+end)()
+
+--- Handles the mutation of various different values.
+local value_mutator = function(name, info, current, new, mutator)
+ return mutator[get_option_type(name, info)](current, new)
+end
+
+--- Handles the '^' operator
+local prepend_value = (function()
+ local methods = {
+ [OptionTypes.NUMBER] = function()
+ error("The '^' operator is not currently supported for")
+ end,
+
+ [OptionTypes.STRING] = function(left, right)
+ return right .. left
+ end,
+
+ [OptionTypes.ARRAY] = function(left, right)
+ for i = #right, 1, -1 do
+ table.insert(left, 1, right[i])
+ end
+
+ return left
+ end,
+
+ [OptionTypes.MAP] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+
+ [OptionTypes.SET] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+ }
+
+ return function(name, info, current, new)
+ return value_mutator(
+ name, info, convert_value_to_lua(name, info, current), convert_value_to_lua(name, info, new), methods
+ )
+ end
+end)()
+
+--- Handles the '+' operator
+local add_value = (function()
+ local methods = {
+ [OptionTypes.NUMBER] = function(left, right)
+ return left + right
+ end,
+
+ [OptionTypes.STRING] = function(left, right)
+ return left .. right
+ end,
+
+ [OptionTypes.ARRAY] = function(left, right)
+ for _, v in ipairs(right) do
+ table.insert(left, v)
+ end
+
+ return left
+ end,
+
+ [OptionTypes.MAP] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+
+ [OptionTypes.SET] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+ }
+
+ return function(name, info, current, new)
+ return value_mutator(
+ name, info, convert_value_to_lua(name, info, current), convert_value_to_lua(name, info, new), methods
+ )
+ end
+end)()
+
+--- Handles the '-' operator
+local remove_value = (function()
+ local remove_one_item = function(t, val)
+ if vim.tbl_islist(t) then
+ local remove_index = nil
+ for i, v in ipairs(t) do
+ if v == val then
+ remove_index = i
+ end
+ end
+
+ if remove_index then
+ table.remove(t, remove_index)
+ end
+ else
+ t[val] = nil
+ end
+ end
+
+ local methods = {
+ [OptionTypes.NUMBER] = function(left, right)
+ return left - right
+ end,
+
+ [OptionTypes.STRING] = function()
+ error("Subtraction not supported for strings.")
+ end,
+
+ [OptionTypes.ARRAY] = function(left, right)
+ if type(right) == "string" then
+ remove_one_item(left, right)
+ else
+ for _, v in ipairs(right) do
+ remove_one_item(left, v)
+ end
+ end
+
+ return left
+ end,
+
+ [OptionTypes.MAP] = function(left, right)
+ if type(right) == "string" then
+ left[right] = nil
+ else
+ for _, v in ipairs(right) do
+ left[v] = nil
+ end
+ end
+
+ return left
+ end,
+
+ [OptionTypes.SET] = function(left, right)
+ if type(right) == "string" then
+ left[right] = nil
+ else
+ for _, v in ipairs(right) do
+ left[v] = nil
+ end
+ end
+
+ return left
+ end,
+ }
+
+ return function(name, info, current, new)
+ return value_mutator(name, info, convert_value_to_lua(name, info, current), new, methods)
+ end
+end)()
+
+local create_option_metatable = function(set_type)
+ local set_mt, option_mt
+
+ local make_option = function(name, value)
+ local info = assert(options_info[name], "Not a valid option name: " .. name)
+
+ if type(value) == "table" and getmetatable(value) == option_mt then
+ assert(name == value._name, "must be the same value, otherwise that's weird.")
+
+ value = value._value
+ end
+
+ return setmetatable({
+ _name = name,
+ _value = value,
+ _info = info,
+ }, option_mt)
+ end
+
+ -- TODO(tjdevries): consider supporting `nil` for set to remove the local option.
+ -- vim.cmd [[set option<]]
+
+ option_mt = {
+ -- To set a value, instead use:
+ -- opt[my_option] = value
+ _set = function(self)
+ local value = convert_value_to_vim(self._name, self._info, self._value)
+ set_scoped_option(self._name, value, set_type)
+
+ return self
+ end,
+
+ get = function(self)
+ return convert_value_to_lua(self._name, self._info, self._value)
+ end,
+
+ append = function(self, right)
+ return self:__add(right):_set()
+ end,
+
+ __add = function(self, right)
+ return make_option(self._name, add_value(self._name, self._info, self._value, right))
+ end,
+
+ prepend = function(self, right)
+ return self:__pow(right):_set()
+ end,
+
+ __pow = function(self, right)
+ return make_option(self._name, prepend_value(self._name, self._info, self._value, right))
+ end,
+
+ remove = function(self, right)
+ return self:__sub(right):_set()
+ end,
+
+ __sub = function(self, right)
+ return make_option(self._name, remove_value(self._name, self._info, self._value, right))
+ end
+ }
+ option_mt.__index = option_mt
+
+ set_mt = {
+ __index = function(_, k)
+ return make_option(k, get_scoped_option(k, set_type))
+ end,
+
+ __newindex = function(_, k, v)
+ local opt = make_option(k, v)
+ opt:_set()
+ end,
+ }
+
+ return set_mt
+end
+
+vim.opt = setmetatable({}, create_option_metatable(SET_TYPES.SET))
+vim.opt_local = setmetatable({}, create_option_metatable(SET_TYPES.LOCAL))
+vim.opt_global = setmetatable({}, create_option_metatable(SET_TYPES.GLOBAL))
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 6f2f846a3b..6a7dc1bbb0 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -1151,6 +1151,7 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
end
end
+ opts.focus_id = "line_diagnostics"
local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi)
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 18155ceb7e..6ae54ea253 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -260,22 +260,18 @@ end
--- - See |vim.api.nvim_open_win()|
function M.hover(_, method, result, _, _, config)
config = config or {}
- local bufnr, winnr = util.focusable_float(method, function()
- if not (result and result.contents) then
- -- return { 'No information available' }
- return
- end
- local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
- markdown_lines = util.trim_empty_lines(markdown_lines)
- if vim.tbl_isempty(markdown_lines) then
- -- return { 'No information available' }
- return
- end
- local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, config)
- util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
- return bufnr, winnr
- end)
- return bufnr, winnr
+ config.focus_id = method
+ if not (result and result.contents) then
+ -- return { 'No information available' }
+ return
+ end
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ markdown_lines = util.trim_empty_lines(markdown_lines)
+ if vim.tbl_isempty(markdown_lines) then
+ -- return { 'No information available' }
+ return
+ end
+ return util.open_floating_preview(markdown_lines, "markdown", config)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
@@ -333,26 +329,21 @@ M['textDocument/implementation'] = location_handler
--- - See |vim.api.nvim_open_win()|
function M.signature_help(_, method, result, _, bufnr, config)
config = config or {}
+ config.focus_id = method
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
-- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
if not (result and result.signatures and result.signatures[1]) then
print('No signature help available')
return
end
- local p_bufnr, winnr = util.focusable_float(method, function()
- local ft = api.nvim_buf_get_option(bufnr, 'filetype')
- local lines = util.convert_signature_help_to_markdown_lines(result, ft)
- lines = util.trim_empty_lines(lines)
- if vim.tbl_isempty(lines) then
- print('No signature help available')
- return
- end
- local p_bufnr, p_winnr = util.fancy_floating_markdown(lines, config)
- util.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}, p_winnr)
-
- return p_bufnr, p_winnr
- end)
- return p_bufnr, winnr
+ local ft = api.nvim_buf_get_option(bufnr, 'filetype')
+ local lines = util.convert_signature_help_to_markdown_lines(result, ft)
+ lines = util.trim_empty_lines(lines)
+ if vim.tbl_isempty(lines) then
+ print('No signature help available')
+ return
+ end
+ return util.open_floating_preview(lines, "markdown", config)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 4df744a357..e16b02fa6c 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -975,6 +975,8 @@ function M.preview_location(location, opts)
-- An empty syntax is more common now with TreeSitter, since TS disables syntax.
syntax = api.nvim_buf_get_option(bufnr, 'filetype')
end
+ opts = opts or {}
+ opts.focus_id = "location"
return M.open_floating_preview(contents, syntax, opts)
end
@@ -997,7 +999,9 @@ end
---buffer id, the newly created window will be the new focus associated with
---the current buffer via the tag `unique_name`.
--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise
+---@deprecated please use open_floating_preview directly
function M.focusable_float(unique_name, fn)
+ vim.notify("focusable_float is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
-- Go back to previous window if we are in a focusable one
if npcall(api.nvim_win_get_var, 0, unique_name) then
return api.nvim_command("wincmd p")
@@ -1026,10 +1030,10 @@ end
--@param fn (function) The return values of this function will be passed
---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new
---floating window should be created
+---@deprecated please use open_floating_preview directly
function M.focusable_preview(unique_name, fn)
- return M.focusable_float(unique_name, function()
- return M.open_floating_preview(fn())
- end)
+ vim.notify("focusable_preview is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
+ return M.open_floating_preview(fn(), {focus_id = unique_name})
end
--- Trims empty lines from input and pad top and bottom with empty lines
@@ -1061,13 +1065,20 @@ end
--- TODO: refactor to separate stripping/converting and make use of open_floating_preview
---
+--- @deprecated please use open_floating_preview directly
+function M.fancy_floating_markdown(contents, opts)
+ vim.notify("fancy_floating_markdown is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
+ return M.open_floating_preview(contents, "markdown", opts)
+end
+
--- Converts markdown into syntax highlighted regions by stripping the code
--- blocks and converting them into highlighted code.
--- This will by default insert a blank line separator after those code block
--- regions to improve readability.
---- The result is shown in a floating preview.
+---
+--- This method configures the given buffer and returns the lines to set.
+---
+--- If you want to open a popup with fancy markdown, use `open_floating_preview` instead
---
---@param contents table of lines to show in window
---@param opts dictionary with optional fields
@@ -1082,7 +1093,7 @@ end
--- - pad_bottom number of lines to pad contents at bottom
--- - separator insert separator after code block
---@returns width,height size of float
-function M.fancy_floating_markdown(contents, opts)
+function M.stylize_markdown(bufnr, contents, opts)
validate {
contents = { contents, 't' };
opts = { opts, 't', true };
@@ -1154,23 +1165,13 @@ function M.fancy_floating_markdown(contents, opts)
end
end
- -- Make the floating window.
- local bufnr = api.nvim_create_buf(false, true)
- local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
- api.nvim_buf_set_option(bufnr, 'modifiable', false)
-
- -- Switch to the floating window to apply the syntax highlighting.
- -- This is because the syntax command doesn't accept a target.
- local cwin = vim.api.nvim_get_current_win()
- vim.api.nvim_set_current_win(winnr)
- api.nvim_win_set_option(winnr, 'conceallevel', 2)
- api.nvim_win_set_option(winnr, 'concealcursor', 'n')
-
- vim.cmd("ownsyntax lsp_markdown")
local idx = 1
--@private
+ -- keep track of syntaxes we already inlcuded.
+ -- no need to include the same syntax more than once
+ local langs = {}
local function apply_syntax_to_region(ft, start, finish)
if ft == "" then
vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1))
@@ -1179,21 +1180,37 @@ function M.fancy_floating_markdown(contents, opts)
local name = ft..idx
idx = idx + 1
local lang = "@"..ft:upper()
- -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
- pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax")
- -- TODO(ashkan): better validation before this.
- if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
- return
+ if not langs[lang] then
+ -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
+ pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax")
+ -- TODO(ashkan): better validation before this.
+ if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
+ return
+ end
+ langs[lang] = true
end
vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", name, start, finish + 1, lang))
end
- for _, h in ipairs(highlights) do
- apply_syntax_to_region(h.ft, h.start, h.finish)
- end
+ -- needs to run in the buffer for the regions to work
+ api.nvim_buf_call(bufnr, function()
+ -- we need to apply lsp_markdown regions speperately, since otherwise
+ -- markdown regions can "bleed" through the other syntax regions
+ -- and mess up the formatting
+ local last = 1
+ for _, h in ipairs(highlights) do
+ if last < h.start then
+ apply_syntax_to_region("lsp_markdown", last, h.start - 1)
+ end
+ apply_syntax_to_region(h.ft, h.start, h.finish)
+ last = h.finish + 1
+ end
+ if last < #stripped then
+ apply_syntax_to_region("lsp_markdown", last, #stripped)
+ end
+ end)
- vim.api.nvim_set_current_win(cwin)
- return bufnr, winnr
+ return stripped
end
--- Creates autocommands to close a preview window when events happen.
@@ -1202,7 +1219,9 @@ end
--@param winnr (number) window id of preview window
--@see |autocmd-events|
function M.close_preview_autocmd(events, winnr)
- api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ if #events > 0 then
+ api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ end
end
--@internal
@@ -1287,13 +1306,16 @@ end
--@param opts dictionary with optional fields
-- - height of floating window
-- - width of floating window
--- - wrap_at character to wrap at for computing height
+-- - wrap boolean enable wrapping of long lines (defaults to true)
+-- - wrap_at character to wrap at for computing height when wrap is enabled
-- - max_width maximal width of floating window
-- - max_height maximal height of floating window
-- - pad_left number of columns to pad contents at left
-- - pad_right number of columns to pad contents at right
-- - pad_top number of lines to pad contents at top
-- - pad_bottom number of lines to pad contents at bottom
+-- - focus_id if a popup with this id is opened, then focus it
+-- - close_events list of events that closes the floating window
--@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
function M.open_floating_preview(contents, syntax, opts)
@@ -1303,27 +1325,77 @@ function M.open_floating_preview(contents, syntax, opts)
opts = { opts, 't', true };
}
opts = opts or {}
+ opts.wrap = opts.wrap ~= false -- wrapping by default
+ opts.stylize_markdown = opts.stylize_markdown ~= false
+ opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}
+
+ local bufnr = api.nvim_get_current_buf()
+
+ -- check if this popup is focusable and we need to focus
+ if opts.focus_id then
+ -- Go back to previous window if we are in a focusable one
+ local current_winnr = api.nvim_get_current_win()
+ if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
+ api.nvim_command("wincmd p")
+ return bufnr, current_winnr
+ end
+ do
+ local win = find_window_by_var(opts.focus_id, bufnr)
+ if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
+ -- focus and return the existing buf, win
+ api.nvim_set_current_win(win)
+ api.nvim_command("stopinsert")
+ return api.nvim_win_get_buf(win), win
+ end
+ end
+ end
+
+ local floating_bufnr = api.nvim_create_buf(false, true)
+ local do_stylize = syntax == "markdown" and opts.stylize_markdown
+
-- Clean up input: trim empty lines from the end, pad
contents = M._trim(contents, opts)
+ if do_stylize then
+ -- applies the syntax and sets the lines to the buffer
+ contents = M.stylize_markdown(floating_bufnr, contents, opts)
+ else
+ if syntax then
+ api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
+ end
+ api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ end
+
-- Compute size of float needed to show (wrapped) lines
- opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
+ if opts.wrap then
+ opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
+ else
+ opts.wrap_at = nil
+ end
local width, height = M._make_floating_popup_size(contents, opts)
- local floating_bufnr = api.nvim_create_buf(false, true)
- if syntax then
- api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
- end
local float_option = M.make_floating_popup_options(width, height, opts)
local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
- if syntax == 'markdown' then
+ if do_stylize then
api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
+ api.nvim_win_set_option(floating_winnr, 'concealcursor', 'n')
end
- api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ -- disable folding
+ api.nvim_win_set_option(floating_winnr, 'foldenable', false)
+ -- soft wrapping
+ api.nvim_win_set_option(floating_winnr, 'wrap', opts.wrap)
+
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
- M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr)
+ api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true})
+ M.close_preview_autocmd(opts.close_events, floating_winnr)
+
+ -- save focus_id
+ if opts.focus_id then
+ api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
+ end
+
return floating_bufnr, floating_winnr
end