impatient.lua 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. -- modified version from https://github.com/lewis6991/impatient.nvim
  2. local vim = vim
  3. local api = vim.api
  4. local uv = vim.loop
  5. local _loadfile = loadfile
  6. local get_runtime = api.nvim__get_runtime
  7. local fs_stat = uv.fs_stat
  8. local mpack = vim.mpack
  9. local appdir = os.getenv "APPDIR"
  10. local M = {
  11. chunks = {
  12. cache = {},
  13. profile = nil,
  14. dirty = false,
  15. path = vim.fn.stdpath "cache" .. "/luacache_chunks",
  16. },
  17. modpaths = {
  18. cache = {},
  19. profile = nil,
  20. dirty = false,
  21. path = vim.fn.stdpath "cache" .. "/luacache_modpaths",
  22. },
  23. log = {},
  24. }
  25. _G.__luacache = M
  26. if not get_runtime then
  27. -- nvim 0.5 compat
  28. get_runtime = function(paths, all, _)
  29. local r = {}
  30. for _, path in ipairs(paths) do
  31. local found = api.nvim_get_runtime_file(path, all)
  32. for i = 1, #found do
  33. r[#r + 1] = found[i]
  34. end
  35. end
  36. return r
  37. end
  38. end
  39. local function log(...)
  40. M.log[#M.log + 1] = table.concat({ string.format(...) }, " ")
  41. end
  42. function M.print_log()
  43. for _, l in ipairs(M.log) do
  44. print(l)
  45. end
  46. end
  47. function M.enable_profile()
  48. local P = require "lvim.impatient.profile"
  49. M.chunks.profile = {}
  50. M.modpaths.profile = {}
  51. P.setup(M.modpaths.profile)
  52. M.print_profile = function()
  53. P.print_profile(M)
  54. end
  55. vim.cmd [[command! LuaCacheProfile lua _G.__luacache.print_profile()]]
  56. end
  57. local function hash(modpath)
  58. local stat = fs_stat(modpath)
  59. if stat then
  60. return stat.mtime.sec .. stat.mtime.nsec .. stat.size
  61. end
  62. error("Could not hash " .. modpath)
  63. end
  64. local function modpath_mangle(modpath)
  65. if appdir then
  66. modpath = modpath:gsub(appdir, "/$APPDIR")
  67. end
  68. return modpath
  69. end
  70. local function modpath_unmangle(modpath)
  71. if appdir then
  72. modpath = modpath:gsub("/$APPDIR", appdir)
  73. end
  74. return modpath
  75. end
  76. local function profile(m, entry, name, loader)
  77. if m.profile then
  78. local mp = m.profile
  79. mp[entry] = mp[entry] or {}
  80. if not mp[entry].loader and loader then
  81. mp[entry].loader = loader
  82. end
  83. if not mp[entry][name] then
  84. mp[entry][name] = uv.hrtime()
  85. end
  86. end
  87. end
  88. local function mprofile(mod, name, loader)
  89. profile(M.modpaths, mod, name, loader)
  90. end
  91. local function cprofile(path, name, loader)
  92. profile(M.chunks, path, name, loader)
  93. end
  94. local function get_runtime_file(basename, paths)
  95. -- Look in the cache to see if we have already loaded a parent module.
  96. -- If we have then try looking in the parents directory first.
  97. local parents = vim.split(basename, "/")
  98. for i = #parents, 1, -1 do
  99. local parent = table.concat(vim.list_slice(parents, 1, i), "/")
  100. local ppath = M.modpaths.cache[parent]
  101. if ppath then
  102. if ppath:sub(-9) == "/init.lua" then
  103. ppath = ppath:sub(1, -10) -- a/b/init.lua -> a/b
  104. else
  105. ppath = ppath:sub(1, -5) -- a/b.lua -> a/b
  106. end
  107. for _, path in ipairs(paths) do
  108. -- path should be of form 'a/b/c.lua' or 'a/b/c/init.lua'
  109. local modpath = ppath .. "/" .. path:sub(#("lua/" .. parent) + 2)
  110. if fs_stat(modpath) then
  111. return modpath, "cache(p)"
  112. end
  113. end
  114. end
  115. end
  116. -- What Neovim does by default; slowest
  117. local modpath = get_runtime(paths, false, { is_lua = true })[1]
  118. return modpath, "standard"
  119. end
  120. local function get_runtime_file_cached(basename, paths)
  121. local mp = M.modpaths
  122. if mp.cache[basename] then
  123. local modpath = mp.cache[basename]
  124. if fs_stat(modpath) then
  125. mprofile(basename, "resolve_end", "cache")
  126. return modpath
  127. end
  128. mp.cache[basename] = nil
  129. mp.dirty = true
  130. end
  131. local modpath, loader = get_runtime_file(basename, paths)
  132. if modpath then
  133. mprofile(basename, "resolve_end", loader)
  134. log("Creating cache for module %s", basename)
  135. mp.cache[basename] = modpath_mangle(modpath)
  136. mp.dirty = true
  137. end
  138. return modpath
  139. end
  140. local function extract_basename(pats)
  141. local basename
  142. -- Deconstruct basename from pats
  143. for _, pat in ipairs(pats) do
  144. for i, npat in ipairs {
  145. -- Ordered by most specific
  146. "lua/(.*)/init%.lua",
  147. "lua/(.*)%.lua",
  148. } do
  149. local m = pat:match(npat)
  150. if i == 2 and m and m:sub(-4) == "init" then
  151. m = m:sub(0, -6)
  152. end
  153. if not basename then
  154. if m then
  155. basename = m
  156. end
  157. elseif m and m ~= basename then
  158. -- matches are inconsistent
  159. return
  160. end
  161. end
  162. end
  163. return basename
  164. end
  165. local function get_runtime_cached(pats, all, opts)
  166. local fallback = false
  167. if all or not opts or not opts.is_lua then
  168. -- Fallback
  169. fallback = true
  170. end
  171. local basename
  172. if not fallback then
  173. basename = extract_basename(pats)
  174. end
  175. if fallback or not basename then
  176. return get_runtime(pats, all, opts)
  177. end
  178. return { get_runtime_file_cached(basename, pats) }
  179. end
  180. -- Copied from neovim/src/nvim/lua/vim.lua with two lines changed
  181. local function load_package(name)
  182. local basename = name:gsub("%.", "/")
  183. local paths = { "lua/" .. basename .. ".lua", "lua/" .. basename .. "/init.lua" }
  184. -- Original line:
  185. -- local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true})
  186. local found = { get_runtime_file_cached(basename, paths) }
  187. if #found > 0 then
  188. local f, err = loadfile(found[1])
  189. return f or error(err)
  190. end
  191. local so_paths = {}
  192. for _, trail in ipairs(vim._so_trails) do
  193. local path = "lua" .. trail:gsub("?", basename) -- so_trails contains a leading slash
  194. table.insert(so_paths, path)
  195. end
  196. -- Original line:
  197. -- found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true})
  198. found = { get_runtime_file_cached(basename, so_paths) }
  199. if #found > 0 then
  200. -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is
  201. -- a) strip prefix up to and including the first dash, if any
  202. -- b) replace all dots by underscores
  203. -- c) prepend "luaopen_"
  204. -- So "foo-bar.baz" should result in "luaopen_bar_baz"
  205. local dash = name:find("-", 1, true)
  206. local modname = dash and name:sub(dash + 1) or name
  207. local f, err = package.loadlib(found[1], "luaopen_" .. modname:gsub("%.", "_"))
  208. return f or error(err)
  209. end
  210. return nil
  211. end
  212. local function load_from_cache(path)
  213. local mc = M.chunks
  214. if not mc.cache[path] then
  215. return nil, string.format("No cache for path %s", path)
  216. end
  217. local mhash, codes = unpack(mc.cache[path])
  218. if mhash ~= hash(modpath_unmangle(path)) then
  219. mc.cache[path] = nil
  220. mc.dirty = true
  221. return nil, string.format("Stale cache for path %s", path)
  222. end
  223. local chunk = loadstring(codes)
  224. if not chunk then
  225. mc.cache[path] = nil
  226. mc.dirty = true
  227. return nil, string.format("Cache error for path %s", path)
  228. end
  229. return chunk
  230. end
  231. local function loadfile_cached(path)
  232. cprofile(path, "load_start")
  233. local chunk, err = load_from_cache(path)
  234. if chunk and not err then
  235. log("Loaded cache for path %s", path)
  236. cprofile(path, "load_end", "cache")
  237. return chunk
  238. end
  239. log(err)
  240. chunk, err = _loadfile(path)
  241. if not err then
  242. log("Creating cache for path %s", path)
  243. M.chunks.cache[modpath_mangle(path)] = { hash(path), string.dump(chunk) }
  244. M.chunks.dirty = true
  245. end
  246. cprofile(path, "load_end", "standard")
  247. return chunk, err
  248. end
  249. function M.save_cache()
  250. local function _save_cache(t)
  251. if t.dirty then
  252. log("Updating chunk cache file: %s", t.path)
  253. local f = io.open(t.path, "w+b")
  254. f:write(mpack.encode(t.cache))
  255. f:flush()
  256. t.dirty = false
  257. end
  258. end
  259. _save_cache(M.chunks)
  260. _save_cache(M.modpaths)
  261. end
  262. function M.clear_cache()
  263. local function _clear_cache(t)
  264. t.cache = {}
  265. os.remove(t.path)
  266. end
  267. _clear_cache(M.chunks)
  268. _clear_cache(M.modpaths)
  269. end
  270. local function init_cache()
  271. local function _init_cache(t)
  272. if fs_stat(t.path) then
  273. log("Loading cache file %s", t.path)
  274. local f = io.open(t.path, "rb")
  275. local ok
  276. ok, t.cache = pcall(function()
  277. return mpack.decode(f:read "*a")
  278. end)
  279. if not ok then
  280. log("Corrupted cache file, %s. Invalidating...", t.path)
  281. os.remove(t.path)
  282. t.cache = {}
  283. end
  284. t.dirty = not ok
  285. end
  286. end
  287. _init_cache(M.chunks)
  288. _init_cache(M.modpaths)
  289. end
  290. local function setup()
  291. init_cache()
  292. -- Override default functions
  293. vim._load_package = load_package
  294. vim.api.nvim__get_runtime = get_runtime_cached
  295. -- luacheck: ignore 121
  296. loadfile = loadfile_cached
  297. vim.cmd [[
  298. augroup impatient
  299. autocmd VimEnter,VimLeave * lua _G.__luacache.save_cache()
  300. augroup END
  301. command! LuaCacheClear lua _G.__luacache.clear_cache()
  302. command! LuaCacheLog lua _G.__luacache.print_log()
  303. ]]
  304. end
  305. setup()
  306. return M