commit 107d3c5db21ace8fa271989417036a3ca622ae2f Author: David Ibia Date: Mon Jul 21 16:53:44 2025 +0100 feat(lua): add utility functions to detect Lua, OS, Python environment, TypeScript environment, Git root, and safe function invocation. diff --git a/lua/absolute/utils/detect-lua.lua b/lua/absolute/utils/detect-lua.lua new file mode 100644 index 0000000..1202dea --- /dev/null +++ b/lua/absolute/utils/detect-lua.lua @@ -0,0 +1,39 @@ +function GetLuaPath(version) + local path = vim.fn.expand("$HOME") .. "/.luarocks/share/lua/" .. version .. "/?.lua;" + path = path .. vim.fn.expand("$HOME") .. "/.luarocks/share/lua/" .. version .. "/?/init.lua;" + return path +end + +function GetInstalledLuaVersion() + -- List the directory contents of the lua rocks directory + local rocks = vim.fn.glob(vim.fn.expand("$HOME") .. "/.luarocks/share/lua/") + + -- Split the string into a table + + rocks = vim.split(rocks, "\n") + + -- Sort the table highest to lowest + + table.sort(rocks, function(a, b) + return tonumber(a) > tonumber(b) + end) + + return rocks +end + +function GetLatestLuaVersion() + local rocks = GetInstalledLuaVersion() + local preferredVersion = rocks[1] + + local path = GetLuaPath(preferredVersion) + + return path +end + +M = {} + +M.GetLuaPath = GetLuaPath +M.GetInstalledLuaVersion = GetInstalledLuaVersion +M.GetLatestLuaVersion = GetLatestLuaVersion + +return M diff --git a/lua/absolute/utils/detect-os.lua b/lua/absolute/utils/detect-os.lua new file mode 100644 index 0000000..6b057cd --- /dev/null +++ b/lua/absolute/utils/detect-os.lua @@ -0,0 +1,43 @@ +SUPPORTED_OS = { + WINDOWS = "Windows", + UNIX = "Unix", + MACOS = "macOS", +} + +local function isMacOS() + -- Try to execute `sw_vers` command which is specific to macOS + local result = os.execute("sw_vers > /dev/null 2>&1") + + -- `os.execute` returns true on success for Lua versions 5.2 and above + -- For Lua 5.1 and below, it returns the exit code of the command. On success, it should be zero. + -- Adjust the check if you're using Lua 5.1 or below: + -- if result == 0 then + if result then + return true + else + return false + end +end + +local function detectOS() + if os.getenv("OS") ~= nil then + return os.getenv("OS") -- This can be useful in Windows environments + elseif os.getenv("PATH"):find(";") then + return SUPPORTED_OS.WINDOWS + else + local is_macos = isMacOS() + + if is_macos then + return SUPPORTED_OS.MACOS + else + return SUPPORTED_OS.UNIX + end + end +end + +M = {} + +M.detect = detectOS +M.supported = SUPPORTED_OS + +return M diff --git a/lua/absolute/utils/detect-python-env.lua b/lua/absolute/utils/detect-python-env.lua new file mode 100644 index 0000000..af086c5 --- /dev/null +++ b/lua/absolute/utils/detect-python-env.lua @@ -0,0 +1,47 @@ +function CheckPoetryVirtualEnv() + local poetry = vim.fn.system("poetry env info -p 2>/dev/null") + if poetry == "" then + return nil + end + return poetry +end + +function ResolvePythonEnvironment() + -- Check if asdf is installed + + -- Get system global python env + local python = vim.fn.system("which python3") + -- Check of virtual environments exists + local virtual = os.getenv("VIRTUAL_ENV") or CheckPoetryVirtualEnv() + + if virtual == nil then + return { + "--python-executable", + python, + } + end + + return { + "--python-executable", + virtual .. "/bin/python", + } +end + +function ResolvePythonEnvironmentAsString() + local env = ResolvePythonEnvironment() + if env == nil then + return "" + end + + local path = table.concat(env, " ") + + return path +end + +M = {} + +M.ResolvePythonEnvironmentAsString = ResolvePythonEnvironmentAsString +M.ResolvePythonEnvironment = ResolvePythonEnvironment +M.CheckPoetryVirtualEnv = CheckPoetryVirtualEnv + +return M diff --git a/lua/absolute/utils/detect-typescript-env.lua b/lua/absolute/utils/detect-typescript-env.lua new file mode 100644 index 0000000..ef963ac --- /dev/null +++ b/lua/absolute/utils/detect-typescript-env.lua @@ -0,0 +1,67 @@ +M = {} +local function trim(s) + return s:gsub("^%s*(.-)%s*$", "%1") +end + +local ResolveTypescriptServer = function() + -- Find the package.json file + local viable = vim.fn.system("find . -name 'package.json' -maxdepth 3 | xargs dirname") + viable = trim(viable) + + if viable == "" then + return nil + end + + -- Check for lock files + local find_npm_result = trim(vim.fn.system("find . -name 'package-lock.json' -maxdepth 3 | wc -l")) + local find_pnpm_result = trim(vim.fn.system("find . -name 'pnpm-lock.yaml' -maxdepth 3 | wc -l")) + local find_yarn_result = trim(vim.fn.system("find . -name 'yarn.lock' -maxdepth 3 | wc -l")) + + local is_npm = find_npm_result == "1" + local is_pnpm = find_pnpm_result == "1" + local is_yarn = find_yarn_result == "1" + + -- if no package lock files are found, end early + if not is_npm and not is_pnpm and not is_yarn then + return nil + end + + -- Determine the preferred package manager + local ppm = "pnpm" -- Default to pnpm + if is_npm then + ppm = "npm" + end + if is_yarn then + ppm = "yarn" + end + + -- Resolve TypeScript server path + local base_command = ppm .. " list typescript --dir " .. viable .. " --json" + local base_result = trim(vim.fn.system(base_command)) + + if base_result == "" then + vim.notify("Failed to run " .. base_command, vim.log.levels.ERROR) + return nil + end + + local typescript_path = nil + + if ppm == "pnpm" or ppm == "npm" or ppm == "yarn" then + local ok, parsed = pcall(vim.fn.json_decode, base_result) + if ok and parsed and #parsed > 0 and parsed[1].path then + typescript_path = parsed[1].path .. "/node_modules/typescript/lib" + end + end + + if not typescript_path or typescript_path == "" then + vim.notify("TypeScript not found. Please install TypeScript globally or in the project.", vim.log.levels.ERROR) + return nil + end + + return typescript_path +end + +M.ResolveTypescriptServer = ResolveTypescriptServer +M.trim = trim + +return M diff --git a/lua/absolute/utils/get-git-root.lua b/lua/absolute/utils/get-git-root.lua new file mode 100644 index 0000000..9748c64 --- /dev/null +++ b/lua/absolute/utils/get-git-root.lua @@ -0,0 +1,10 @@ +function GetProjectRoot() + local git_root = vim.fn.systemlist("git rev-parse --show-toplevel")[1] + if git_root == nil then + return vim.fn.getcwd() + else + return git_root + end +end + +return GetProjectRoot diff --git a/lua/absolute/utils/safe-invoke.lua b/lua/absolute/utils/safe-invoke.lua new file mode 100644 index 0000000..8dcfddc --- /dev/null +++ b/lua/absolute/utils/safe-invoke.lua @@ -0,0 +1,15 @@ +function SafeInvoke(func) + -- safely invoke a functions + -- if the function returns an error, return an empty string + -- otherwise return the result of the function + + local ok, result = pcall(func) + + if ok then + return result + else + return "" + end +end + +return SafeInvoke diff --git a/lua/absolute/utils/telescope_image_preview.lua b/lua/absolute/utils/telescope_image_preview.lua new file mode 100644 index 0000000..ce0a272 --- /dev/null +++ b/lua/absolute/utils/telescope_image_preview.lua @@ -0,0 +1,127 @@ +-- Sourced from github +-- https://github.com/3rd/image.nvim/issues/183#issuecomment-2572606511 + +M = {} + +local supported_images = { "svg", "png", "jpg", "jpeg", "gif", "webp", "avif" } +local from_entry = require("telescope.from_entry") +local Path = require("plenary.path") +local conf = require("telescope.config").values +local Previewers = require("telescope.previewers") + +local previewers = require("telescope.previewers") +local image_api = require("image") + +local is_image_preview = false +local image = nil +local last_file_path = "" + +local is_supported_image = function(filepath) + local split_path = vim.split(filepath:lower(), ".", { plain = true }) + local extension = split_path[#split_path] + return vim.tbl_contains(supported_images, extension) +end + +local delete_image = function() + if not image then + return + end + + image:clear() + + is_image_preview = false +end + +local create_image = function(filepath, winid, bufnr) + image = image_api.hijack_buffer(filepath, winid, bufnr) + + if not image then + return + end + + vim.schedule(function() + image:render() + end) + + is_image_preview = true +end + +local function defaulter(f, default_opts) + default_opts = default_opts or {} + return { + new = function(opts) + if conf.preview == false and not opts.preview then + return false + end + opts.preview = type(opts.preview) ~= "table" and {} or opts.preview + if type(conf.preview) == "table" then + for k, v in pairs(conf.preview) do + opts.preview[k] = vim.F.if_nil(opts.preview[k], v) + end + end + return f(opts) + end, + __call = function() + local ok, err = pcall(f(default_opts)) + if not ok then + error(debug.traceback(err)) + end + end, + } +end + +-- NOTE: Add teardown to cat previewer to clear image when close Telescope +local file_previewer = defaulter(function(opts) + opts = opts or {} + local cwd = opts.cwd or vim.fn.getcwd() + return Previewers.new_buffer_previewer({ + title = "File Preview", + dyn_title = function(_, entry) + return Path:new(from_entry.path(entry, true)):normalize(cwd) + end, + + get_buffer_by_name = function(_, entry) + return from_entry.path(entry, true) + end, + + define_preview = function(self, entry, _) + local p = from_entry.path(entry, true) + if p == nil or p == "" then + return + end + + conf.buffer_previewer_maker(p, self.state.bufnr, { + bufname = self.state.bufname, + winid = self.state.winid, + preview = opts.preview, + }) + end, + + teardown = function(_) + if is_image_preview then + delete_image() + end + end, + }) +end, {}) + +local buffer_previewer_maker = function(filepath, bufnr, opts) + -- NOTE: Clear image when preview other file + if is_image_preview and last_file_path ~= filepath then + delete_image() + end + + last_file_path = filepath + + if is_supported_image(filepath) then + filepath = string.gsub(filepath, " ", "%%20"):gsub("\\", "") + create_image(filepath, opts.winid, bufnr) + else + previewers.buffer_previewer_maker(filepath, bufnr, opts) + end +end + +M.buffer_previewer_maker = buffer_previewer_maker +M.file_previewer = file_previewer.new + +return M