feat(lua): add utility functions to detect Lua, OS, Python environment, TypeScript environment, Git root, and safe function invocation.

This commit is contained in:
David Ibia
2025-07-21 16:53:44 +01:00
commit 107d3c5db2
7 changed files with 348 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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