534 lines
14 KiB
Lua
534 lines
14 KiB
Lua
-- Some functions adapted from: https://github.com/orbitalquark/textadept-lsp
|
|
-- and others added as needed.
|
|
--
|
|
-- @copyright Jefferson Gonzalez
|
|
-- @license MIT
|
|
|
|
local core = require "core"
|
|
local common = require "core.common"
|
|
local config = require "core.config"
|
|
local json = require "plugins.lsp.json"
|
|
|
|
local util = {}
|
|
|
|
---Check if the given file is currently opened on the editor.
|
|
---@param abs_filename string
|
|
function util.doc_is_open(abs_filename)
|
|
-- make path separator consistent
|
|
abs_filename = abs_filename:gsub("\\", "/")
|
|
for _, doc in ipairs(core.docs) do
|
|
---@cast doc core.doc
|
|
if doc.abs_filename then
|
|
local doc_path = doc.abs_filename:gsub("\\", "/")
|
|
if doc_path == abs_filename then
|
|
return true;
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
---Converts a utf-8 column position into the equivalent utf-16 position.
|
|
---@param doc core.doc
|
|
---@param line integer
|
|
---@param column integer
|
|
---@return integer col_position
|
|
function util.doc_utf8_to_utf16(doc, line, column)
|
|
local ltext = doc.lines[line]
|
|
local ltext_len = ltext and #ltext or 0
|
|
local ltext_ulen = ltext and utf8extra.len(ltext) or 0
|
|
column = common.clamp(column, 1, ltext_len > 0 and ltext_len or 1)
|
|
-- no need for conversion so return column as is
|
|
if ltext_len == ltext_ulen then return column end
|
|
if column > 1 then
|
|
local col = 1
|
|
for pos, code in utf8extra.next, ltext do
|
|
if pos >= column then
|
|
return col
|
|
end
|
|
-- Codepoints that high are encoded using surrogate pairs
|
|
if code < 0x010000 then
|
|
col = col + 1
|
|
else
|
|
col = col + 2
|
|
end
|
|
end
|
|
return col
|
|
end
|
|
return column
|
|
end
|
|
|
|
---Converts a utf-16 column position into the equivalent utf-8 position.
|
|
---@param doc core.doc
|
|
---@param line integer
|
|
---@param column integer
|
|
---@return integer col_position
|
|
function util.doc_utf16_to_utf8(doc, line, column)
|
|
local ltext = doc.lines[line]
|
|
local ltext_len = ltext and #ltext or 0
|
|
local ltext_ulen = ltext and utf8extra.len(ltext) or 0
|
|
column = common.clamp(column, 1, ltext_len > 0 and ltext_len or 1)
|
|
-- no need for conversion so return column as is
|
|
if ltext_len == ltext_ulen then return column end
|
|
if column > 1 then
|
|
local col = 1
|
|
local utf8_pos = 1
|
|
for pos, code in utf8extra.next, ltext do
|
|
if col >= column then
|
|
return pos
|
|
end
|
|
utf8_pos = pos
|
|
-- Codepoints that high are encoded using surrogate pairs
|
|
if code < 0x010000 then
|
|
col = col + 1
|
|
else
|
|
col = col + 2
|
|
end
|
|
end
|
|
return utf8_pos
|
|
end
|
|
return column
|
|
end
|
|
|
|
---Split a string by the given delimeter
|
|
---@param s string The string to split
|
|
---@param delimeter string Delimeter without lua patterns
|
|
---@param delimeter_pattern? string Optional delimeter with lua patterns
|
|
---@return table
|
|
---@return boolean ends_with_delimiter
|
|
function util.split(s, delimeter, delimeter_pattern)
|
|
if not delimeter_pattern then
|
|
delimeter_pattern = delimeter
|
|
end
|
|
|
|
local last_idx = 1
|
|
local result = {}
|
|
for match_idx, afer_match_idx in s:gmatch("()"..delimeter_pattern.."()") do
|
|
table.insert(result, string.sub(s, last_idx, match_idx - 1))
|
|
last_idx = afer_match_idx
|
|
end
|
|
if last_idx > #s then
|
|
return result, true
|
|
else
|
|
table.insert(result, string.sub(s, last_idx))
|
|
return result, false
|
|
end
|
|
end
|
|
|
|
---Get the extension component of a filename.
|
|
---@param filename string
|
|
---@return string
|
|
function util.file_extension(filename)
|
|
local parts = util.split(filename, "%.")
|
|
if #parts > 1 then
|
|
return parts[#parts]:gsub("%%", "")
|
|
end
|
|
|
|
return filename
|
|
end
|
|
|
|
---Check if a file exists.
|
|
---@param file_path string
|
|
---@return boolean
|
|
function util.file_exists(file_path)
|
|
local file = io.open(file_path, "r")
|
|
if file ~= nil then
|
|
file:close()
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
---Converts the given LSP DocumentUri into a valid filename path.
|
|
---@param uri string LSP DocumentUri to convert into a filename.
|
|
---@return string
|
|
function util.tofilename(uri)
|
|
local filename = ""
|
|
if PLATFORM == "Windows" then
|
|
filename = uri:gsub('^file:///', '')
|
|
else
|
|
filename = uri:gsub('^file://', '')
|
|
end
|
|
|
|
filename = filename:gsub(
|
|
'%%(%x%x)',
|
|
function(hex) return string.char(tonumber(hex, 16)) end
|
|
)
|
|
|
|
if PLATFORM == "Windows" then filename = filename:gsub('/', '\\') end
|
|
|
|
return filename
|
|
end
|
|
|
|
---Convert a file path to a LSP valid uri.
|
|
---@param file_path string
|
|
---@return string
|
|
function util.touri(file_path)
|
|
if PLATFORM ~= "Windows" then
|
|
file_path = 'file://' .. file_path
|
|
else
|
|
file_path = 'file:///' .. file_path:gsub('\\', '/')
|
|
end
|
|
|
|
return file_path
|
|
end
|
|
|
|
---Converts a document range returned by lsp to a valid document selection.
|
|
---@param range table LSP Range.
|
|
---@param doc? core.doc
|
|
---@return integer line1
|
|
---@return integer col1
|
|
---@return integer line2
|
|
---@return integer col2
|
|
function util.toselection(range, doc)
|
|
local line1 = range.start.line + 1
|
|
local col1 = range.start.character + 1
|
|
local line2 = range['end'].line + 1
|
|
local col2 = range['end'].character + 1
|
|
|
|
if doc then
|
|
col1 = util.doc_utf16_to_utf8(doc, line1, col1)
|
|
col2 = util.doc_utf16_to_utf8(doc, line2, col2)
|
|
end
|
|
|
|
return line1, col1, line2, col2
|
|
end
|
|
|
|
---Opens the given location on a external application.
|
|
---@param location string
|
|
function util.open_external(location)
|
|
local filelauncher = ""
|
|
if PLATFORM == "Windows" then
|
|
filelauncher = "start"
|
|
elseif PLATFORM == "Mac OS X" then
|
|
filelauncher = "open"
|
|
else
|
|
filelauncher = "xdg-open"
|
|
end
|
|
|
|
-- non-Windows platforms need the text quoted (%q)
|
|
if PLATFORM ~= "Windows" then
|
|
location = string.format("%q", location)
|
|
end
|
|
|
|
system.exec(filelauncher .. " " .. location)
|
|
end
|
|
|
|
---Prettify json output and logs it if config.lsp.log_file is set.
|
|
---@param code string
|
|
---@return string
|
|
function util.jsonprettify(code)
|
|
if config.plugins.lsp.prettify_json then
|
|
code = json.prettify(code)
|
|
end
|
|
|
|
if config.plugins.lsp.log_file and #config.plugins.lsp.log_file > 0 then
|
|
local log = io.open(config.plugins.lsp.log_file, "a+")
|
|
log:write("Output: \n" .. tostring(code) .. "\n\n")
|
|
log:close()
|
|
end
|
|
|
|
return code
|
|
end
|
|
|
|
---Gets the last component of a path. For example:
|
|
---/my/path/to/somwhere would return somewhere.
|
|
---@param path string
|
|
---@return string
|
|
function util.getpathname(path)
|
|
local components = {}
|
|
if PLATFORM == "Windows" then
|
|
components = util.split(path, "\\")
|
|
else
|
|
components = util.split(path, "/")
|
|
end
|
|
|
|
if #components > 0 then
|
|
return components[#components]
|
|
end
|
|
|
|
return path
|
|
end
|
|
|
|
---Check if a value is on a table.
|
|
---@param value any
|
|
---@param table_array table
|
|
---@return boolean
|
|
function util.intable(value, table_array)
|
|
for _, element in pairs(table_array) do
|
|
if element == value then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
---Check if a command exists on the system by inspecting the PATH envar.
|
|
---@param command string
|
|
---@return boolean
|
|
function util.command_exists(command)
|
|
local command_win = nil
|
|
|
|
if PLATFORM == "Windows" then
|
|
if not command:find("%.exe$") then
|
|
command_win = command .. ".exe"
|
|
end
|
|
end
|
|
|
|
if
|
|
util.file_exists(command)
|
|
or
|
|
(command_win and util.file_exists(command_win))
|
|
then
|
|
return true
|
|
end
|
|
|
|
local env_path = os.getenv("PATH")
|
|
local path_list = {}
|
|
|
|
if PLATFORM ~= "Windows" then
|
|
path_list = util.split(env_path, ":")
|
|
else
|
|
path_list = util.split(env_path, ";")
|
|
end
|
|
|
|
-- Automatic support for brew, macports, etc...
|
|
if PLATFORM == "Mac OS X" then
|
|
if
|
|
system.get_file_info("/usr/local/bin")
|
|
and
|
|
not string.find(env_path, "/usr/local/bin", 1, true)
|
|
then
|
|
table.insert(path_list, 1, "/usr/local/bin")
|
|
end
|
|
end
|
|
|
|
for _, path in pairs(path_list) do
|
|
local path_fix = path:gsub("[/\\]$", "") .. PATHSEP
|
|
if util.file_exists(path_fix .. command) then
|
|
return true
|
|
elseif command_win and util.file_exists(path_fix .. command_win) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
---From a list of executable names get the one that is installed
|
|
---on the system or first one if none of them exists.
|
|
---@param executables table<integer,string>
|
|
---@return string executable_name
|
|
function util.get_best_executable(executables)
|
|
for _, executable in ipairs(executables) do
|
|
if util.command_exists(executable) then
|
|
return executable
|
|
end
|
|
end
|
|
return executables[1]
|
|
end
|
|
|
|
---Remove by key from a table and returns a new
|
|
---table with element removed.
|
|
---@param table_object table
|
|
---@param key_name string|integer
|
|
---@return table
|
|
function util.table_remove_key(table_object, key_name)
|
|
local new_table = {}
|
|
for key, data in pairs(table_object) do
|
|
if key ~= key_name then
|
|
new_table[key] = data
|
|
end
|
|
end
|
|
|
|
return new_table
|
|
end
|
|
|
|
---Get a table specific field or nil if not found.
|
|
---@param t table The table we are going to search for the field.
|
|
---@param fieldset string A field spec in the format
|
|
---"parent[.child][.subchild]" eg: "myProp.subProp.subSubProp"
|
|
---@return any|nil The value of the given field or nil if not found.
|
|
function util.table_get_field(t, fieldset)
|
|
local fields = util.split(fieldset, ".", "%.")
|
|
local field = fields[1]
|
|
local value = nil
|
|
|
|
if field and #fields > 1 and t[field] then
|
|
local sub_fields = table.concat(fields, ".", 2)
|
|
value = util.table_get_field(t[field], sub_fields)
|
|
elseif field and #fields > 0 and t[field] then
|
|
value = t[field]
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
---Merge the content of the tables into a new one.
|
|
---Arguments from the later tables take precedence.
|
|
---Doesn't touch the original tables.
|
|
---`nil` arguments are ignored.
|
|
---@param ... table?
|
|
---@return table
|
|
function util.deep_merge(...)
|
|
local t = {}
|
|
local args = table.pack(...)
|
|
for i=1,args.n do
|
|
local other = args[i]
|
|
if other then
|
|
assert(type(other) == "table", string.format("Argument %d must be a table", i))
|
|
for k, v in pairs(other) do
|
|
if type(v) == "table" then
|
|
if type(t[k]) == "table" then
|
|
t[k] = util.deep_merge(t[k], v)
|
|
else
|
|
t[k] = util.deep_merge({}, v)
|
|
end
|
|
else
|
|
t[k] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return t
|
|
end
|
|
|
|
---Check if a table is really empty.
|
|
---@param t table
|
|
---@return boolean
|
|
function util.table_empty(t)
|
|
return next(t) == nil
|
|
end
|
|
|
|
---Convert markdown to plain text.
|
|
---@param text string
|
|
---@return string
|
|
function util.strip_markdown(text)
|
|
local clean_text = ""
|
|
local prev_line = ""
|
|
for match in (text.."\n"):gmatch("(.-)".."\n") do
|
|
match = match .. "\n"
|
|
|
|
-- strip markdown
|
|
local new_line = match
|
|
-- Block quotes
|
|
:gsub("^>+(%s*)", "%1")
|
|
-- headings
|
|
:gsub("^(%s*)######%s(.-)\n", "%1%2\n")
|
|
:gsub("^(%s*)#####%s(.-)\n", "%1%2\n")
|
|
:gsub("^(%s*)####%s(.-)\n", "%1%2\n")
|
|
:gsub("^(%s*)####%s(.-)\n", "%1%2\n")
|
|
:gsub("^(%s*)###%s(.-)\n", "%1%2\n")
|
|
:gsub("^(%s*)##%s(.-)\n", "%1%2\n")
|
|
:gsub("^(%s*)#%s(.-)\n", "%1%2\n")
|
|
-- heading custom id
|
|
:gsub("{#.-}", "")
|
|
-- emoji
|
|
:gsub(":[%w%-_]+:", "")
|
|
-- bold and italic
|
|
:gsub("%*%*%*(.-)%*%*%*", "%1")
|
|
:gsub("___(.-)___", "%1")
|
|
:gsub("%*%*_(.-)_%*%*", "%1")
|
|
:gsub("__%*(.-)%*__", "%1")
|
|
:gsub("___(.-)___", "%1")
|
|
-- bold
|
|
:gsub("%*%*(.-)%*%*", "%1")
|
|
:gsub("__(.-)__", "%1")
|
|
-- strikethrough
|
|
:gsub("%-%-(.-)%-%-", "%1")
|
|
-- italic
|
|
:gsub("%*(.-)%*", "%1")
|
|
:gsub("%s_(.-)_%s", "%1")
|
|
:gsub("\\_(.-)\\_", "_%1_")
|
|
:gsub("^_(.-)_", "%1")
|
|
-- code
|
|
:gsub("^%s*```(%w+)%s*\n", "")
|
|
:gsub("^%s*```%s*\n", "")
|
|
:gsub("``(.-)``", "%1")
|
|
:gsub("`(.-)`", "%1")
|
|
-- lines
|
|
:gsub("^%-%-%-%-*%s*\n", "")
|
|
:gsub("^%*%*%*%**%s*\n", "")
|
|
-- reference links
|
|
:gsub("^%[[^%^](.-)%]:.-\n", "")
|
|
-- footnotes
|
|
:gsub("^%[%^(.-)%]:%s+", "[%1]: ")
|
|
:gsub("%[%^(.-)%]", "[%1]")
|
|
-- Images
|
|
:gsub("!%[(.-)%]%((.-)%)", "")
|
|
-- links
|
|
:gsub("%s<(.-)>%s", "%1")
|
|
:gsub("%[(.-)%]%s*%[(.-)%]", "%1")
|
|
:gsub("%[(.-)%]%((.-)%)", "%1: %2")
|
|
-- remove escaped punctuations
|
|
:gsub("\\(%p)", "%1")
|
|
|
|
-- if paragraph put in same line
|
|
local is_paragraph = false
|
|
|
|
local prev_spaces = prev_line:match("^%g+")
|
|
local prev_endings = prev_line:match("[ \t\r\n]+$")
|
|
local new_spaces = new_line:match("^%g+")
|
|
|
|
if prev_spaces and new_spaces then
|
|
local new_lines = prev_endings ~= nil
|
|
and prev_endings:gsub("[ \t\r]+", "") or ""
|
|
|
|
if #new_lines == 1 then
|
|
is_paragraph = true
|
|
clean_text = clean_text:gsub("[%s\n]+$", "")
|
|
.. " " .. new_line:gsub("^%s+", "")
|
|
end
|
|
end
|
|
|
|
if not is_paragraph then
|
|
clean_text = clean_text .. new_line
|
|
end
|
|
|
|
prev_line = new_line
|
|
end
|
|
return clean_text
|
|
end
|
|
|
|
---@param text string
|
|
---@param font renderer.font
|
|
---@param max_width number
|
|
function util.wrap_text(text, font, max_width)
|
|
local lines = util.split(text, "\n")
|
|
local wrapped_text = ""
|
|
local longest_line = 0;
|
|
for _, line in ipairs(lines) do
|
|
local line_len = line:ulen() or 0
|
|
if line_len > longest_line then
|
|
longest_line = line_len
|
|
local line_width = font:get_width(line)
|
|
if line_width > max_width then
|
|
local words = util.split(line, " ")
|
|
local new_line = words[1] and words[1] or ""
|
|
wrapped_text = wrapped_text .. new_line
|
|
for w=2, #words do
|
|
if font:get_width(new_line .. " " .. words[w]) <= max_width then
|
|
new_line = new_line .. " " .. words[w]
|
|
wrapped_text = wrapped_text .. " " .. words[w]
|
|
else
|
|
wrapped_text = wrapped_text .. "\n" .. words[w]
|
|
new_line = words[w]
|
|
end
|
|
end
|
|
wrapped_text = wrapped_text .. "\n"
|
|
else
|
|
wrapped_text = wrapped_text .. line .. "\n"
|
|
end
|
|
else
|
|
wrapped_text = wrapped_text .. line .. "\n"
|
|
end
|
|
end
|
|
|
|
wrapped_text = wrapped_text:gsub("\n\n\n\n?", "\n\n"):gsub("%s*$", "")
|
|
|
|
return wrapped_text
|
|
end
|
|
|
|
|
|
return util
|