├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .stylua.toml ├── Readme.md ├── TODO.md ├── build.zig ├── build.zig.zon ├── ftplugin └── zig.lua ├── lua └── zig-lamp │ ├── cmd.lua │ ├── config.lua │ ├── ffi.lua │ ├── health.lua │ ├── module │ ├── init.lua │ ├── pkg.lua │ ├── zig.lua │ └── zls.lua │ └── util.lua ├── plugin └── zig-lamp.lua └── src ├── fmtzon.zig ├── zig-lamp.zig └── zon2json.zig /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | paths: 8 | - "**.zig" 9 | - "**.zig.zon" 10 | - "**.yml" 11 | schedule: 12 | - cron: "0 2 * * *" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | version: [0.14.0, master] 21 | fail-fast: false 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Setup neovim nightly 25 | uses: MunifTanjim/setup-neovim-action@v1 26 | - name: Setup Zig 27 | uses: goto-bus-stop/setup-zig@v2 28 | with: 29 | version: ${{ matrix.version }} 30 | - uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | - name: Build with Zig 34 | run: zig build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 80 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 4 5 | quote_style = "AutoPreferDouble" 6 | call_parentheses = "Always" 7 | collapse_simple_statement = "Never" 8 | 9 | [sort_requires] 10 | enabled = true 11 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # zig-lamp 2 | 3 | This is a plugin for neovim and a library for zig. 4 | 5 | For neovim, you can install zls easily through this plugin. 6 | 7 | For zig, you can use this plugin to parse zig build dependency from `build.zig.zon`. 8 | 9 | ## Install(neovim) 10 | 11 | > for `0.13.0` and previous version, use `0.0.1`! 12 | 13 | For neovim user, please use neovim `0.10` and zig `0.14.0`! 14 | 15 | this plugin's dependency is [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) and [lspconfig](https://github.com/neovim/nvim-lspconfig)! 16 | 17 | If you are using `lazy.nvim`, just add this to your configuration file: 18 | 19 | ```lua 20 | -- no need to call any setup 21 | { 22 | "jinzhongjia/zig-lamp", 23 | event = "VeryLazy", 24 | build = ":ZigLamp build sync", 25 | -- or ":ZigLamp build" for async build, the build job will return immediately 26 | -- or ":ZigLamp build sync 20000" for sync build with specified timeout 20000ms 27 | dependencies = { 28 | "neovim/nvim-lspconfig", 29 | "nvim-lua/plenary.nvim", 30 | }, 31 | -- Here is default config, in general you no need to set these options 32 | init = function() 33 | -- if set this Non-negative value, zig-lamp will automatically install zls when open zig file. 34 | vim.g.zig_lamp_zls_auto_install = nil 35 | -- if set this Non-negative value, zig-lamp will fallback system zls when not found downloaded zls. 36 | vim.g.zig_lamp_fall_back_sys_zls = nil 37 | -- this is setting for zls with lspconfig, the opts you need to see document of zls and lspconfig. 38 | vim.g.zig_lamp_zls_lsp_opt = {} 39 | vim.g.zig_lamp_pkg_help_fg = "#CF5C00" 40 | vim.g.zig_lamp_zig_fetch_timeout = 5000 41 | end, 42 | } 43 | ``` 44 | 45 | **Do not set zls through lspconfig, `zig-lamp` will do this!** 46 | 47 | for windows user: you need `curl` and `unzip` 48 | 49 | for unix-like user: you need `curl` and `tar` 50 | 51 | ### Notice 52 | 53 | Since external libraries are introduced, if the zig compiled libraries appear panic or do not conform to the c API, then neovim will crash. Please open the issue report 54 | 55 | ## Install(zig) 56 | 57 | 1. Add to `build.zig.zon` 58 | 59 | ```sh 60 | # It is recommended to replace the following branch with commit id 61 | zig fetch --save https://github.com/jinzhongjia/zig-lamp/archive/main.tar.gz 62 | # Of course, you can also use git+https to fetch this package! 63 | ``` 64 | 65 | 2. Config to `build.zig` 66 | 67 | ```zig 68 | // To standardize development, maybe you should use `lazyDependency()` instead of `dependency()` 69 | // more info to see: https://ziglang.org/download/0.12.0/release-notes.html#toc-Lazy-Dependencies 70 | const zig_lamp = b.dependency("zig-lamp", .{ 71 | .target = target, 72 | .optimize = optimize, 73 | }); 74 | 75 | // add module 76 | exe.root_module.addImport("zigLamp", zig_lamp.module("zigLamp")); 77 | ``` 78 | 79 | ## Command 80 | 81 | - `ZigLamp info`: display infos 82 | - `ZigLamp zls install`: automatically install zls matching the current system zig version 83 | - `ZigLamp zls uninstall`: uninstall the specified zls 84 | - `ZigLamp build`: you can add param `sync` + timeout(ms optional) or `async` to select build mode 85 | - `ZigLamp pkg`: package manager panel 86 | 87 | ## ScreenShot 88 | 89 | ![pkg_panel](https://github.com/user-attachments/assets/01324e66-5912-4532-beeb-ac82c3ca84d0) 90 | ![info](https://github.com/user-attachments/assets/c5c988b5-d0b4-453e-8967-2b00b2bd3a11) 91 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - zig fmt error parse 2 | - info panel highlight 3 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const lib = b.addSharedLibrary(.{ 8 | .name = "zig-lamp", 9 | .root_source_file = b.path("src/zig-lamp.zig"), 10 | .target = target, 11 | .optimize = .ReleaseFast, 12 | }); 13 | 14 | b.installArtifact(lib); 15 | 16 | _ = b.addModule("zigLamp", .{ 17 | .root_source_file = b.path("src/zig-lamp.zig"), 18 | .target = target, 19 | .optimize = optimize, 20 | }); 21 | 22 | const lib_unit_tests = b.addTest(.{ 23 | .root_source_file = b.path("src/zig-lamp.zig"), 24 | .target = target, 25 | .optimize = optimize, 26 | }); 27 | 28 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 29 | 30 | const test_step = b.step("test", "Run unit tests"); 31 | test_step.dependOn(&run_lib_unit_tests.step); 32 | } 33 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zig_lamp, 3 | .version = "0.0.3", 4 | .fingerprint = 0x519db185c03fa148, 5 | .minimum_zig_version = "0.14.0", 6 | .dependencies = .{}, 7 | .paths = .{ 8 | "build.zig", 9 | "build.zig.zon", 10 | "src", 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /ftplugin/zig.lua: -------------------------------------------------------------------------------- 1 | -- this file will run when enter zig.lua 2 | 3 | local util = require("zig-lamp.util") 4 | local zls = require("zig-lamp.module.zls") 5 | 6 | -- config zls for lsp 7 | do 8 | if not zls.lsp_if_inited() then 9 | local zls_version = zls.get_zls_version() 10 | local zls_path = zls.get_zls_path(zls_version) 11 | 12 | local is_there_zls = zls_path and zls_version 13 | 14 | if (not is_there_zls) and not vim.g.zig_lamp_fall_back_sys_zls then 15 | if vim.g.zig_lamp_zls_auto_install then 16 | zls.zls_install({}) 17 | return 18 | end 19 | -- stylua: ignore 20 | util.Warn("Not found valid zls, please run \"ZigLamp zls install\" to install it.") 21 | return 22 | end 23 | 24 | ---@diagnostic disable-next-line: param-type-mismatch 25 | zls.setup_lspconfig(zls_version) 26 | end 27 | if zls.get_current_lsp_zls_version() or zls.if_using_sys_zls() then 28 | zls.launch_zls() 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lua/zig-lamp/cmd.lua: -------------------------------------------------------------------------------- 1 | -- this file for zig-lamp command support 2 | 3 | local util = require("zig-lamp.util") 4 | local api, fn = vim.api, vim.fn 5 | local M = {} 6 | 7 | --- @type subCmd 8 | local command = { 9 | cmd = "ZigLamp", 10 | cb = nil, 11 | sub = {}, 12 | complete = nil, 13 | } 14 | 15 | --- @alias cmdCb fun(param:string[])|nil 16 | --- @alias cmdComplete (fun():string[])|string[]|nil 17 | 18 | --- @class subCmd 19 | --- @field cmd string|nil 20 | --- @field cb cmdCb|nil 21 | --- @field sub subCmd[]|nil 22 | --- @field complete cmdComplete|nil 23 | 24 | --- @param cmd string|nil 25 | --- @param cb cmdCb|nil 26 | --- @param complete cmdComplete|nil 27 | --- @return subCmd 28 | local function create_subCmd(cmd, cb, complete) 29 | return { cmd = cmd, cb = cb, sub = {}, complete = complete } 30 | end 31 | 32 | --- @param cmds string[] note: this function will modify cmds 33 | --- @return subCmd|nil 34 | --- @return number -2 is no cmds, -1 is cmds not exist, 0 is just ok, >0 is too long 35 | local function get_command(cmds) 36 | if #cmds == 0 then 37 | return command, -2 38 | end 39 | 40 | local current_cmd = command 41 | local depth = 0 42 | 43 | for i, cmd_name in ipairs(cmds) do 44 | local found = false 45 | for _, sub_cmd in pairs(current_cmd.sub) do 46 | if sub_cmd.cmd == cmd_name then 47 | if i == #cmds then 48 | return sub_cmd, 0 49 | end 50 | if vim.tbl_isempty(sub_cmd.sub) then 51 | return sub_cmd, depth + 1 52 | end 53 | current_cmd = sub_cmd 54 | depth = depth + 1 55 | found = true 56 | break 57 | end 58 | end 59 | if not found then 60 | return nil, -1 61 | end 62 | end 63 | 64 | return current_cmd, 0 65 | end 66 | 67 | --- @param cmd subCmd 68 | --- @return string[] 69 | local function get_cmd_after_keys(cmd) 70 | local result = {} 71 | 72 | -- Add sub commands 73 | for _, sub_cmd in pairs(cmd.sub) do 74 | if sub_cmd.cmd then 75 | table.insert(result, sub_cmd.cmd) 76 | end 77 | end 78 | 79 | -- Add completions 80 | local completions = cmd.complete 81 | if type(completions) == "function" then 82 | completions = completions() 83 | end 84 | 85 | if type(completions) == "table" then 86 | for _, completion in pairs(completions) do 87 | table.insert(result, completion) 88 | end 89 | end 90 | 91 | return result 92 | end 93 | 94 | --- @param cb cmdCb 95 | --- @param complete cmdComplete 96 | --- @param ...string 97 | function M.set_command(cb, complete, ...) 98 | local cmds = { ... } 99 | local current_sub = command.sub 100 | 101 | for i, cmd_name in pairs(cmds) do 102 | local found_cmd = nil 103 | 104 | -- Find existing command 105 | for _, sub_cmd in pairs(current_sub) do 106 | if sub_cmd.cmd == cmd_name then 107 | found_cmd = sub_cmd 108 | break 109 | end 110 | end 111 | 112 | -- Create or update command 113 | if found_cmd then 114 | if i == #cmds then 115 | found_cmd.cb = cb 116 | found_cmd.complete = complete 117 | end 118 | current_sub = found_cmd.sub 119 | else 120 | local new_cmd = create_subCmd( 121 | cmd_name, 122 | i == #cmds and cb or nil, 123 | i == #cmds and complete or nil 124 | ) 125 | table.insert(current_sub, new_cmd) 126 | current_sub = new_cmd.sub 127 | end 128 | end 129 | end 130 | 131 | --- @param cmds string[] 132 | function M.delete_command(cmds) 133 | local current_sub = command.sub 134 | 135 | for i, cmd_name in pairs(cmds) do 136 | for j, sub_cmd in pairs(current_sub) do 137 | if sub_cmd.cmd == cmd_name then 138 | if i == #cmds then 139 | table.remove(current_sub, j) 140 | return 141 | end 142 | current_sub = sub_cmd.sub 143 | break 144 | end 145 | end 146 | end 147 | end 148 | 149 | local function complete_command(_, cmdline, _) 150 | local args = fn.split(cmdline) 151 | table.remove(args, 1) -- Remove command name 152 | 153 | local sub_cmd, meta_result = get_command(vim.deepcopy(args)) 154 | if meta_result == -1 or meta_result > 1 or not sub_cmd then 155 | return {} 156 | end 157 | 158 | local candidates = get_cmd_after_keys(sub_cmd) 159 | if #args == 0 then 160 | return candidates 161 | end 162 | 163 | local last_arg = args[#args] 164 | local filtered = {} 165 | 166 | for _, candidate in ipairs(candidates) do 167 | if candidate:find("^" .. vim.pesc(last_arg)) then 168 | table.insert(filtered, candidate) 169 | end 170 | end 171 | 172 | return #filtered > 0 and filtered or candidates 173 | end 174 | 175 | --- @param info commandCallback 176 | local function handle_command(info) 177 | local args_copy = vim.deepcopy(info.fargs) 178 | local sub_cmd, meta_result = get_command(args_copy) 179 | 180 | if meta_result == -1 then 181 | util.Info("not exist function") 182 | return 183 | end 184 | 185 | -- Calculate how many arguments to remove 186 | local args_to_remove = math.max(0, meta_result) 187 | local params = vim.list_slice(info.fargs, args_to_remove + 1) 188 | 189 | if sub_cmd and sub_cmd.cb then 190 | sub_cmd.cb(params) 191 | end 192 | end 193 | 194 | --- @class commandCallback 195 | --- @field name string 196 | --- @field args string 197 | --- @field fargs string[] 198 | --- @field bang boolean 199 | --- @field line1 number 200 | --- @field line2 number 201 | --- @field range number 202 | --- @field count number 203 | --- @field reg string 204 | --- @field mods string 205 | --- @field smods table 206 | 207 | -- setup command, must be called by zig-lamp/init.lua 208 | function M.init_command() 209 | api.nvim_create_user_command(command.cmd, handle_command, { 210 | range = true, 211 | nargs = "*", 212 | desc = "Command for zig-lamp", 213 | complete = complete_command, 214 | }) 215 | end 216 | 217 | return M 218 | -------------------------------------------------------------------------------- /lua/zig-lamp/config.lua: -------------------------------------------------------------------------------- 1 | local fs, fn = vim.fs, vim.fn 2 | 3 | local M = {} 4 | 5 | ---@diagnostic disable-next-line: param-type-mismatch 6 | M.data_path = fs.normalize(fs.joinpath(fn.stdpath("data"), "zig-lamp")) 7 | 8 | M.tmp_path = fs.normalize(fs.joinpath(M.data_path, "tmp")) 9 | 10 | M.version = "0.0.1" 11 | 12 | return M 13 | -------------------------------------------------------------------------------- /lua/zig-lamp/ffi.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local path = require("plenary.path") 3 | local util = require("zig-lamp.util") 4 | 5 | local M = {} 6 | 7 | ffi.cdef([[ 8 | bool check_shasum(const char* file_path, const char* shasum); 9 | const char* get_build_zon_info(const char* file_path); 10 | void free_build_zon_info(); 11 | const char* fmt_zon(const char* source_code); 12 | void free_fmt_zon(); 13 | ]]) 14 | 15 | -- stylua: ignore 16 | local plugin_path = vim.fs.normalize(string.sub(debug.getinfo(1).source, 2, #"/ffi.lua" * -1) .. "../../") 17 | 18 | local library_path = vim.fs.normalize((function() 19 | if package.config:sub(1, 1) == "\\" then 20 | return vim.fs.joinpath(plugin_path, "zig-out/bin/zig-lamp.dll") 21 | else 22 | if vim.fn.has("macunix") == 1 then 23 | return vim.fs.joinpath(plugin_path, "zig-out/lib/libzig-lamp.dylib") 24 | end 25 | return vim.fs.joinpath(plugin_path, "zig-out/lib/libzig-lamp.so") 26 | end 27 | end)()) 28 | 29 | --- @type ffi.namespace*|nil|true 30 | local _zig_lamp = nil 31 | 32 | --- @return ffi.namespace*|nil 33 | function M.get_lamp() 34 | -- when true, zig_lamp is not found 35 | if _zig_lamp == true then 36 | return nil 37 | end 38 | if _zig_lamp then 39 | return _zig_lamp 40 | end 41 | local _p = path:new(library_path) 42 | if _p:exists() then 43 | _zig_lamp = ffi.load(library_path) 44 | return _zig_lamp 45 | end 46 | _zig_lamp = true 47 | return nil 48 | end 49 | 50 | -- whether zig-lamp is loaded 51 | --- @return boolean 52 | function M.is_loaded() 53 | if _zig_lamp == nil or _zig_lamp == true then 54 | return false 55 | end 56 | return true 57 | end 58 | 59 | -- if zig-lamp is true, load it 60 | function M.lazy_load() 61 | if _zig_lamp ~= true then 62 | return 63 | end 64 | local _p = path:new(library_path) 65 | if _p:exists() then 66 | _zig_lamp = ffi.load(library_path) 67 | end 68 | end 69 | 70 | --- @return string 71 | function M.get_plugin_path() 72 | return plugin_path 73 | end 74 | 75 | -- check sha256 digest 76 | --- @param file_path string 77 | --- @param shasum string 78 | --- @return boolean 79 | function M.check_shasum(file_path, shasum) 80 | local zig_lamp = M.get_lamp() 81 | if not zig_lamp then 82 | util.Info("not found zig dynamic library, skip shasum check") 83 | return true 84 | end 85 | util.Info("try to check shasum") 86 | return zig_lamp.check_shasum(file_path, shasum) 87 | end 88 | 89 | --- @class ZigDependency 90 | --- @field url? string 91 | --- @field hash? string 92 | --- @field path? string 93 | --- @field lazy? boolean 94 | --- 95 | --- @class ZigBuildZon 96 | --- @field name string 97 | --- @field version string 98 | --- @field fingerprint string 99 | --- @field minimum_zig_version string|nil 100 | --- @field dependencies { [string] : ZigDependency } 101 | --- @field paths string[] 102 | 103 | -- get build.zig.zon info 104 | -- this will parse as a table 105 | --- @param file_path string 106 | --- @return ZigBuildZon|nil 107 | function M.get_build_zon_info(file_path) 108 | local zig_lamp = M.get_lamp() 109 | -- stylua: ignore 110 | if not zig_lamp then return nil end 111 | 112 | local _p = path:new(file_path) 113 | -- stylua: ignore 114 | if not _p:exists() then return nil end 115 | 116 | local res = ffi.string(zig_lamp.get_build_zon_info(file_path)) 117 | if res == "" then 118 | return nil 119 | end 120 | zig_lamp.free_build_zon_info() 121 | local _tmp = vim.fn.json_decode(res) 122 | return _tmp 123 | end 124 | 125 | --- @deprecated not use this 126 | function M.free_build_zon_info() 127 | local zig_lamp = M.get_lamp() 128 | -- stylua: ignore 129 | if not zig_lamp then return end 130 | zig_lamp.free_build_zon_info() 131 | end 132 | 133 | -- format zon code 134 | --- @param source_code string 135 | --- @return string|nil 136 | function M.fmt_zon(source_code) 137 | local zig_lamp = M.get_lamp() 138 | -- stylua: ignore 139 | if not zig_lamp then return nil end 140 | local res = ffi.string(zig_lamp.fmt_zon(source_code)) 141 | if res == "" then 142 | return nil 143 | end 144 | zig_lamp.free_fmt_zon() 145 | return res 146 | end 147 | 148 | --- @deprecated not use this 149 | function M.free_fmt_zon() 150 | local zig_lamp = M.get_lamp() 151 | -- stylua: ignore 152 | if not zig_lamp then return end 153 | zig_lamp.free_fmt_zon() 154 | end 155 | 156 | return M 157 | -------------------------------------------------------------------------------- /lua/zig-lamp/health.lua: -------------------------------------------------------------------------------- 1 | local health = vim.health 2 | local M = {} 3 | 4 | local function check_lspconfig() 5 | health.start("check lspconfig") 6 | local status, _ = pcall(require, "lspconfig") 7 | if status then 8 | health.ok("found lspconfig") 9 | else 10 | health.error("not found lspconfig") 11 | end 12 | end 13 | 14 | local function check_zig() 15 | health.start("check zig") 16 | if vim.fn.executable("zig") == 1 then 17 | health.ok("found zig") 18 | else 19 | health.error("not found zig") 20 | end 21 | end 22 | 23 | local function check_curl() 24 | health.start("check curl") 25 | if vim.fn.executable("curl") == 1 then 26 | health.ok("found curl") 27 | else 28 | health.error("not found curl") 29 | end 30 | end 31 | 32 | local function check_tar() 33 | local util = require("zig-lamp.util") 34 | if util.sys ~= "windows" then 35 | health.start("check tar") 36 | if vim.fn.executable("tar") == 1 then 37 | health.ok("found tar") 38 | else 39 | health.error("not found tar") 40 | end 41 | end 42 | end 43 | 44 | local function check_unzip() 45 | local util = require("zig-lamp.util") 46 | if util.sys == "windows" then 47 | health.start("check unzip") 48 | if vim.fn.executable("unzip") == 1 then 49 | health.ok("found unzip") 50 | else 51 | health.error("not found unzip") 52 | end 53 | end 54 | end 55 | 56 | local function check_lib() 57 | health.start("check dynamic library") 58 | local zig_ffi = require("zig-lamp.ffi") 59 | if zig_ffi.get_lamp() then 60 | health.ok("found lib") 61 | else 62 | health.error( 63 | 'not found lib, you can use command ":ZigLamp build" to build library' 64 | ) 65 | end 66 | end 67 | 68 | M.check = function() 69 | check_zig() 70 | check_curl() 71 | check_unzip() 72 | check_tar() 73 | check_lspconfig() 74 | check_lib() 75 | end 76 | 77 | return M 78 | -------------------------------------------------------------------------------- /lua/zig-lamp/module/init.lua: -------------------------------------------------------------------------------- 1 | -- this file is just to store module infos 2 | 3 | local cmd = require("zig-lamp.cmd") 4 | local config = require("zig-lamp.config") 5 | local job = require("plenary.job") 6 | local util = require("zig-lamp.util") 7 | local zig = require("zig-lamp.module.zig") 8 | local zls = require("zig-lamp.module.zls") 9 | 10 | local M = {} 11 | 12 | -- Setup syntax highlighting for ZigLamp_info filetype 13 | local function setup_syntax_highlighting() 14 | vim.api.nvim_create_autocmd("FileType", { 15 | pattern = "ZigLamp_info", 16 | callback = function() 17 | local buf = vim.api.nvim_get_current_buf() 18 | -- Define syntax highlighting 19 | vim.cmd([[ 20 | syntax match ZigLampTitle "^Zig Lamp$" 21 | syntax match ZigLampSection "^Zig info:$\|^ZLS info:$" 22 | syntax match ZigLampKey "^\s\+version:\|^\s\+data path:\|^\s\+system version:\|^\s\+lsp using version:\|^\s\+local versions:$" 23 | syntax match ZigLampValue ":\s*\zs.*$" 24 | syntax match ZigLampListItem "^\s\+- .*$" 25 | 26 | highlight default link ZigLampTitle Title 27 | highlight default link ZigLampSection Statement 28 | highlight default link ZigLampKey Identifier 29 | highlight default link ZigLampValue String 30 | highlight default link ZigLampListItem Special 31 | ]]) 32 | end, 33 | }) 34 | end 35 | 36 | local function create_info_content() 37 | local zig_version = zig.version() 38 | local sys_zls_version = zls.sys_version() 39 | local list = zls.local_zls_lists() 40 | local current_lsp_zls = zls.get_current_lsp_zls_version() 41 | 42 | local content = { 43 | "Zig Lamp", 44 | " version: " .. config.version, 45 | " data path: " .. config.data_path, 46 | "", 47 | "Zig info:", 48 | " version: " .. (zig_version or "not found"), 49 | "", 50 | } 51 | 52 | -- Add ZLS info if any ZLS version exists 53 | if sys_zls_version or current_lsp_zls or #list > 0 then 54 | table.insert(content, "ZLS info:") 55 | 56 | if sys_zls_version then 57 | table.insert(content, " system version: " .. sys_zls_version) 58 | end 59 | 60 | -- Determine LSP version being used 61 | if current_lsp_zls then 62 | table.insert(content, " lsp using version: " .. current_lsp_zls) 63 | elseif zls.if_using_sys_zls() and sys_zls_version then 64 | table.insert( 65 | content, 66 | " lsp using version: sys " .. sys_zls_version 67 | ) 68 | end 69 | 70 | -- Add local versions 71 | if #list > 0 then 72 | table.insert(content, " local versions:") 73 | for _, val in pairs(list) do 74 | table.insert(content, " - " .. val) 75 | end 76 | end 77 | end 78 | 79 | return content 80 | end 81 | 82 | local function cb_info() 83 | local new_buf = vim.api.nvim_create_buf(false, true) 84 | local content = create_info_content() 85 | 86 | vim.api.nvim_buf_set_lines(new_buf, 0, -1, true, content) 87 | vim.api.nvim_buf_set_option(new_buf, "filetype", "ZigLamp_info") 88 | vim.api.nvim_buf_set_option(new_buf, "bufhidden", "delete") 89 | vim.api.nvim_buf_set_option(new_buf, "modifiable", false) 90 | 91 | local win = vim.api.nvim_open_win(new_buf, true, { 92 | split = "right", 93 | style = "minimal", 94 | width = 50, 95 | }) 96 | 97 | vim.keymap.set("n", "q", function() 98 | vim.api.nvim_win_close(win, true) 99 | end, { 100 | buffer = new_buf, 101 | desc = "quit for ZigLamp info panel", 102 | }) 103 | end 104 | 105 | local function build_job(args, callback_success, callback_failure) 106 | return job:new({ 107 | cwd = require("zig-lamp.ffi").get_plugin_path(), 108 | command = "zig", 109 | args = args, 110 | on_exit = function(j, code, signal) 111 | if code == 0 then 112 | callback_success() 113 | else 114 | callback_failure(code, signal) 115 | end 116 | end, 117 | }) 118 | end 119 | 120 | local function cb_build(params) 121 | local zig_ffi = require("zig-lamp.ffi") 122 | 123 | if zig_ffi.is_loaded() then 124 | util.Warn( 125 | "sorry, lamp lib has been loaded, could not update it! please restart neovim and run command again!" 126 | ) 127 | return 128 | end 129 | 130 | local build_args = { "build", "-Doptimize=ReleaseFast" } 131 | local mode = params[1] or "async" 132 | 133 | if mode == "async" then 134 | local _j = build_job( 135 | build_args, 136 | vim.schedule_wrap(function() 137 | util.Info("build lamp lib success!") 138 | zig_ffi.lazy_load() 139 | end), 140 | vim.schedule_wrap(function(code, signal) 141 | util.Error( 142 | string.format( 143 | "build lamp lib failed, code is %d, signal is %d", 144 | code, 145 | signal 146 | ) 147 | ) 148 | end) 149 | ) 150 | _j:start() 151 | elseif mode == "sync" then 152 | local _j = build_job( 153 | build_args, 154 | vim.schedule_wrap(function() 155 | util.Info("build lamp lib success!") 156 | end), 157 | vim.schedule_wrap(function(code, _) 158 | util.Error("build lamp lib failed, code is " .. code) 159 | end) 160 | ) 161 | 162 | local wait_time = params[2] 163 | and tonumber(params[2]) 164 | and math.floor(tonumber(params[2])) 165 | or 15000 166 | _j:sync(wait_time) 167 | else 168 | util.Warn("error param: " .. mode) 169 | end 170 | end 171 | 172 | function M.setup() 173 | setup_syntax_highlighting() 174 | cmd.set_command(cb_info, nil, "info") 175 | cmd.set_command(cb_build, { "async", "sync" }, "build") 176 | end 177 | 178 | return M 179 | -------------------------------------------------------------------------------- /lua/zig-lamp/module/pkg.lua: -------------------------------------------------------------------------------- 1 | local cmd = require("zig-lamp.cmd") 2 | local job = require("plenary.job") 3 | local util = require("zig-lamp.util") 4 | local zig_ffi = require("zig-lamp.ffi") 5 | local M = {} 6 | 7 | local api, fn = vim.api, vim.fn 8 | 9 | local nvim_open_win = api.nvim_open_win 10 | local nvim_set_option_value = api.nvim_set_option_value 11 | 12 | local help_namespace = vim.api.nvim_create_namespace("ZigLamp_pkg_help") 13 | local help_hl_group = "ZigLamp_pkg_help" 14 | 15 | -- find zig.build.zon file 16 | --- @param _path string? 17 | local function find_build_zon(_path) 18 | local path = require("plenary.path") 19 | if not _path then 20 | _path = fn.getcwd() 21 | end 22 | local _p = path:new(_path) 23 | -- try find build.zig.zon 24 | local _root = _p:find_upwards("build.zig.zon") 25 | return _root 26 | end 27 | 28 | --- @param _url string 29 | local function get_hash(_url) 30 | if vim.fn.executable("zig") == 0 then 31 | return nil 32 | end 33 | util.Info("start get package hash") 34 | local _handle = vim.system({ "zig", "fetch", _url }, { text = true }) 35 | local result = _handle:wait() 36 | if result.code ~= 0 then 37 | util.Error( 38 | string.format( 39 | "failed fetch: %s, code is %d, signal is %d", 40 | _url, 41 | result.code, 42 | result.signal 43 | ) 44 | ) 45 | return nil 46 | end 47 | if result.stdout then 48 | return vim.trim(result.stdout) 49 | end 50 | return nil 51 | end 52 | 53 | local function render_help_text(buffer) 54 | local content = { 55 | "Key [q] to quit", 56 | "Key [i] to add or edit", 57 | "Key [o] to switch dependency type(url or path)", 58 | "Key [r] to reload from file", 59 | "Key [d] to delete dependency or path", 60 | "Key [s] to sync changes to file", 61 | } 62 | 63 | for _index, ele in pairs(content) do 64 | api.nvim_buf_set_extmark(buffer, help_namespace, _index - 1, 0, { 65 | virt_text = { 66 | { ele .. " ", help_hl_group }, 67 | }, 68 | virt_text_pos = "right_align", 69 | }) 70 | end 71 | end 72 | 73 | -- this function only display content, and set key_cb, not do other thing!!!! 74 | --- @param ctx pkg_ctx 75 | local function render(ctx) 76 | local buffer = ctx.buffer 77 | local zon_info = ctx.zon_info 78 | 79 | local str_len = string.len 80 | local package_info_str = "  Package Info" 81 | --- @type string[], function[] 82 | local content = { package_info_str, "" } 83 | --- @type { group: string, line: integer, col_start: integer, col_end: integer }[] 84 | local highlight = { 85 | { 86 | group = "Title", 87 | line = 0, 88 | col_start = 0, 89 | col_end = str_len(package_info_str), 90 | }, 91 | } 92 | local current_lnum = 1 93 | 94 | local name_str = "  Name: " 95 | table.insert(content, name_str .. (zon_info.name or "[none]")) 96 | current_lnum = current_lnum + 1 97 | table.insert(highlight, { 98 | group = "Title", 99 | line = current_lnum, 100 | col_start = 0, 101 | col_end = str_len(name_str), 102 | }) 103 | 104 | local version_str = "  Version: " 105 | table.insert(content, version_str .. (zon_info.version or "[none]")) 106 | current_lnum = current_lnum + 1 107 | table.insert(highlight, { 108 | group = "Title", 109 | line = current_lnum, 110 | col_start = 0, 111 | col_end = str_len(version_str), 112 | }) 113 | 114 | -- for fingerprint 115 | local fingerprint_str = "  Fingerprint: " 116 | table.insert(content, fingerprint_str .. (zon_info.fingerprint or "[none]")) 117 | current_lnum = current_lnum + 1 118 | table.insert(highlight, { 119 | group = "Title", 120 | line = current_lnum, 121 | col_start = 0, 122 | col_end = str_len(fingerprint_str), 123 | }) 124 | 125 | local min_version = zon_info.minimum_zig_version or "[none]" 126 | local min_version_str = "  Minimum zig version: " 127 | table.insert(content, min_version_str .. min_version) 128 | current_lnum = current_lnum + 1 129 | table.insert(highlight, { 130 | group = "Title", 131 | line = current_lnum, 132 | col_start = 0, 133 | col_end = str_len(min_version_str), 134 | }) 135 | 136 | if zon_info.paths and #zon_info.paths == 1 and zon_info.paths[1] == "" then 137 | local paths_str = "  Paths [include all]" 138 | table.insert(content, paths_str) 139 | current_lnum = current_lnum + 1 140 | table.insert(highlight, { 141 | group = "Title", 142 | line = current_lnum, 143 | col_start = 0, 144 | col_end = str_len(paths_str), 145 | }) 146 | elseif zon_info.paths and #zon_info.paths > 0 then 147 | local paths_str = "  Paths: " 148 | table.insert(content, paths_str) 149 | current_lnum = current_lnum + 1 150 | table.insert(highlight, { 151 | group = "Title", 152 | line = current_lnum, 153 | col_start = 0, 154 | col_end = str_len(paths_str), 155 | }) 156 | if zon_info.paths and #zon_info.paths > 0 then 157 | for _, val in pairs(zon_info.paths) do 158 | current_lnum = current_lnum + 1 159 | table.insert(content, " - " .. val) 160 | end 161 | end 162 | else 163 | local paths_str = "  Paths [none]" 164 | table.insert(content, paths_str) 165 | current_lnum = current_lnum + 1 166 | table.insert(highlight, { 167 | group = "Title", 168 | line = current_lnum, 169 | col_start = 0, 170 | col_end = str_len(paths_str), 171 | }) 172 | end 173 | 174 | local deps_str = string.format( 175 | "  Dependencies [%s]: ", 176 | vim.tbl_count(zon_info.dependencies) 177 | ) 178 | table.insert(content, deps_str) 179 | current_lnum = current_lnum + 1 180 | table.insert(highlight, { 181 | group = "Title", 182 | line = current_lnum, 183 | col_start = 0, 184 | col_end = str_len(deps_str), 185 | }) 186 | local deps_is_empty = vim.tbl_isempty(zon_info.dependencies) 187 | if zon_info.dependencies and not deps_is_empty then 188 | for name, _info in pairs(zon_info.dependencies) do 189 | local name_prefix = " - " 190 | table.insert(content, name_prefix .. name) 191 | current_lnum = current_lnum + 1 192 | table.insert(highlight, { 193 | group = "Tag", 194 | line = current_lnum, 195 | col_start = str_len(name_prefix), 196 | col_end = str_len(name_prefix) + str_len(name), 197 | }) 198 | 199 | if _info.url then 200 | local url_prefix = " url: " 201 | table.insert(content, url_prefix .. _info.url) 202 | current_lnum = current_lnum + 1 203 | table.insert(highlight, { 204 | group = "Underlined", 205 | line = current_lnum, 206 | col_start = str_len(url_prefix), 207 | col_end = str_len(url_prefix) + str_len(_info.url), 208 | }) 209 | elseif _info.path then 210 | local path_prefix = " path: " 211 | table.insert(content, path_prefix .. _info.path) 212 | current_lnum = current_lnum + 1 213 | table.insert(highlight, { 214 | group = "Underlined", 215 | line = current_lnum, 216 | col_start = str_len(path_prefix), 217 | col_end = str_len(path_prefix) + str_len(_info.path), 218 | }) 219 | else 220 | local __prefix = " url or path is none" 221 | table.insert(content, __prefix) 222 | current_lnum = current_lnum + 1 223 | end 224 | 225 | if _info.url then 226 | local hash_prefix = " hash: " 227 | table.insert(content, hash_prefix .. (_info.hash or "[none]")) 228 | current_lnum = current_lnum + 1 229 | table.insert(highlight, { 230 | group = "Underlined", 231 | line = current_lnum, 232 | col_start = str_len(hash_prefix), 233 | col_end = str_len(hash_prefix) 234 | + str_len(_info.hash or "[none]"), 235 | }) 236 | end 237 | 238 | if _info.lazy == nil then 239 | local lazy_prefix = " lazy: [empty]" 240 | table.insert(content, lazy_prefix) 241 | current_lnum = current_lnum + 1 242 | else 243 | local lazy_prefix = " lazy: " 244 | table.insert(content, lazy_prefix .. tostring(_info.lazy)) 245 | current_lnum = current_lnum + 1 246 | end 247 | end 248 | end 249 | 250 | nvim_set_option_value("modifiable", true, { buf = buffer }) 251 | api.nvim_buf_set_lines(buffer, 0, -1, true, content) 252 | 253 | for _, hl in pairs(highlight) do 254 | api.nvim_buf_add_highlight( 255 | buffer, 256 | help_namespace, 257 | hl.group, 258 | hl.line, 259 | hl.col_start, 260 | hl.col_end 261 | ) 262 | end 263 | 264 | render_help_text(buffer) 265 | nvim_set_option_value("modifiable", false, { buf = buffer }) 266 | end 267 | 268 | --- @param ctx pkg_ctx 269 | local function delete_cb(ctx) 270 | -- local buffer = ctx.buffer 271 | return function() 272 | local lnum = api.nvim_win_get_cursor(0)[1] - 2 273 | if lnum < 1 then 274 | return 275 | end 276 | lnum = lnum - 4 277 | 278 | lnum = lnum - 1 279 | 280 | if 281 | ctx.zon_info.paths 282 | and #ctx.zon_info.paths > 0 283 | and ctx.zon_info.paths[1] ~= "" 284 | then 285 | for _index, _ in pairs(ctx.zon_info.paths) do 286 | if lnum - 1 == 0 then 287 | table.remove(ctx.zon_info.paths, _index) 288 | render(ctx) 289 | return 290 | end 291 | lnum = lnum - 1 292 | end 293 | end 294 | 295 | lnum = lnum - 1 296 | local deps_is_empty = vim.tbl_isempty(ctx.zon_info.dependencies) 297 | if ctx.zon_info.dependencies and not deps_is_empty then 298 | for name, _info in pairs(ctx.zon_info.dependencies) do 299 | local _len 300 | if _info.url then 301 | _len = 4 302 | else 303 | _len = 3 304 | end 305 | 306 | if lnum > 0 and lnum < _len + 1 then 307 | ctx.zon_info.dependencies[name] = nil 308 | render(ctx) 309 | return 310 | end 311 | lnum = lnum - _len 312 | end 313 | end 314 | end 315 | end 316 | 317 | --- @param ctx pkg_ctx 318 | local edit_cb = function(ctx) 319 | return function() 320 | local lnum = api.nvim_win_get_cursor(0)[1] - 2 321 | if lnum < 1 then 322 | return 323 | end 324 | -- for name 325 | if lnum - 1 == 0 then 326 | vim.ui.input({ 327 | prompt = "Enter value for name: ", 328 | default = ctx.zon_info.name, 329 | }, function(input) 330 | -- stylua: ignore 331 | if not input then return end 332 | if input == "" then 333 | util.Warn("sorry, the name of package can not be empty!") 334 | return 335 | end 336 | 337 | ctx.zon_info.name = input 338 | render(ctx) 339 | end) 340 | return 341 | end 342 | lnum = lnum - 1 343 | 344 | -- for version 345 | if lnum - 1 == 0 then 346 | vim.ui.input({ 347 | prompt = "Enter value for version: ", 348 | default = ctx.zon_info.version, 349 | }, function(input) 350 | -- stylua: ignore 351 | if not input then return end 352 | if input == "" then 353 | util.Warn("sorry, the version of package can not be empty!") 354 | return 355 | end 356 | ctx.zon_info.version = input 357 | render(ctx) 358 | end) 359 | return 360 | end 361 | lnum = lnum - 1 362 | 363 | -- for fingerprint 364 | if lnum - 1 == 0 then 365 | vim.ui.input({ 366 | prompt = "Enter value for fingerprint: ", 367 | default = ctx.zon_info.fingerprint, 368 | }, function(input) 369 | -- stylua: ignore 370 | if not input then return end 371 | if input == "" then 372 | ctx.zon_info.fingerprint = nil 373 | else 374 | ctx.zon_info.fingerprint = input 375 | end 376 | render(ctx) 377 | end) 378 | end 379 | lnum = lnum - 1 380 | 381 | -- for minimum zig version 382 | if lnum - 1 == 0 then 383 | vim.ui.input({ 384 | prompt = "Enter value for minimum zig version: ", 385 | default = ctx.zon_info.minimum_zig_version, 386 | }, function(input) 387 | -- stylua: ignore 388 | if not input then return end 389 | if input == "" then 390 | ctx.zon_info.minimum_zig_version = nil 391 | else 392 | ctx.zon_info.minimum_zig_version = input 393 | end 394 | render(ctx) 395 | end) 396 | return 397 | end 398 | lnum = lnum - 1 399 | 400 | -- for path 401 | if lnum - 1 == 0 then 402 | vim.ui.input({ 403 | prompt = "Enter value for new path: ", 404 | }, function(input) 405 | -- stylua: ignore 406 | if not input then return end 407 | if 408 | ctx.zon_info.paths 409 | and #ctx.zon_info.paths == 1 410 | and ctx.zon_info.paths[1] == "" 411 | then 412 | ctx.zon_info.paths = { input } 413 | else 414 | table.insert(ctx.zon_info.paths, input) 415 | end 416 | 417 | render(ctx) 418 | end) 419 | return 420 | end 421 | lnum = lnum - 1 422 | 423 | if 424 | ctx.zon_info.paths 425 | and #ctx.zon_info.paths > 0 426 | and ctx.zon_info.paths[1] ~= "" 427 | then 428 | for _index, val in pairs(ctx.zon_info.paths) do 429 | if lnum - 1 == 0 then 430 | vim.ui.input({ 431 | prompt = "Enter value for path: ", 432 | default = val, 433 | }, function(input) 434 | -- stylua: ignore 435 | if not input then return end 436 | ctx.zon_info.paths[_index] = input 437 | render(ctx) 438 | end) 439 | return 440 | end 441 | lnum = lnum - 1 442 | end 443 | end 444 | 445 | -- for dependencies 446 | if lnum - 1 == 0 then 447 | vim.ui.input({ 448 | prompt = "Enter value for new dependency name: ", 449 | default = "new_dep", 450 | }, function(input) 451 | -- stylua: ignore 452 | if not input then return end 453 | ctx.zon_info.dependencies[input] = {} 454 | render(ctx) 455 | end) 456 | return 457 | end 458 | lnum = lnum - 1 459 | 460 | -- for dependency 461 | local deps_is_empty = vim.tbl_isempty(ctx.zon_info.dependencies) 462 | if ctx.zon_info.dependencies and not deps_is_empty then 463 | for name, _info in pairs(ctx.zon_info.dependencies) do 464 | if lnum - 1 == 0 then 465 | vim.ui.input({ 466 | prompt = "Enter value for dependency name: ", 467 | default = name, 468 | }, function(input) 469 | -- stylua: ignore 470 | if not input then return end 471 | ctx.zon_info.dependencies[input] = _info 472 | ctx.zon_info.dependencies[name] = nil 473 | render(ctx) 474 | end) 475 | return 476 | end 477 | lnum = lnum - 1 478 | 479 | if lnum - 1 == 0 then 480 | if _info.url == nil and _info.path == nil then 481 | --- @type "url" | "path" | nil 482 | local choice 483 | local function __input() 484 | vim.ui.input({ 485 | -- stylua: ignore 486 | prompt = string.format("Enter value for dependency %s: ", choice), 487 | }, function(input) 488 | -- stylua: ignore 489 | if not input then return end 490 | if choice == "path" then 491 | ctx.zon_info.dependencies[name].path = input 492 | else 493 | ctx.zon_info.dependencies[name].url = input 494 | local _hash = get_hash(input) 495 | if _hash then 496 | ctx.zon_info.dependencies[name].hash = 497 | _hash 498 | end 499 | end 500 | 501 | render(ctx) 502 | end) 503 | end 504 | vim.ui.select({ "url", "path" }, { 505 | prompt = "Select tabs or spaces:", 506 | format_item = function(item) 507 | return "I'd like to choose " .. item 508 | end, 509 | }, function(_tmp) 510 | -- stylua: ignore 511 | if not _tmp then return end 512 | choice = _tmp 513 | __input() 514 | end) 515 | return 516 | end 517 | local is_url = _info.url ~= nil 518 | vim.ui.input({ 519 | prompt = string.format( 520 | "Enter value for dependency %s: ", 521 | is_url and "url" or "path" 522 | ), 523 | default = is_url and _info.url or _info.path, 524 | }, function(input) 525 | -- stylua: ignore 526 | if not input then return end 527 | -- when input empty string 528 | if input == "" then 529 | ctx.zon_info.dependencies[name].url = nil 530 | ctx.zon_info.dependencies[name].path = nil 531 | render(ctx) 532 | return 533 | end 534 | 535 | if is_url then 536 | ctx.zon_info.dependencies[name].url = input 537 | local _hash = get_hash(input) 538 | if _hash then 539 | ctx.zon_info.dependencies[name].hash = _hash 540 | end 541 | else 542 | ctx.zon_info.dependencies[name].path = input 543 | end 544 | render(ctx) 545 | end) 546 | return 547 | end 548 | lnum = lnum - 1 549 | 550 | if _info.url then 551 | if lnum - 1 == 0 then 552 | vim.ui.input({ 553 | prompt = "Enter value for dependency hash: ", 554 | default = _info.hash, 555 | }, function(input) 556 | -- stylua: ignore 557 | if not input then return end 558 | ctx.zon_info.dependencies[name].hash = input 559 | render(ctx) 560 | end) 561 | return 562 | end 563 | lnum = lnum - 1 564 | end 565 | 566 | if lnum - 1 == 0 then 567 | vim.ui.select({ "true", "false", "empty" }, { 568 | prompt = "choose whether to lazy:", 569 | format_item = function(item) 570 | return "I'd like to choose " .. item 571 | end, 572 | }, function(choice) 573 | -- stylua: ignore 574 | if not choice then return end 575 | if choice == "true" then 576 | ctx.zon_info.dependencies[name].lazy = true 577 | elseif choice == "false" then 578 | ctx.zon_info.dependencies[name].lazy = false 579 | elseif choice == "empty" then 580 | ctx.zon_info.dependencies[name].lazy = nil 581 | end 582 | render(ctx) 583 | end) 584 | end 585 | lnum = lnum - 1 586 | end 587 | end 588 | end 589 | end 590 | 591 | --- @param ctx pkg_ctx 592 | local function sync_cb(ctx) 593 | return function() 594 | local zon_str = util.wrap_j2zon(ctx.zon_info) 595 | local fmted_code = zig_ffi.fmt_zon(zon_str) 596 | if not fmted_code then 597 | util.warn("in sync zon step, format zon failed!") 598 | return 599 | end 600 | ctx.zon_path:write(fmted_code, "w", 438) 601 | util.Info("sync zon success!") 602 | end 603 | end 604 | 605 | --- @param ctx pkg_ctx 606 | local function reload_cb(ctx) 607 | return function() 608 | -- try parse build.zig.zon 609 | local zon_info = zig_ffi.get_build_zon_info(ctx.zon_path:absolute()) 610 | if not zon_info then 611 | util.Warn("reload failed, because parse build.zig.zon failed!") 612 | return 613 | end 614 | 615 | ctx.zon_info = zon_info 616 | render(ctx) 617 | util.Info("reload success!") 618 | end 619 | end 620 | 621 | --- @param _ pkg_ctx 622 | local function quit_cb(_) 623 | return function() 624 | api.nvim_win_close(0, true) 625 | end 626 | end 627 | 628 | --- @param ctx pkg_ctx 629 | local function switch_cb(ctx) 630 | return function() 631 | local lnum = api.nvim_win_get_cursor(0)[1] - 2 632 | if lnum < 1 then 633 | return 634 | end 635 | -- for name 636 | lnum = lnum - 1 637 | -- for version 638 | lnum = lnum - 1 639 | -- for fingerprint 640 | lnum = lnum - 1 641 | -- for minimum zig version 642 | lnum = lnum - 1 643 | 644 | -- for path title 645 | lnum = lnum - 1 646 | 647 | if 648 | ctx.zon_info.paths 649 | and #ctx.zon_info.paths > 0 650 | and ctx.zon_info.paths[1] ~= "" 651 | then 652 | for _, _ in pairs(ctx.zon_info.paths) do 653 | -- for path 654 | lnum = lnum - 1 655 | end 656 | end 657 | 658 | -- for dependencies 659 | lnum = lnum - 1 660 | 661 | -- for dependency 662 | local deps_is_empty = vim.tbl_isempty(ctx.zon_info.dependencies) 663 | if ctx.zon_info.dependencies and not deps_is_empty then 664 | for name, _info in pairs(ctx.zon_info.dependencies) do 665 | local _len 666 | local is_url = _info.url ~= nil 667 | if is_url then 668 | _len = 4 669 | else 670 | _len = 3 671 | end 672 | 673 | if lnum > 0 and lnum < _len + 1 then 674 | vim.ui.input({ 675 | prompt = string.format( 676 | "Enter value for dependency %s: ", 677 | (not is_url) and "url" or "path" 678 | ), 679 | }, function(input) 680 | -- stylua: ignore 681 | if not input then return end 682 | -- when input empty string 683 | if input == "" then 684 | ctx.zon_info.dependencies[name].url = nil 685 | ctx.zon_info.dependencies[name].path = nil 686 | render(ctx) 687 | return 688 | end 689 | 690 | if not is_url then 691 | local _hash = get_hash(input) 692 | if _hash then 693 | ctx.zon_info.dependencies[name].url = input 694 | ctx.zon_info.dependencies[name].hash = _hash 695 | ctx.zon_info.dependencies[name].path = nil 696 | end 697 | else 698 | ctx.zon_info.dependencies[name].path = input 699 | ctx.zon_info.dependencies[name].url = nil 700 | ctx.zon_info.dependencies[name].hash = nil 701 | end 702 | render(ctx) 703 | end) 704 | return 705 | end 706 | lnum = lnum - _len 707 | end 708 | end 709 | end 710 | end 711 | 712 | --- @param ctx pkg_ctx 713 | local function set_keymap(ctx) 714 | -- stylua: ignore 715 | --- @type { lhs: string, cb: fun(ctx: pkg_ctx), desc: string }[] 716 | local key_metas = { 717 | { lhs = "q", desc = "quit for ZigLamp info panel", cb = quit_cb }, 718 | { lhs = "i", desc = "edit or add for ZigLamp info panel", cb = edit_cb }, 719 | { lhs = "d", desc = "delete for ZigLamp info panel", cb = delete_cb }, 720 | { lhs = "o", desc = "switch for ZigLamp info panel", cb = switch_cb }, 721 | { lhs = "s", desc = "sync for ZigLamp info panel", cb = sync_cb }, 722 | { lhs = "r", desc = "reload for ZigLamp info panel", cb = reload_cb }, 723 | } 724 | for _, key_meta in pairs(key_metas) do 725 | api.nvim_buf_set_keymap(ctx.buffer, "n", key_meta.lhs, "", { 726 | noremap = true, 727 | nowait = true, 728 | desc = key_meta.desc, 729 | callback = key_meta.cb(ctx), 730 | }) 731 | end 732 | end 733 | 734 | --- @param buffer integer 735 | local function set_buf_option(buffer) 736 | nvim_set_option_value("filetype", "ZigLamp_info", { buf = buffer }) 737 | nvim_set_option_value("bufhidden", "delete", { buf = buffer }) 738 | nvim_set_option_value("undolevels", -1, { buf = buffer }) 739 | nvim_set_option_value("modifiable", false, { buf = buffer }) 740 | end 741 | 742 | --- @param _ string[] 743 | local function cb_pkg(_) 744 | -- try find build.zig.zon 745 | local zon_path = find_build_zon() 746 | if not zon_path then 747 | util.Warn("not found build.zig.zon") 748 | return 749 | end 750 | 751 | -- try parse build.zig.zon 752 | local zon_info = zig_ffi.get_build_zon_info(zon_path:absolute()) 753 | if not zon_info then 754 | util.Warn("parse build.zig.zon failed!") 755 | return 756 | end 757 | local new_buf = api.nvim_create_buf(false, true) 758 | 759 | --- @alias pkg_ctx { zon_info: ZigBuildZon, zon_path: table, buffer: integer } 760 | 761 | --- @type pkg_ctx 762 | local ctx = { 763 | zon_info = zon_info, 764 | zon_path = zon_path, 765 | buffer = new_buf, 766 | } 767 | 768 | -- local bak_zon_info = vim.deepcopy(zon_info) 769 | 770 | render(ctx) 771 | set_buf_option(new_buf) 772 | 773 | -- window 774 | -- stylua: ignore 775 | local _ = nvim_open_win(new_buf, true, { split = "below", style = "minimal" }) 776 | set_keymap(ctx) 777 | end 778 | 779 | function M.setup() 780 | cmd.set_command(cb_pkg, { "info" }, "pkg") 781 | -- The timing of the delay function taking effect 782 | vim.schedule(function() 783 | local hl_val = { 784 | fg = util.adjust_brightness( 785 | vim.g.zig_lamp_pkg_help_fg or "#CF5C00", 786 | 30 787 | ), 788 | italic = true, 789 | -- standout = true, 790 | -- undercurl = true, 791 | } 792 | api.nvim_set_hl(0, help_hl_group, hl_val) 793 | end) 794 | end 795 | 796 | return M 797 | -------------------------------------------------------------------------------- /lua/zig-lamp/module/zig.lua: -------------------------------------------------------------------------------- 1 | local job = require("plenary.job") 2 | 3 | local M = {} 4 | 5 | -- get zig version 6 | --- @return string|nil 7 | function M.version() 8 | --- @diagnostic disable-next-line: missing-fields 9 | local _tmp = job:new({ command = "zig", args = { "version" } }) 10 | local _result, _ = _tmp:sync() 11 | 12 | if _result and #_result > 0 then 13 | return _result[1] 14 | end 15 | return nil 16 | end 17 | 18 | function M.setup() end 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /lua/zig-lamp/module/zls.lua: -------------------------------------------------------------------------------- 1 | local cmd = require("zig-lamp.cmd") 2 | local config = require("zig-lamp.config") 3 | local curl = require("plenary.curl") 4 | local job = require("plenary.job") 5 | local path = require("plenary.path") 6 | local scan = require("plenary.scandir") 7 | local util = require("zig-lamp.util") 8 | local zig = require("zig-lamp.module.zig") 9 | local zig_ffi = require("zig-lamp.ffi") 10 | local M = {} 11 | 12 | local lsp_is_initialized = false 13 | 14 | local if_using_sys_zls = false 15 | 16 | function M.if_using_sys_zls() 17 | return if_using_sys_zls 18 | end 19 | 20 | --- @type string|nil 21 | local current_lsp_zls_version = nil 22 | 23 | --- @param zls_version string|nil 24 | function M.set_current_lsp_zls_version(zls_version) 25 | current_lsp_zls_version = zls_version 26 | end 27 | 28 | --- @return string|nil 29 | function M.get_current_lsp_zls_version() 30 | return current_lsp_zls_version 31 | end 32 | 33 | --- @return boolean 34 | function M.lsp_if_inited() 35 | return lsp_is_initialized 36 | end 37 | 38 | function M.lsp_inited() 39 | lsp_is_initialized = true 40 | end 41 | 42 | -- zls meta info url 43 | local zls_meta_url = "https://releases.zigtools.org/v1/zls/select-version" 44 | local zls_store_path = vim.fs.joinpath(config.data_path, "zls") 45 | local zls_db_path = path:new(config.data_path, "zlsdb.json") 46 | 47 | --- @type { version_map: table } 48 | local _db = nil 49 | 50 | -- get zls file name 51 | local function get_filename() 52 | if util.sys == "windows" then 53 | return "zls.exe" 54 | else 55 | return "zls" 56 | end 57 | end 58 | 59 | -- get db 60 | local function get_db() 61 | -- when db is already loaded, return it 62 | if _db then 63 | return _db 64 | end 65 | -- when db is not loaded, load it from disk 66 | if not zls_db_path:exists() then 67 | -- when db is not exist, create it 68 | util.mkdir(zls_db_path:parent():absolute()) 69 | zls_db_path:touch() 70 | _db = { 71 | version_map = {}, 72 | } 73 | else 74 | -- when db is exist, load it 75 | local content = zls_db_path:read() 76 | if content ~= "" then 77 | -- when content is not empty, decode it 78 | _db = vim.fn.json_decode(content) 79 | else 80 | -- when content is empty, create db 81 | _db = { 82 | version_map = {}, 83 | } 84 | end 85 | end 86 | return _db 87 | end 88 | 89 | -- Note: this is not load db from disk 90 | -- save db 91 | local function save_db() 92 | -- stylua: ignore 93 | if not _db then return end 94 | 95 | if not zls_db_path:exists() then 96 | util.mkdir(zls_db_path:parent():absolute()) 97 | end 98 | 99 | zls_db_path:write(vim.fn.json_encode(_db), "w", 438) 100 | end 101 | 102 | --- @param zig_version string 103 | --- @return string|nil 104 | function M.get_zls_version_from_db(zig_version) 105 | local db = get_db() 106 | return db.version_map[zig_version] 107 | end 108 | 109 | -- please call save db after using this function 110 | --- @param zig_version string 111 | --- @param zls_version string 112 | local function set_zls_version_to_db(zig_version, zls_version) 113 | local db = get_db() 114 | db.version_map[zig_version] = zls_version 115 | end 116 | 117 | -- please call save db after using this function 118 | --- @param zig_version string 119 | local function db_delete_with_zig_version(zig_version) 120 | local db = get_db() 121 | db.version_map[zig_version] = nil 122 | end 123 | 124 | -- please call save db after using this function 125 | --- @param zls_version string 126 | local function db_delete_with_zls_version(zls_version) 127 | local db = get_db() 128 | for _zig, _zls in pairs(db.version_map) do 129 | if _zls == zls_version then 130 | db.version_map[_zig] = nil 131 | end 132 | end 133 | end 134 | 135 | -- generate src and dest path for extract zls 136 | --- @param zls_version string 137 | --- @return string, string 138 | local function generate_src_and_dest(zls_version) 139 | local src_loc = vim.fs.joinpath(config.tmp_path, zls_version) 140 | local _p = path:new(zls_store_path, zls_version) 141 | -- stylua: ignore 142 | if _p:exists() then _p:rm({ recursive = true }) end 143 | util.mkdir(_p:absolute()) 144 | 145 | local dest_loc = vim.fs.normalize(_p:absolute()) 146 | return src_loc, dest_loc 147 | end 148 | 149 | -- extract zls for unix, like linux, macos 150 | --- @param zls_version string 151 | --- @param callback? fun() 152 | local function extract_zls_for_unix(zls_version, callback) 153 | local src_loc, dest_loc = generate_src_and_dest(zls_version) 154 | 155 | ---@diagnostic disable-next-line: missing-fields 156 | local _j = job:new({ 157 | command = "tar", 158 | args = { "-xvf", src_loc, "-C", dest_loc, get_filename() }, 159 | }) 160 | -- stylua: ignore 161 | _j:after_success(function() if callback then callback() end end) 162 | _j:after_failure(vim.schedule_wrap(function(_, code, signal) 163 | util.error("failed to extract zls", code, signal) 164 | end)) 165 | _j:start() 166 | end 167 | 168 | -- this function must call in main loop 169 | --- @param zls_version string 170 | --- @param callback? fun() 171 | local function extract_zls_for_win(zls_version, callback) 172 | local src_loc, dest_loc = generate_src_and_dest(zls_version) 173 | 174 | ---@diagnostic disable-next-line: missing-fields 175 | local _j = job:new({ 176 | command = "unzip", 177 | args = { "-j", src_loc, get_filename(), "-d", dest_loc }, 178 | }) 179 | -- stylua: ignore 180 | _j:after_success(function() if callback then callback() end end) 181 | _j:after_failure(vim.schedule_wrap(function(_, code, signal) 182 | util.error("failed to extract zls", code, signal) 183 | end)) 184 | _j:start() 185 | end 186 | 187 | -- verify zls version 188 | --- @param zls_version string 189 | --- @return boolean|nil 190 | local function verify_local_zls_version(zls_version) 191 | local _p = path:new(zls_store_path, zls_version, get_filename()) 192 | if not _p:exists() then 193 | return nil 194 | end 195 | ---@diagnostic disable-next-line: missing-fields 196 | local _j = job:new({ 197 | command = _p:absolute(), 198 | args = { "--version" }, 199 | }) 200 | local _result, _ = _j:sync() 201 | 202 | -- stylua: ignore 203 | if not _result then return false end 204 | -- stylua: ignore 205 | if _result[1] == zls_version then return true end 206 | 207 | return false 208 | end 209 | 210 | --- @return string|nil 211 | function M.get_zls_version() 212 | -- get zig version 213 | local zig_version = zig.version() 214 | if not zig_version then 215 | return nil 216 | end 217 | 218 | -- get zls version from db 219 | local zls_version = M.get_zls_version_from_db(zig_version) 220 | if not zls_version then 221 | return nil 222 | end 223 | 224 | return zls_version 225 | end 226 | 227 | --- @param zls_version string|nil 228 | function M.get_zls_path(zls_version) 229 | if not zls_version then 230 | return nil 231 | end 232 | -- detect zls whether exist 233 | local zls_path = path:new(zls_store_path, zls_version, get_filename()) 234 | if not zls_path:exists() then 235 | return nil 236 | end 237 | 238 | return vim.fs.normalize(zls_path:absolute()) 239 | end 240 | 241 | -- whether exist system zls 242 | function M.if_sys_zls() 243 | return vim.fn.executable("zls") == 1 244 | end 245 | 246 | -- get zls version 247 | --- @return string|nil 248 | function M.sys_version() 249 | if not M.if_sys_zls() then 250 | return nil 251 | end 252 | --- @diagnostic disable-next-line: missing-fields 253 | local _tmp = job:new({ command = "zls", args = { "--version" } }) 254 | _tmp:after_failure(vim.schedule_wrap(function(_, code, signal) 255 | util.error("failed to get sys zls version", code, signal) 256 | end)) 257 | local _result, _ = _tmp:sync() 258 | if not _result then 259 | return nil 260 | end 261 | 262 | return _result[1] 263 | end 264 | 265 | --- @class zlsMetaArchInfo 266 | --- @field tarball string 267 | --- @field shasum string 268 | --- @field size string this is litter number 269 | 270 | --- @class zlsMeta 271 | --- @field date string 272 | --- @field version string 273 | --- @field ["aarch64-linux"] zlsMetaArchInfo 274 | --- @field ["aarch64-macos"] zlsMetaArchInfo 275 | --- @field ["wasm32-wasi"] zlsMetaArchInfo 276 | --- @field ["x86-linux"] zlsMetaArchInfo 277 | --- @field ["x86-windows"] zlsMetaArchInfo 278 | --- @field ["x86_64-linux"] zlsMetaArchInfo 279 | --- @field ["x86_64-macos"] zlsMetaArchInfo 280 | --- @field ["x86_64-windows"] zlsMetaArchInfo 281 | 282 | --- @class zlsMetaErr 283 | --- @field code 0|1|2|3 284 | --- @field message string 285 | 286 | -- this function api is here: 287 | -- https://github.com/zigtools/release-worker#unsupported-release-cycle 288 | --- @param zig_version string 289 | --- @param callback fun(json:zlsMeta|zlsMetaErr|nil) 290 | local function get_meta_json(zig_version, callback) 291 | --- @param response { exit: number, status: number, headers: table, body: string } 292 | local function __tmp(response) 293 | -- stylua: ignore 294 | if response.status ~= 200 then callback() return end 295 | 296 | --- @type zlsMeta|zlsMetaErr 297 | local info = vim.fn.json_decode(response.body) 298 | if not info.code then 299 | set_zls_version_to_db(zig_version, info.version) 300 | save_db() 301 | end 302 | callback(info) 303 | end 304 | 305 | local query = { zig_version = zig_version, compatibility = "only-runtime" } 306 | 307 | -- stylua: ignore 308 | curl.get(zls_meta_url, { query = query, callback = vim.schedule_wrap(__tmp) }) 309 | end 310 | 311 | --- @param zls_version string 312 | --- @param arch_info zlsMetaArchInfo 313 | --- @param callback fun(result: boolean, ctx: { exit: number, status: number, headers: table, body: string}) 314 | local function download_zls(zls_version, arch_info, callback) 315 | -- check tmp path whether exist 316 | local _p = path:new(config.tmp_path) 317 | if not _p:exists() then 318 | util.mkdir(config.tmp_path) 319 | end 320 | 321 | local loc = vim.fs.joinpath(config.tmp_path, zls_version) 322 | local _loc = path:new(loc) 323 | if _loc:exists() then 324 | _loc:rm() 325 | end 326 | --- @param out { exit: number, status: number, headers: table, body: string} 327 | local _tmp = function(out) 328 | if out.status ~= 200 then 329 | callback(false, out) 330 | return 331 | end 332 | local is_ok = zig_ffi.check_shasum(loc, arch_info.shasum) 333 | callback(is_ok, out) 334 | end 335 | -- asynchronously download 336 | curl.get( 337 | arch_info.tarball, 338 | { output = loc, callback = vim.schedule_wrap(_tmp) } 339 | ) 340 | end 341 | 342 | -- delete specific zls version 343 | --- @param zls_version string 344 | local function remove_zls(zls_version) 345 | local _p = path:new(zls_store_path, zls_version) 346 | if _p:exists() then 347 | _p:rm({ recursive = true }) 348 | end 349 | end 350 | 351 | -- get all local zls version 352 | --- @return string[] 353 | function M.local_zls_lists() 354 | local res = {} 355 | if not path:new(zls_store_path):exists() then 356 | return res 357 | end 358 | 359 | local _s = scan.scan_dir(zls_store_path, { only_dirs = true }) 360 | for _, value in pairs(_s) do 361 | local _pp = vim.fn.fnamemodify(value, ":t") 362 | table.insert(res, _pp) 363 | end 364 | 365 | return res 366 | end 367 | 368 | --- @param meta zlsMeta 369 | --- @return zlsMetaArchInfo 370 | local function get_arch_info(meta) 371 | local _key = util.arch() .. "-" .. util.sys 372 | return meta[_key] 373 | end 374 | 375 | --- @param zls_version string 376 | local function remove_zls_tmp(zls_version) 377 | local _p = path:new(config.tmp_path, zls_version) 378 | if _p:exists() then 379 | _p:rm() 380 | end 381 | end 382 | 383 | local function lsp_on_new_config(new_config, new_root_dir) 384 | local _zls_path = "zls" 385 | 386 | if not if_using_sys_zls then 387 | local _zls_version = M.get_zls_version() 388 | --- @type string 389 | ---@diagnostic disable-next-line: assign-type-mismatch 390 | _zls_path = M.get_zls_path(_zls_version) 391 | new_config.cmd = { _zls_path } 392 | end 393 | 394 | if vim.fn.filereadable(vim.fs.joinpath(new_root_dir, "zls.json")) ~= 0 then 395 | new_config.cmd = { _zls_path, "--config-path", "zls.json" } 396 | end 397 | end 398 | 399 | --- @param zls_version string|nil 400 | function M.setup_lspconfig(zls_version) 401 | local lspconfig = require("lspconfig") 402 | 403 | -- support use user's config 404 | local lsp_opt = vim.g.zig_lamp_zls_lsp_opt or {} 405 | lsp_opt.autostart = false 406 | lsp_opt.on_new_config = lsp_on_new_config 407 | 408 | lspconfig.zls.setup(lsp_opt) 409 | M.set_current_lsp_zls_version(zls_version) 410 | if_using_sys_zls = zls_version == nil 411 | 412 | M.lsp_inited() 413 | end 414 | 415 | -- we need setup lspconfig first 416 | --- @param bufnr integer|nil 417 | function M.launch_zls(bufnr) 418 | local lspconfig_configs = require("lspconfig.configs") 419 | local zls_config = lspconfig_configs.zls 420 | 421 | zls_config.launch(bufnr) 422 | end 423 | 424 | -- callback for zls install 425 | --- @param _ string[] 426 | function M.zls_install(_) 427 | local zig_version = zig.version() 428 | if not zig_version then 429 | util.Warn("not found zig") 430 | return 431 | end 432 | util.Info("get zig version: " .. zig_version) 433 | 434 | local is_local = true 435 | 436 | local db_zls_version = M.get_zls_version_from_db(zig_version) 437 | if not db_zls_version then 438 | util.Info("not found zls version in db, try to get meta json") 439 | is_local = false 440 | goto l1 441 | end 442 | util.Info("found zls version in db: " .. db_zls_version) 443 | 444 | if not verify_local_zls_version(db_zls_version) then 445 | -- when zls version is not correct 446 | db_delete_with_zig_version(zig_version) 447 | remove_zls(db_zls_version) 448 | util.Info("zls version verify failed, try to get meta json") 449 | is_local = false 450 | goto l1 451 | end 452 | 453 | -- stylua: ignore 454 | util.Info(string.format("zls version %s is already installed", db_zls_version)) 455 | 456 | ::l1:: 457 | if is_local then 458 | return 459 | end 460 | 461 | --- @param zls_version string 462 | local function after_install(zls_version) 463 | -- when inited, return 464 | if M.lsp_if_inited() then 465 | return 466 | end 467 | 468 | M.setup_lspconfig(zls_version) 469 | local buf_lists = vim.api.nvim_list_bufs() 470 | for _, bufnr in pairs(buf_lists) do 471 | -- stylua: ignore 472 | local filetype = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) 473 | if filetype == "zig" then 474 | M.launch_zls(bufnr) 475 | end 476 | end 477 | end 478 | 479 | -- run after extract zls, check zls version 480 | --- @param info zlsMeta 481 | local function after_extract(info) 482 | return vim.schedule_wrap(function() 483 | remove_zls_tmp(info.version) 484 | if verify_local_zls_version(info.version) then 485 | util.Info("success to install zls version " .. info.version) 486 | after_install(info.version) 487 | else 488 | util.Error("failed to install zls") 489 | end 490 | end) 491 | end 492 | 493 | -- run after download zls, extract zls 494 | --- @param info zlsMeta 495 | local function after_download(info) 496 | local function _tmp() 497 | util.Info("try to extract zls") 498 | if util.sys == "windows" then 499 | extract_zls_for_win(info.version, after_extract(info)) 500 | else 501 | -- for unix, like linux, macos 502 | extract_zls_for_unix(info.version, after_extract(info)) 503 | end 504 | end 505 | 506 | --- @param result boolean 507 | --- @param ctx { exit: number, status: number, headers: table, body: string} 508 | return function(result, ctx) 509 | if result then 510 | vim.schedule(_tmp) 511 | else 512 | util.Error("failed to download zls, status: " .. ctx.status) 513 | end 514 | end 515 | end 516 | 517 | -- run aftet get meta json 518 | --- @param info zlsMeta|zlsMetaErr|nil 519 | local function after_meta(info) 520 | -- when info is nil, network error 521 | -- when info.code is 0, Unsupported 522 | -- when info.code is 1, Unsupported Release Cycle 523 | -- when info.code is 2, Incompatible development build 524 | -- when info.code is 3, Incompatible tagged release 525 | if info == nil then 526 | util.Error("failed to get zls meta json, please check your network") 527 | return 528 | elseif info.code == 0 then 529 | util.Warn("current zig version is not supported by zls") 530 | return 531 | elseif info.code == 1 then 532 | util.Warn("Unsupported Release Cycle, zls hasn't updated yet") 533 | return 534 | elseif info.code == 2 then 535 | util.Warn("Incompatible development build, non-zls compatible") 536 | return 537 | elseif info.code == 3 then 538 | util.Warn("Incompatible tagged release, non-zls compatible") 539 | return 540 | end 541 | 542 | ---@diagnostic disable-next-line: param-type-mismatch 543 | local archinfo = get_arch_info(info) 544 | 545 | util.Info("try to download zls") 546 | -- download zls 547 | ---@diagnostic disable-next-line: param-type-mismatch 548 | download_zls(info.version, archinfo, after_download(info)) 549 | end 550 | 551 | util.Info("try to get zls meta json") 552 | -- get meta json 553 | get_meta_json(zig_version, after_meta) 554 | end 555 | 556 | --- @param params string[] 557 | --- @diagnostic disable-next-line: unused-local 558 | local function cb_zls_uninstall(params) 559 | if #params == 0 then 560 | util.Info("please input zls version") 561 | return 562 | end 563 | 564 | local lists = M.local_zls_lists() 565 | if not vim.tbl_contains(lists, params[1]) then 566 | util.Info("please input correct zls version") 567 | return 568 | end 569 | 570 | local zls_version = params[1] 571 | 572 | if zls_version == M.get_current_lsp_zls_version() then 573 | local zls_clients = vim.lsp.get_clients({ name = "zls" }) 574 | if #zls_clients > 0 then 575 | -- stylua: ignore 576 | util.Warn("the zls which you want to uninstall is running, please stop it first") 577 | return 578 | end 579 | end 580 | 581 | db_delete_with_zls_version(zls_version) 582 | remove_zls(zls_version) 583 | save_db() 584 | 585 | util.Info("success to uninstall zls version " .. zls_version) 586 | if zls_version == M.get_current_lsp_zls_version() then 587 | M.set_current_lsp_zls_version(nil) 588 | end 589 | end 590 | 591 | local function complete_zls_uninstall() 592 | return M.local_zls_lists() 593 | end 594 | 595 | -- now, we have two commands 596 | -- zls install 597 | -- zls uninstall 598 | local function set_command() 599 | cmd.set_command(M.zls_install, nil, "zls", "install") 600 | -- stylua: ignore 601 | cmd.set_command(cb_zls_uninstall, complete_zls_uninstall, "zls", "uninstall") 602 | end 603 | 604 | function M.setup() 605 | set_command() 606 | end 607 | 608 | return M 609 | -------------------------------------------------------------------------------- /lua/zig-lamp/util.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- TODO: this need to test on macos 4 | local arch = vim.uv.os_uname().machine 5 | 6 | function M.arch() 7 | if arch == "arm64" then 8 | return "aarch64" 9 | end 10 | return arch 11 | end 12 | 13 | if vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 then 14 | M.sys = "windows" 15 | elseif vim.fn.has("macunix") == 1 then 16 | M.sys = "macos" 17 | elseif vim.fn.has("unix") == 1 then 18 | M.sys = "linux" 19 | end 20 | 21 | --- @param _path string 22 | function M.mkdir(_path) 23 | vim.fn.mkdir(_path, "p") 24 | end 25 | 26 | -- this is public notify message prefix 27 | local _notify_public_message = "[ZigLamp]: " 28 | 29 | -- Error notify 30 | --- @param message string 31 | function M.Error(message) 32 | -- stylua: ignore 33 | vim.api.nvim_notify(_notify_public_message .. message, vim.log.levels.ERROR, {}) 34 | end 35 | 36 | -- Info notify 37 | --- @param message string 38 | function M.Info(message) 39 | -- stylua: ignore 40 | vim.api.nvim_notify(_notify_public_message .. message, vim.log.levels.INFO, {}) 41 | end 42 | 43 | -- Warn notify 44 | --- @param message string 45 | function M.Warn(message) 46 | -- stylua: ignore 47 | vim.api.nvim_notify(_notify_public_message .. message, vim.log.levels.WARN, {}) 48 | end 49 | 50 | local function is_array(t) 51 | local i = 0 52 | for _ in pairs(t) do 53 | i = i + 1 54 | if t[i] == nil then 55 | return false 56 | end 57 | end 58 | return true 59 | end 60 | 61 | --- @param info ZigBuildZon 62 | --- @return string 63 | function M.wrap_j2zon(info) 64 | local res = "" 65 | res = res .. ".{" 66 | res = res .. ".name = ." .. (info.name or "") .. "," 67 | res = res .. ".version = " .. M.data2zon(info.version or "") .. "," 68 | res = res .. ".fingerprint = " .. info.fingerprint .. "," 69 | if info.minimum_zig_version then 70 | -- stylua: ignore 71 | res = res .. ".minimum_zig_version = " .. M.data2zon(info.minimum_zig_version or "") .. "," 72 | end 73 | -- stylua: ignore 74 | res = res .. ".dependencies = ".. M.data2zon(info.dependencies or {}) .. "," 75 | res = res .. ".paths=" .. M.data2zon(info.paths or {}) .. "," 76 | res = res .. "}" 77 | return res 78 | end 79 | 80 | -- whether the str is legal for zig 81 | --- @param str string 82 | local function str_if_legal(str) 83 | local result = string.find(str, "-") == nil 84 | local first_char = string.sub(str, 1, 1) 85 | result = result and string.match(first_char, "[%a_]") ~= nil 86 | return result 87 | end 88 | 89 | -- convert lua type to a zon string, but now we can not format the string 90 | function M.data2zon(obj) 91 | local res = "" 92 | if type(obj) == "table" then 93 | res = res .. ".{" 94 | local if_arr = is_array(obj) 95 | for key, value in pairs(obj) do 96 | if not if_arr then 97 | if str_if_legal(key) then 98 | res = res .. "." .. key .. "=" 99 | else 100 | res = res .. '.@"' .. key .. '"=' 101 | end 102 | end 103 | res = res .. M.data2zon(value) 104 | res = res .. "," 105 | end 106 | res = res .. "}" 107 | elseif type(obj) == "string" then 108 | res = string.format('"%s"', obj) 109 | elseif type(obj) == "boolean" then 110 | if obj then 111 | res = "true" 112 | else 113 | res = "false" 114 | end 115 | elseif type(obj) == "number" then 116 | res = tostring(obj) 117 | end 118 | return res 119 | end 120 | 121 | --- @param color string 122 | --- @param amount number 123 | function M.adjust_brightness(color, amount) 124 | local r = tonumber(color:sub(2, 3), 16) 125 | local g = tonumber(color:sub(4, 5), 16) 126 | local b = tonumber(color:sub(6, 7), 16) 127 | 128 | r = math.min(255, math.max(0, r + amount)) 129 | g = math.min(255, math.max(0, g + amount)) 130 | b = math.min(255, math.max(0, b + amount)) 131 | 132 | return string.format("#%02X%02X%02X", r, g, b) 133 | end 134 | return M 135 | -------------------------------------------------------------------------------- /plugin/zig-lamp.lua: -------------------------------------------------------------------------------- 1 | require("zig-lamp.cmd").init_command() 2 | require("zig-lamp.module").setup() 3 | require("zig-lamp.module.pkg").setup() 4 | require("zig-lamp.module.zig").setup() 5 | require("zig-lamp.module.zls").setup() 6 | -------------------------------------------------------------------------------- /src/fmtzon.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub var fmted_source: ?[:0]const u8 = null; 4 | 5 | pub fn fmtZon(source: [:0]const u8, allocator: std.mem.Allocator) ![:0]const u8 { 6 | var tree = try std.zig.Ast.parse(allocator, source, .zon); 7 | defer tree.deinit(allocator); 8 | 9 | var buffer = std.ArrayList(u8).init(allocator); 10 | 11 | try tree.renderToArrayList(&buffer, .{}); 12 | return buffer.toOwnedSliceSentinel(0); 13 | } 14 | -------------------------------------------------------------------------------- /src/zig-lamp.zig: -------------------------------------------------------------------------------- 1 | //! zig lamp library 2 | 3 | const std = @import("std"); 4 | const zon2json = @import("zon2json.zig"); 5 | const fmtzon = @import("fmtzon.zig"); 6 | const fs = std.fs; 7 | const Sha256 = std.crypto.hash.sha2.Sha256; 8 | 9 | // In real world, this may set to page_size, usually it's 4096. 10 | const BUF_SIZE = 4096; 11 | const empty_str = ""; 12 | 13 | pub const zig2json = zon2json.parse; 14 | 15 | pub fn sha256Digest( 16 | file: fs.File, 17 | ) ![Sha256.digest_length]u8 { 18 | var sha256 = Sha256.init(.{}); 19 | const rdr = file.reader(); 20 | 21 | var buf: [BUF_SIZE]u8 = undefined; 22 | var n = try rdr.read(&buf); 23 | while (n != 0) { 24 | sha256.update(buf[0..n]); 25 | n = try rdr.read(&buf); 26 | } 27 | 28 | return sha256.finalResult(); 29 | } 30 | 31 | pub const fmtZon = fmtzon.fmtZon; 32 | 33 | // this function for ffi call 34 | export fn check_shasum(file_path: [*c]const u8, shasum: [*c]const u8) bool { 35 | const file_path_len = std.mem.len(file_path); 36 | const shasum_len = std.mem.len(shasum); 37 | 38 | const file = fs.openFileAbsolute(file_path[0..file_path_len], .{}) catch { 39 | return false; 40 | }; 41 | defer file.close(); 42 | 43 | const digest = sha256Digest(file) catch return false; 44 | 45 | var hash: [64]u8 = std.mem.zeroes([64]u8); 46 | _ = std.fmt.bufPrint(&hash, "{s}", .{std.fmt.fmtSliceHexLower(&digest)}) catch return false; 47 | for (0..shasum_len) |i| { 48 | if (shasum[i] != hash[i]) { 49 | return false; 50 | } 51 | } 52 | return true; 53 | } 54 | 55 | const _allocator: std.mem.Allocator = std.heap.smp_allocator; 56 | var json: ?[:0]const u8 = null; 57 | 58 | export fn get_build_zon_info(file_path: [*c]const u8) [*c]const u8 { 59 | 60 | // free previous json 61 | if (json) |_json| 62 | _allocator.free(_json); 63 | 64 | // get file path length 65 | const file_path_len = std.mem.len(file_path); 66 | 67 | var file = fs.openFileAbsolute(file_path[0..file_path_len], .{ .mode = .read_only }) catch return empty_str; 68 | defer file.close(); 69 | 70 | // no need to call deinit 71 | var arr = std.ArrayList(u8).init(_allocator); 72 | 73 | zon2json.parse( 74 | _allocator, 75 | file.reader().any(), 76 | arr.writer(), 77 | void{}, 78 | .{ .file_name = file_path[0..file_path_len] }, 79 | ) catch return empty_str; 80 | 81 | json = arr.toOwnedSliceSentinel(0) catch return empty_str; 82 | 83 | if (json == null) return empty_str; 84 | 85 | return json.?; 86 | } 87 | 88 | export fn free_build_zon_info() void { 89 | if (json) |_json| { 90 | _allocator.free(_json); 91 | json = null; 92 | } 93 | } 94 | 95 | export fn fmt_zon(source_code: [*c]const u8) [*c]const u8 { 96 | if (fmtzon.fmted_source) |_tmp| 97 | _allocator.free(_tmp); 98 | 99 | const source_code_len = std.mem.len(source_code); 100 | 101 | fmtzon.fmted_source = fmtzon.fmtZon(source_code[0..source_code_len :0], _allocator) catch return empty_str; 102 | 103 | if (fmtzon.fmted_source == null) return empty_str; 104 | 105 | return fmtzon.fmted_source.?; 106 | } 107 | 108 | export fn free_fmt_zon() void { 109 | if (fmtzon.fmted_source) |_tmp| { 110 | _allocator.free(_tmp); 111 | fmtzon.fmted_source = null; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/zon2json.zig: -------------------------------------------------------------------------------- 1 | //! this file is from repo https://github.com/Cloudef/zig2nix 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | 5 | fn stringifyFieldName(allocator: std.mem.Allocator, ast: std.zig.Ast, idx: std.zig.Ast.Node.Index) !?[]const u8 { 6 | if (ast.firstToken(idx) < 2) return null; 7 | const slice = ast.tokenSlice(ast.firstToken(idx) - 2); 8 | if (slice[0] == '@') { 9 | const v = try std.zig.string_literal.parseAlloc(allocator, slice[1..]); 10 | defer allocator.free(v); 11 | return try std.json.stringifyAlloc(allocator, v, .{}); 12 | } 13 | return try std.json.stringifyAlloc(allocator, slice, .{}); 14 | } 15 | 16 | fn stringifyValue(allocator: std.mem.Allocator, ast: std.zig.Ast, idx: std.zig.Ast.Node.Index) !?[]const u8 { 17 | const index = if (comptime builtin.zig_version.minor > 14) @intFromEnum(idx) else idx; 18 | const slice = ast.tokenSlice(ast.nodes.items(.main_token)[index]); 19 | if (slice[0] == '\'') { 20 | switch (std.zig.parseCharLiteral(slice)) { 21 | .success => |v| return try std.json.stringifyAlloc(allocator, v, .{}), 22 | .failure => return error.parseCharLiteralFailed, 23 | } 24 | } else if (slice[0] == '"') { 25 | const v = try std.zig.string_literal.parseAlloc(allocator, slice); 26 | defer allocator.free(v); 27 | return try std.json.stringifyAlloc(allocator, v, .{}); 28 | } 29 | switch (std.zig.number_literal.parseNumberLiteral(slice)) { 30 | .int => |v| return try std.json.stringifyAlloc(allocator, v, .{}), 31 | .float => |v| return try std.json.stringifyAlloc(allocator, v, .{}), 32 | .big_int => |v| return try std.json.stringifyAlloc(allocator, v, .{}), 33 | .failure => {}, 34 | } 35 | if (std.mem.eql(u8, slice, "true")) { 36 | return try std.json.stringifyAlloc(allocator, true, .{}); 37 | } else if (std.mem.eql(u8, slice, "false")) { 38 | return try std.json.stringifyAlloc(allocator, false, .{}); 39 | } 40 | 41 | // literal 42 | return try std.json.stringifyAlloc(allocator, slice, .{}); 43 | } 44 | 45 | fn stringify(allocator: std.mem.Allocator, writer: anytype, ast: std.zig.Ast, idx: std.zig.Ast.Node.Index, has_name: bool) !void { 46 | if (has_name) { 47 | if (try stringifyFieldName(allocator, ast, idx)) |name| { 48 | defer allocator.free(name); 49 | try writer.print("{s}:", .{name}); 50 | if (std.mem.eql(u8, name, "\"fingerprint\"")) { 51 | const index = if (builtin.zig_version.minor > 14) @intFromEnum(idx) else idx; 52 | const slice = ast.tokenSlice(ast.nodes.items(.main_token)[index]); 53 | 54 | const v = try std.fmt.allocPrint(allocator, "\"{s}\"", .{slice}); 55 | defer allocator.free(v); 56 | 57 | try writer.writeAll(v); 58 | return; 59 | } 60 | } 61 | } 62 | 63 | var buf: [2]std.zig.Ast.Node.Index = undefined; 64 | if (ast.fullStructInit(&buf, idx)) |v| { 65 | try writer.writeAll("{"); 66 | for (v.ast.fields, 0..) |i, n| { 67 | try stringify(allocator, writer, ast, i, true); 68 | if (n + 1 != v.ast.fields.len) try writer.writeAll(","); 69 | } 70 | try writer.writeAll("}"); 71 | } else if (ast.fullArrayInit(&buf, idx)) |v| { 72 | try writer.writeAll("["); 73 | for (v.ast.elements, 0..) |i, n| { 74 | try stringify(allocator, writer, ast, i, false); 75 | if (n + 1 != v.ast.elements.len) try writer.writeAll(","); 76 | } 77 | try writer.writeAll("]"); 78 | } else if (try stringifyValue(allocator, ast, idx)) |v| { 79 | defer allocator.free(v); 80 | try writer.writeAll(v); 81 | } else { 82 | return error.UnknownType; 83 | } 84 | } 85 | 86 | pub const Options = struct { 87 | limit: usize = std.math.maxInt(usize), 88 | file_name: []const u8 = "build.zig.zon", // for errors 89 | }; 90 | 91 | pub fn parse(allocator: std.mem.Allocator, reader: std.io.AnyReader, writer: anytype, error_writer: anytype, opts: Options) !void { 92 | const zon = blk: { 93 | var tmp = try reader.readAllAlloc(allocator, opts.limit); 94 | errdefer allocator.free(tmp); 95 | tmp = try allocator.realloc(tmp, tmp.len + 1); 96 | tmp[tmp.len - 1] = 0; 97 | break :blk tmp[0 .. tmp.len - 1 :0]; 98 | }; 99 | 100 | defer allocator.free(zon); 101 | var ast = try std.zig.Ast.parse(allocator, zon, .zon); 102 | defer ast.deinit(allocator); 103 | 104 | if (ast.errors.len > 0) { 105 | if (@TypeOf(error_writer) != void) { 106 | for (ast.errors) |e| { 107 | const loc = ast.tokenLocation(ast.errorOffset(e), e.token); 108 | try error_writer.print("error: {s}:{}:{}: ", .{ opts.file_name, loc.line, loc.column }); 109 | try ast.renderError(e, error_writer); 110 | try error_writer.writeAll("\n"); 111 | } 112 | } 113 | return error.ParseFailed; 114 | } 115 | 116 | try stringify(allocator, writer, ast, if (builtin.zig_version.minor > 14) ast.nodes.items(.data)[0].node else ast.nodes.items(.data)[0].lhs, false); 117 | } 118 | --------------------------------------------------------------------------------