diff --git a/lua/tabnine/binary.lua b/lua/tabnine/binary.lua index b8c7662..fa77619 100644 --- a/lua/tabnine/binary.lua +++ b/lua/tabnine/binary.lua @@ -8,7 +8,7 @@ local TabnineBinary = {} local config = require("tabnine.config") local api_version = "4.4.223" -local binaries_path = utils.script_path() .. "/binaries" +local binaries_path = utils.module_dir() .. "/binaries" local function arch_and_platform() local os_uname = uv.os_uname() diff --git a/lua/tabnine/chat/binary.lua b/lua/tabnine/chat/binary.lua index fbf9f52..fff0bd3 100644 --- a/lua/tabnine/chat/binary.lua +++ b/lua/tabnine/chat/binary.lua @@ -13,7 +13,7 @@ local function binary_name() end end -local binary_path = utils.script_path() .. "/../chat/target/release/" .. binary_name() +local binary_path = utils.module_dir() .. "/chat/target/release/" .. binary_name() function ChatBinary:available() return vim.fn.executable(binary_path) == 1 diff --git a/lua/tabnine/chat/init.lua b/lua/tabnine/chat/init.lua index 3b36647..4156fb9 100644 --- a/lua/tabnine/chat/init.lua +++ b/lua/tabnine/chat/init.lua @@ -7,8 +7,8 @@ local config = require("tabnine.config") local M = { enabled = false } -local CHAT_STATE_FILE = utils.script_path() .. "/../chat_state.json" -local CHAT_SETTINGS_FILE = utils.script_path() .. "/../chat_settings.json" +local CHAT_STATE_FILE = utils.module_dir() .. "/chat_state.json" +local CHAT_SETTINGS_FILE = utils.module_dir() .. "/chat_settings.json" local chat_state = nil local chat_settings = nil diff --git a/lua/tabnine/status.lua b/lua/tabnine/status.lua index 513f651..93f0333 100644 --- a/lua/tabnine/status.lua +++ b/lua/tabnine/status.lua @@ -3,7 +3,7 @@ local fn = vim.fn local utils = require("tabnine.utils") local M = {} -local DISABLED_FILE = utils.script_path() .. "/.disabled" +local DISABLED_FILE = utils.module_dir() .. "/.disabled" local config = require("tabnine.config") local state = require("tabnine.state") local tabnine_binary = require("tabnine.binary") diff --git a/lua/tabnine/utils.lua b/lua/tabnine/utils.lua index af9292c..fec79fe 100644 --- a/lua/tabnine/utils.lua +++ b/lua/tabnine/utils.lua @@ -1,105 +1,161 @@ local fn = vim.fn local api = vim.api - +local unpack = table.unpack or unpack +local pack = table.pack or vim.F.pack_len local M = {} -local last_changedtick = vim.b.changedtick +---@param func fun(...: unknown): any the callback +---@param delay integer delay in milliseconds +---@return fun(...: unknown) function M.debounce(func, delay) local timer_id return function(...) if timer_id then fn.timer_stop(timer_id) end - local args = { ... } + local args = pack(...) timer_id = fn.timer_start(delay, function() - func(unpack(args)) + return func(unpack(args, 1, args.n)) end) end end +---@param str string +---@return string[] function M.str_to_lines(str) return fn.split(str, "\n") end +---@param lines string[] +---@return string function M.lines_to_str(lines) return fn.join(lines, "\n") end +---@param str string +---@param suffix string +---@return string function M.remove_matching_suffix(str, suffix) if not M.ends_with(str, suffix) then return str end return str:sub(1, -#suffix - 1) end +---@param str string +---@param prefix string +---@return string function M.remove_matching_prefix(str, prefix) if not M.starts_with(str, prefix) then return str end return str:sub(#prefix) end +---@generic T +---@param tbl T[] | {n?: integer} The table to get a subset from +---@param from integer? defaults to 1 +---@param to integer? defaults to tbl.n or #tbl. +---@return T[] function M.subset(tbl, from, to) + to = to or tbl.n or #tbl -- support table.pack values if no end given + -- We can't use table.pack here because nvim_buf_set_extmark will error if non-integer keys are present + -- Ideally, implementations would ignore string keys in an 'array'. return { unpack(tbl, from, to) } end -function M.script_path() +---returns the directory of the running script +---@return string +function M.script_dir() local str = debug.getinfo(2, "S").source:sub(2) - return str:match("(.*/)") .. "../.." + return str:match("(.*/)") or "./" +end + +---returns the directory of the root of the module +---@return string +function M.module_dir() + -- HACK: This only works if this file is not moved! + return M.script_dir() .. "../.." end -function M.prequire(...) - local status, lib = pcall(require, ...) +---pcall require. Returns nil if the module is not found. +---Note: use @module to manage types. +---@param modname string +---@return unknown? +function M.prequire(modname) + local status, lib = pcall(require, modname) if status then return lib end return nil end +---@return boolean function M.pumvisible() local cmp = M.prequire("cmp") - if cmp then - return cmp.visible() - else - return vim.fn.pumvisible() > 0 - end + if cmp then return cmp.visible() end + return vim.fn.pumvisible() > 0 end +---Return the position of the cursor +---@return number line, number column function M.current_position() - return { fn.line("."), fn.col(".") } + ---@diagnostic disable-next-line: return-type-mismatch -- This is fine, they return non-nil values + return fn.line("."), fn.col(".") end +---Returns true if str ends with suffix. +---@param str string +---@param suffix string +---@return boolean function M.ends_with(str, suffix) - if str == "" then return true end - return str:sub(-#suffix) == suffix end +---Returns true if str starts with prefix. +---@param str string +---@param prefix string +---@return boolean function M.starts_with(str, prefix) - if str == "" then return true end - return str:sub(1, #prefix) == prefix end +---Returns true if the current cursor position is the end of the current line +---@return boolean function M.is_end_of_line() return fn.col(".") == fn.col("$") end +---Returns the text after the current cursor position to the end of the current line +---@return string function M.end_of_line() return api.nvim_buf_get_text(0, fn.line(".") - 1, fn.col(".") - 1, fn.line(".") - 1, fn.col("$"), {})[1] end -function M.document_changed() - local current_changedtick = last_changedtick - last_changedtick = vim.b.changedtick - return last_changedtick > current_changedtick +do + ---The last time we checked for a document change + local last_changedtick = vim.b.changedtick + ---Returns true if the document has changed since the last call + ---@return boolean + function M.document_changed() + local current_changedtick = last_changedtick + last_changedtick = vim.b.changedtick + return last_changedtick > current_changedtick + end end +---Returns the currently selected text. If no in visual mode, returns the empty string. +---@return string function M.selected_text() - local mode = vim.fn.mode() + local mode = vim.fn.mode() ---@type string if mode ~= "v" and mode ~= "V" and mode ~= "" then return "" end - local a_orig = vim.fn.getreg("a") + local a_orig = vim.fn.getreg("a", 1) vim.cmd([[silent! normal! "aygv]]) - local text = vim.fn.getreg("a") + ---@diagnostic disable-next-line: assign-type-mismatch -- This is fine. it returns a string + local text = vim.fn.getreg("a") ---@type string vim.fn.setreg("a", a_orig) return text end +---gets the unique values from the array +---@generic T +---@param array T[] +---@return T[] function M.set(array) - local set = {} - local uniqueValues = {} + local set = {} ---@type table + local uniqueValues = {} ---@type unknown[] for _, value in ipairs(array) do if not set[value] then @@ -111,39 +167,17 @@ function M.set(array) return uniqueValues end -function M.select_range(range) - local start_row, start_col, end_row, end_col = range[1][1], range[1][2], range[2][1], range[2][2] - - local v_table = { charwise = "v", linewise = "V", blockwise = "" } - selection_mode = selection_mode or "charwise" - - -- Normalise selection_mode - if vim.tbl_contains(vim.tbl_keys(v_table), selection_mode) then selection_mode = v_table[selection_mode] end - - -- enter visual mode if normal or operator-pending (no) mode - -- Why? According to https://learnvimscriptthehardway.stevelosh.com/chapters/15.html - -- If your operator-pending mapping ends with some text visually selected, Vim will operate on that text. - -- Otherwise, Vim will operate on the text between the original cursor position and the new position. - local mode = api.nvim_get_mode() - if mode.mode ~= selection_mode then - -- Call to `nvim_replace_termcodes()` is needed for sending appropriate command to enter blockwise mode - selection_mode = vim.api.nvim_replace_termcodes(selection_mode, true, true, true) - api.nvim_cmd({ cmd = "normal", bang = true, args = { selection_mode } }, {}) - end - - api.nvim_win_set_cursor(0, { start_row, start_col - 1 }) - vim.cmd("normal! o") - api.nvim_win_set_cursor(0, { end_row, end_col - 1 }) -end - -function M.select_range(range) +---Selects a given range of text +---@param range table +---@param selection_mode? 'charwise'|'linewise'|'blockwise'|'v'|'V'|'' +function M.select_range(range, selection_mode) local start_row, start_col, end_row, end_col = range[1][1], range[1][2], range[2][1], range[2][2] local v_table = { charwise = "v", linewise = "V", blockwise = "" } selection_mode = selection_mode or "charwise" -- Normalise selection_mode - if vim.tbl_contains(vim.tbl_keys(v_table), selection_mode) then selection_mode = v_table[selection_mode] end + selection_mode = v_table[selection_mode] or selection_mode -- enter visual mode if normal or operator-pending (no) mode -- Why? According to https://learnvimscriptthehardway.stevelosh.com/chapters/15.html