511 lines
16 KiB
Lua
511 lines
16 KiB
Lua
local api = vim.api
|
|
local fn = vim.fn
|
|
|
|
local M = {}
|
|
|
|
------------------------
|
|
------- Requests -------
|
|
------------------------
|
|
|
|
local REQUEST_STATE = {
|
|
id = 0,
|
|
callbacks = {},
|
|
}
|
|
|
|
local function add_callback(callback)
|
|
REQUEST_STATE.id = REQUEST_STATE.id + 1
|
|
REQUEST_STATE.callbacks[REQUEST_STATE.id] = callback
|
|
return REQUEST_STATE.id
|
|
end
|
|
|
|
---Invoke the callback, called by vscode
|
|
---@param id number callback id
|
|
---@param result any result
|
|
---@param is_error boolean is error
|
|
function M.invoke_callback(id, result, is_error)
|
|
vim.schedule(function()
|
|
local callback = REQUEST_STATE.callbacks[id]
|
|
REQUEST_STATE.callbacks[id] = nil
|
|
if callback then
|
|
if is_error then
|
|
callback(result, nil)
|
|
else
|
|
callback(nil, result)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
---- Run an action asynchronously
|
|
---@param name string The action name, generally a vscode command
|
|
---@param opts? table Optional options table, all fields are optional
|
|
--- - args: (table) Optional arguments for the action
|
|
--- - range: (table) Specific range for the action. In visual mode, this parameter is generally not needed.
|
|
--- Three formats supported (All values are 0-indexed):
|
|
--- - [start_line, end_line]
|
|
--- - [start_line, start_character, end_line, end_character]
|
|
--- - {start = { line = start_line , character = start_character}, end = { line = end_line , character = end_character}}
|
|
--- - restore_selection: (boolean) Whether to preserve the current selection, only valid when `range` is specified. Defaults to `true`
|
|
--- - callback: (function(err: string|nil, ret: any))
|
|
--- Optional callback function to handle the action result.
|
|
--- The first argument is the error message, and the second is the result.
|
|
--- If no callback is provided, any error message will be shown as a notification in VSCode.
|
|
function M.action(name, opts)
|
|
opts = opts or {}
|
|
opts.restore_selection = opts.restore_selection ~= false
|
|
vim.validate({
|
|
name = { name, "string" },
|
|
opts = { opts, "table", true },
|
|
})
|
|
vim.validate({
|
|
["opts.callback"] = { opts.callback, "f", true },
|
|
["opts.args"] = { opts.args, "t", true },
|
|
["opts.range"] = {
|
|
opts.range,
|
|
function(range)
|
|
if range == nil then
|
|
return true
|
|
end
|
|
if type(range) ~= "table" then
|
|
return false
|
|
end
|
|
if vim.islist(opts.range) then
|
|
return #opts.range == 2 or #opts.range == 4
|
|
end
|
|
return range.start
|
|
and range.start.line
|
|
and range.start.character
|
|
and range["end"]
|
|
and range["end"].line
|
|
and range["end"].character
|
|
end,
|
|
},
|
|
["opts.restore_selection"] = { opts.restore_selection, "b", true },
|
|
})
|
|
if opts.args and not vim.islist(opts.args) then
|
|
opts.args = { opts.args }
|
|
end
|
|
if opts.callback then
|
|
opts.callback = add_callback(opts.callback)
|
|
end
|
|
vim.schedule(function()
|
|
vim.rpcnotify(vim.g.vscode_channel, "vscode-action", name, opts)
|
|
end)
|
|
end
|
|
|
|
--- Run an action synchronously
|
|
---@param name string The action name, generally a vscode command
|
|
---@param opts? table Optional options table, all fields are optional
|
|
--- - args: (table) Optional arguments for the action
|
|
--- - range: (table) Specific range for the action. In visual mode, this parameter is generally not needed.
|
|
--- Three formats supported (All values are 0-indexed):
|
|
--- - [start_line, end_line]
|
|
--- - [start_line, start_character, end_line, end_character]
|
|
--- - {start = { line = start_line , character = start_character}, end = { line = end_line , character = end_character}}
|
|
--- - restore_selection: (boolean) Whether to preserve the current selection, only valid when `range` is specified. Defaults to `true`
|
|
---@param timeout? number Timeout in milliseconds. The default value is -1, which means no timeout.
|
|
---
|
|
---@return any: result
|
|
function M.call(name, opts, timeout)
|
|
opts = opts or {}
|
|
opts.restore_selection = opts.restore_selection ~= false
|
|
timeout = timeout or -1
|
|
vim.validate({
|
|
name = { name, "string" },
|
|
opts = { opts, "table", true },
|
|
timeout = { timeout, "number", true },
|
|
})
|
|
vim.validate({
|
|
["opts.callback"] = { opts.callback, "nil" },
|
|
["opts.args"] = { opts.args, "t", true },
|
|
["opts.range"] = {
|
|
opts.range,
|
|
function(range)
|
|
if range == nil then
|
|
return true
|
|
end
|
|
if type(range) ~= "table" then
|
|
return false
|
|
end
|
|
if vim.islist(opts.range) then
|
|
return #opts.range == 2 or #opts.range == 4
|
|
end
|
|
return range.start
|
|
and range.start.line
|
|
and range.start.character
|
|
and range["end"]
|
|
and range["end"].line
|
|
and range["end"].character
|
|
end,
|
|
},
|
|
["opts.restore_selection"] = { opts.restore_selection, "b", true },
|
|
})
|
|
|
|
if opts.args and not vim.islist(opts.args) then
|
|
opts.args = { opts.args }
|
|
end
|
|
|
|
if timeout <= 0 then
|
|
return vim.rpcrequest(vim.g.vscode_channel, "vscode-action", name, opts)
|
|
end
|
|
|
|
local done = false
|
|
local err, res
|
|
opts.callback = function(_err, _res)
|
|
err = _err
|
|
res = _res
|
|
done = true
|
|
end
|
|
M.action(name, opts)
|
|
vim.wait(timeout, function()
|
|
return done
|
|
end)
|
|
if done then
|
|
if err == nil then
|
|
return res
|
|
else
|
|
error(err)
|
|
end
|
|
else
|
|
error(string.format("Call '%s' timed out.", name))
|
|
end
|
|
end
|
|
|
|
--- Evaluate javascript synchronously inside vscode with access to the
|
|
--- [VSCode API](https://code.visualstudio.com/api/references/vscode-api) and return the result.
|
|
---
|
|
---@param code string the javascript code to run
|
|
--- - the code runs in an async function context
|
|
--- (so `await` can be used. Make sure to `await` if calling an async function from the VSCode API)
|
|
--- - use `return` to return a value to lua
|
|
--- - use the `vscode` variable to access the VSCode API
|
|
--- - use the `args` variable to access any arguments passed from lua
|
|
---@param opts? table Optional options table, all fields are optional
|
|
--- - args: (any) Optional arguments to serialize and make available to the code being run (as the `args` variable)
|
|
---@param timeout? number Timeout in milliseconds. The default value is -1, which means no timeout.
|
|
---
|
|
---@return any: the result of evaluating the given code in VSCode
|
|
function M.eval(code, opts, timeout)
|
|
vim.validate({
|
|
code = { code, "string" },
|
|
opts = { opts, "table", true },
|
|
timeout = { timeout, "number", true },
|
|
})
|
|
opts = opts or {}
|
|
opts.args = { code, opts.args }
|
|
return M.call("eval", opts, timeout)
|
|
end
|
|
|
|
--- Evaluate javascript asynchronously inside vscode with access to the
|
|
--- [VSCode API](https://code.visualstudio.com/api/references/vscode-api).
|
|
---
|
|
---@param code string the javascript code to run
|
|
---@param opts? table Optional options table, all fields are optional
|
|
--- - args: (any) Optional arguments to serialize and make available to the code being run (as the `args` variable)
|
|
--- - callback: (function(err: string|nil, ret: any))
|
|
--- Optional callback function to handle the evaluated result.
|
|
--- The first argument is the error message, and the second is the result.
|
|
--- If no callback is provided, any error message will be shown as a notification in VSCode.
|
|
function M.eval_async(code, opts)
|
|
vim.validate({
|
|
code = { code, "string" },
|
|
opts = { opts, "table", true },
|
|
})
|
|
opts = opts or {}
|
|
opts.args = { code, opts.args }
|
|
M.action("eval", opts)
|
|
end
|
|
|
|
---------------------------
|
|
------- Event Hooks -------
|
|
---------------------------
|
|
|
|
local EVENT_STATE = {}
|
|
|
|
--[[
|
|
List of events:
|
|
|
|
event -> args
|
|
-------------
|
|
]]
|
|
|
|
---@param event string
|
|
---@param callback function
|
|
function M.on(event, callback)
|
|
vim.validate({
|
|
event = { event, "string" },
|
|
callback = { callback, "function" },
|
|
})
|
|
local cbs = EVENT_STATE[event] or {}
|
|
if not vim.tbl_contains(cbs, callback) then
|
|
table.insert(cbs, callback)
|
|
EVENT_STATE[event] = cbs
|
|
end
|
|
end
|
|
|
|
---@param event string
|
|
---@vararg any
|
|
function M.fire_event(event, ...)
|
|
local args = { ... }
|
|
vim.schedule(function()
|
|
vim.tbl_map(function(cb)
|
|
cb(unpack(args))
|
|
end, EVENT_STATE[event] or {})
|
|
end)
|
|
end
|
|
|
|
-------------------------------------------
|
|
------- VSCode settings integration -------
|
|
-------------------------------------------
|
|
|
|
---Check if configuration has a certain value.
|
|
---@param name string|string[] The configuration name or an array of configuration names.
|
|
---@return boolean|boolean[] Returns true if the configuration has a certain value, false otherwise.
|
|
--- If name is an array, returns an array of booleans indicating whether each configuration
|
|
--- has a certain value or not.
|
|
function M.has_config(name)
|
|
vim.validate({ name = { name, { "s", "t" } } })
|
|
return M.call("has_config", { args = { name } })
|
|
end
|
|
|
|
---Get configuration value
|
|
---@param name string|string[] The configuration name or an array of configuration names.
|
|
---@return unknown|unknown[] The value of the configuration. If name is an array,
|
|
--- returns an array of values corresponding to each configuration.
|
|
function M.get_config(name)
|
|
vim.validate({ name = { name, { "s", "t" } } })
|
|
return M.call("get_config", { args = { name } })
|
|
end
|
|
|
|
---Update configuration value
|
|
---@param name string|string[] The configuration name or an array of configuration names.
|
|
---@param value unknown|unknown[] The new value for the configuration.
|
|
---@param target nil|"global"|"workspace"|"workspace_folder" The configuration target. Defaults to "workspace_folder".
|
|
function M.update_config(name, value, target)
|
|
vim.validate({ name = { name, { "s", "t" } } })
|
|
local name_is_table = type(name) == "table"
|
|
local value_is_table = type(value) == "table"
|
|
if name_is_table and not value_is_table then
|
|
error([[The "name" is a table, but the "value" is not]])
|
|
elseif value_is_table and not name_is_table then
|
|
error([[The "value" is a table, but the "name" is not]])
|
|
end
|
|
assert(
|
|
target == nil or target == "global" or target == "workspace" or target == "workspace_folder",
|
|
[[The "target" must be nil or one of "global", "workspace", or "workspace_folder"]]
|
|
)
|
|
return M.call("update_config", { args = { name, value, target } })
|
|
end
|
|
|
|
---------------------------
|
|
------ Notifications ------
|
|
---------------------------
|
|
|
|
--- Display a notification to the user.
|
|
---
|
|
---@param msg string Content of the notification to show to the user.
|
|
---@param level integer|nil One of the values from |vim.log.levels|.
|
|
---@param opts table|nil Optional parameters. Unused by default.
|
|
---@diagnostic disable-next-line: unused-local
|
|
function M.notify(msg, level, opts)
|
|
local levels = vim.log.levels
|
|
level = level or levels.INFO
|
|
|
|
-- legacy
|
|
if type(level) == "string" then
|
|
if level == "error" then
|
|
level = levels.ERROR
|
|
elseif level == "warn" then
|
|
level = levels.WARN
|
|
else
|
|
level = levels.INFO
|
|
end
|
|
end
|
|
|
|
local cmd
|
|
if level >= levels.ERROR then
|
|
cmd = "await vscode.window.showErrorMessage(args)"
|
|
elseif level >= levels.WARN then
|
|
cmd = "await vscode.window.showWarningMessage(args)"
|
|
else
|
|
cmd = "await vscode.window.showInformationMessage(args)"
|
|
end
|
|
|
|
M.eval_async(cmd, { args = msg })
|
|
end
|
|
|
|
---------------------------------
|
|
------ map-operator helper ------
|
|
---------------------------------
|
|
|
|
do
|
|
---@class Context
|
|
---@field range lsp.Range
|
|
---@field is_linewise boolean true indicates linewise, otherwise it is charwise.
|
|
---@field is_single_line boolean true if start.line and end.line are equal.
|
|
---@field is_current_line boolean is single line, and is current line
|
|
|
|
local op_func_id = 0
|
|
|
|
---@see map-operator
|
|
---
|
|
---Example: Remap 'gq' to use 'editor.action.formatSelection'
|
|
---
|
|
---```lua
|
|
--- local format = vscode.to_op(function(ctx)
|
|
--- vscode.action("editor.action.formatSelection", { range = ctx.range })
|
|
--- end)
|
|
---
|
|
--- vim.keymap.set({ "n", "x" }, "gq", format, { expr = true })
|
|
--- vim.keymap.set({ "n" }, "gqq", function()
|
|
--- return format() .. "_"
|
|
--- end, { expr = true })
|
|
---````
|
|
function M.to_op(func)
|
|
op_func_id = op_func_id + 1
|
|
local op_func_name = "__vscode_op_func_" .. tostring(op_func_id)
|
|
local operatorfunc = "v:lua." .. op_func_name
|
|
|
|
local op_func = function(motion)
|
|
local mode = api.nvim_get_mode().mode
|
|
if not motion then
|
|
if mode == "n" then
|
|
vim.go.operatorfunc = operatorfunc
|
|
return "g@"
|
|
elseif mode ~= "\x16" and mode:lower() ~= "v" then
|
|
return "<Ignore>"
|
|
end
|
|
end
|
|
|
|
local start_pos
|
|
local end_pos
|
|
if motion then
|
|
start_pos = api.nvim_buf_get_mark(0, "[")
|
|
end_pos = api.nvim_buf_get_mark(0, "]")
|
|
else
|
|
local a = fn.getpos("v")
|
|
local b = fn.getpos(".")
|
|
start_pos = { a[2], a[3] - 1 }
|
|
end_pos = { b[2], b[3] - 1 }
|
|
end
|
|
|
|
if start_pos[1] > end_pos[1] or (start_pos[1] == end_pos[1] and start_pos[2] > end_pos[2]) then
|
|
start_pos, end_pos = end_pos, start_pos
|
|
end
|
|
|
|
local is_linewise = motion == "line" or mode == "V"
|
|
if is_linewise then
|
|
start_pos = { start_pos[1], 0 }
|
|
end_pos = { end_pos[1], #fn.getline(end_pos[1]) }
|
|
end
|
|
|
|
local range = vim.lsp.util.make_given_range_params(start_pos, end_pos, 0, "utf-16").range
|
|
local is_single_line = range.start.line == range["end"].line
|
|
local is_current_line = is_single_line and range.start.line == fn.line(".") - 1
|
|
---@type Context
|
|
local ctx = {
|
|
range = range,
|
|
is_linewise = is_linewise,
|
|
is_single_line = is_single_line,
|
|
is_current_line = is_current_line,
|
|
}
|
|
func(ctx)
|
|
return "<Ignore>"
|
|
end
|
|
|
|
_G[op_func_name] = op_func
|
|
return _G[op_func_name]
|
|
end
|
|
end
|
|
|
|
-----------------------
|
|
------ utilities ------
|
|
-----------------------
|
|
|
|
---Perform operations in insert mode
|
|
---If in visual mode, this function will preserve the selection after
|
|
---switching to insert mode.
|
|
---
|
|
---Example:
|
|
---Make `editor.action.addSelectionToNextFindMatch` work correctly in any mode.
|
|
---This is the behavior of the default VSCode shortcut Ctrl+d: Add Selection To Next Find Match.
|
|
---
|
|
---```lua
|
|
---vim.keymap.set({ "n", "x", "i" }, "<C-d>", function()
|
|
--- vscode.with_insert(function()
|
|
--- vscode.action("editor.action.addSelectionToNextFindMatch")
|
|
--- end)
|
|
---end)
|
|
---````
|
|
---
|
|
---@param callback function Callback function to run after switching to insert mode.
|
|
---@param ms? number Milliseconds to defer the callback. Defaults to 30.
|
|
function M.with_insert(callback, ms)
|
|
vim.validate({ callback = { callback, "f" }, ms = { ms, "n", true } })
|
|
|
|
local mode = api.nvim_get_mode().mode
|
|
|
|
local startinsert = function(keys)
|
|
keys = api.nvim_replace_termcodes(keys, true, true, true)
|
|
api.nvim_feedkeys(keys, "n", false)
|
|
end
|
|
|
|
---@param ranges lsp.Range[]|nil`
|
|
local run_callback = function(ranges)
|
|
if ranges then
|
|
M.action("start-multiple-cursors", { args = { ranges }, callback = callback })
|
|
else
|
|
M.action("noop", { callback = callback })
|
|
end
|
|
end
|
|
|
|
--- Insert ---
|
|
if mode == "i" then
|
|
run_callback()
|
|
return
|
|
end
|
|
|
|
--- Normal ---
|
|
if mode == "n" then
|
|
startinsert("i")
|
|
run_callback()
|
|
return
|
|
end
|
|
|
|
--- Visual ---
|
|
if mode:match("[vV\x16]") then
|
|
local A = fn.getpos("v")
|
|
local B = fn.getpos(".")
|
|
local start_pos = { A[2], A[3] - 1 }
|
|
local end_pos = { B[2], B[3] - 1 }
|
|
|
|
if start_pos[1] > end_pos[1] or (start_pos[1] == end_pos[1] and start_pos[2] > end_pos[2]) then
|
|
start_pos, end_pos = end_pos, start_pos
|
|
end
|
|
|
|
if mode == "V" then
|
|
start_pos = { start_pos[1], 0 }
|
|
end_pos = { end_pos[1], #fn.getline(end_pos[1]) }
|
|
end
|
|
|
|
local range = vim.lsp.util.make_given_range_params(start_pos, end_pos, 0, "utf-16").range
|
|
local ranges = { range }
|
|
|
|
api.nvim_win_set_cursor(0, end_pos)
|
|
startinsert("<Esc>" .. (vim.o.selection ~= "exclusive" and "a" or "i"))
|
|
|
|
-- Wait for the cursor synchronization task to complete in VSCode
|
|
vim.defer_fn(function()
|
|
run_callback(ranges)
|
|
end, ms or 30)
|
|
|
|
return
|
|
end
|
|
|
|
--- Other ---
|
|
startinsert("<Esc><Esc>i")
|
|
run_callback()
|
|
end
|
|
|
|
return M
|