├── LICENSE ├── lua └── zigclient │ ├── bundle.lua │ └── init.lua ├── scratch.lua └── test_plugin.lua /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023: bfredl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lua/zigclient/bundle.lua: -------------------------------------------------------------------------------- 1 | local h = _G._mod_zigclient_bundle or {} 2 | _G._mod_zigclient_bundle = h 3 | 4 | function h.process_srcloc(s) 5 | return { 6 | col = s.col; 7 | line = s.line; 8 | col_start = s.col - (s.span_main - s.span_start); 9 | col_end = s.col + (s.span_end - s.span_main); 10 | src_path = s.src_path; 11 | } 12 | end 13 | 14 | function h.process_bundle(bundle) 15 | msgs = {} 16 | for _,msg in ipairs(bundle) do 17 | local main = h.process_srcloc(msg.src_loc) 18 | main.msg = msg.msg 19 | local notes = {} 20 | for _,note in ipairs(msg.notes) do 21 | local n = h.process_srcloc(note.src_loc) 22 | n.msg = note.msg 23 | table.insert(notes, n); 24 | end 25 | local reftrace = {} 26 | for _,pos in ipairs(msg.src_loc.ref_trace) do 27 | local p = h.process_srcloc(pos.src_loc) 28 | p.decl_name = pos.decl_name 29 | table.insert(reftrace, p); 30 | end 31 | table.insert(msgs, {main=main,notes=notes,reftrace=reftrace}) 32 | end 33 | return msgs 34 | end 35 | 36 | function h.is_in_base(msg_item, base_path) 37 | if not vim.startswith(msg_item.src_path, '/') then 38 | return true -- DUBBEL bULL 39 | end 40 | return vim.startswith(msg_item.src_path, base_path) -- BULL 41 | end 42 | 43 | function h.item_to_diag(item, kind, main) 44 | local diag = { 45 | bufnr = vim.fn.bufadd(item.src_path); 46 | lnum = item.line; 47 | col = item.col_start; 48 | end_col = item.col_end; 49 | -- TODO: main col lol 50 | message = item.msg; 51 | } 52 | if kind == "error" then 53 | diag.severity = vim.diagnostic.severity.ERROR; 54 | elseif kind == "error_base" then 55 | diag.message = "HABLA: "..main.msg 56 | diag.severity = vim.diagnostic.severity.ERROR; 57 | elseif kind == "ref" then 58 | diag.message = "Referenced here" 59 | diag.severity = vim.diagnostic.severity.INFO; 60 | -- TODO: do something so we can jump there 61 | -- diag.user_data = main 62 | else 63 | diag.severity = vim.diagnostic.severity.INFO; 64 | end 65 | return diag 66 | end 67 | 68 | function h.msg_to_diag(msg, base_path, diags) 69 | diags = diags or {} 70 | local base_loc 71 | if h.is_in_base(msg.main, base_path) then 72 | base_loc = msg.main 73 | table.insert(diags, h.item_to_diag(msg.main, "error")) 74 | end 75 | for _, item in ipairs(msg.reftrace) do 76 | if h.is_in_base(item, base_path) then 77 | if base_loc == nil then 78 | base_loc = item 79 | table.insert(diags, h.item_to_diag(item, "error_base", msg.main)) 80 | else 81 | table.insert(diags, h.item_to_diag(item, "ref", msg.main)) 82 | end 83 | end 84 | end 85 | for _, item in ipairs(msg.notes) do 86 | if h.is_in_base(item, base_path) then 87 | table.insert(diags, h.item_to_diag(item, "note", msg.main)) 88 | end 89 | end 90 | return diags 91 | end 92 | 93 | function h.bundle_to_diags(bundle, base_path) 94 | diags = {} 95 | for _,msg in ipairs(bundle) do 96 | h.msg_to_diag(msg, base_path, diags) 97 | end 98 | return diags 99 | end 100 | 101 | return h 102 | -------------------------------------------------------------------------------- /lua/zigclient/init.lua: -------------------------------------------------------------------------------- 1 | local h = _G._mod_zigclient or {} 2 | _G._mod_zigclient = h 3 | 4 | local has_luadev, luadev = pcall(require, "luadev") 5 | local print = has_luadev and luadev.print or _G.print 6 | 7 | h.client_messages = { 8 | exit = 0; 9 | update = 1; 10 | run = 2; 11 | hot_update = 3; 12 | query_test_metadata = 4; 13 | run_test = 5; 14 | } 15 | 16 | h.server_messages = { 17 | zig_version = 0; 18 | error_bundle = 1; 19 | progress = 2; 20 | emit_bin_path = 3; 21 | test_metadata = 4; 22 | test_results = 5; 23 | } 24 | 25 | local ffi = require'ffi' 26 | function u32(bytes, where) 27 | return ffi.cast('uint32_t*', bytes)[where or 0] 28 | end 29 | 30 | function h:parse_output() 31 | while true do 32 | if self.data == nil or #self.data < 8 then 33 | return 34 | end 35 | local kind = u32(string.sub(self.data, 1, 4)) 36 | local len = u32(string.sub(self.data, 5, 8)) 37 | if #self.data < 8+len then 38 | return 39 | end 40 | -- print("msg:", kind, len) 41 | 42 | local body = string.sub(self.data,9, 8+len) -- there's a body alright 43 | local nxt = string.sub(self.data,9+len) 44 | self.data = #nxt > 0 and nxt or nil 45 | 46 | self._body[kind] = body 47 | 48 | local s = h.server_messages 49 | local value = body 50 | if kind == s.zig_version then 51 | self.zig_version = body 52 | elseif kind == s.progress then 53 | elseif kind == s.emit_bin_path then 54 | self.bin_path = body 55 | if string.sub(self.bin_path, 1, 1) == '\0' then -- ?? 56 | self.bin_path = string.sub(self.bin_path,2) 57 | end 58 | value = self.bin_path 59 | elseif kind == s.error_bundle then 60 | self.err_bundle = h.parse_errors(body) 61 | value = self.err_bundle 62 | elseif kind == s.test_metadata then 63 | self.test_metadata = self.parse_test_metadata(body) 64 | value = self.test_metadata 65 | elseif kind == s.test_results then 66 | -- TODO 67 | end 68 | 69 | if self.cb then 70 | local status, res = pcall(self.cb,kind,value) 71 | if not status then 72 | _G.err = res 73 | print('callback error', res) 74 | end 75 | end 76 | end 77 | end 78 | 79 | function h.parse_errors(body) 80 | extra_len = u32(body) 81 | string_bytes_len = u32(body,1) 82 | extra_data = string.sub(body,9,8+extra_len*4) 83 | string_data = string.sub(body,8+extra_len*4+1) 84 | extra = ffi.cast('uint32_t*', extra_data) 85 | string_bytes = ffi.cast('char*', string_data) 86 | 87 | eml_len = extra[0] 88 | eml_start = extra[1] 89 | eml_log_text = extra[2] 90 | 91 | local function src_loc(src_at, rec) 92 | if src_at == 0 then return {} end 93 | local reference_trace_len = extra[src_at+7] 94 | local srcref_at = src_at+8 95 | local ref_trace = {} 96 | local ref_hidden = nil 97 | for i = 1,reference_trace_len do 98 | ref_decl_name = extra[srcref_at+2*(i-1)+0] 99 | ref_src_loc = extra[srcref_at+2*(i-1)+1] 100 | if ref_src_loc ~= 0 then 101 | ref_decl = ffi.string(string_bytes+ref_decl_name) 102 | if rec then -- format in theory allows recursive traces, assume such maddnes won't be needed for now 103 | ref_src_loc = src_loc(ref_src_loc, false) 104 | end 105 | table.insert(ref_trace, {decl_name=ffi.string(string_bytes+ref_decl_name), src_loc=ref_src_loc}) 106 | else 107 | ref_hidden = ref_decl_name 108 | end 109 | end 110 | return { 111 | src_path = ffi.string(string_bytes+extra[src_at]); 112 | line = extra[src_at+1]; 113 | col = extra[src_at+2]; 114 | span_start = extra[src_at+3]; 115 | span_main = extra[src_at+4]; 116 | span_end = extra[src_at+5]; 117 | source_line = ffi.string(string_bytes+extra[src_at+6]); 118 | ref_trace = ref_trace; 119 | ref_hidden = ref_hidden; 120 | } 121 | end 122 | 123 | local function message(msg_at, rec) 124 | local msg = { 125 | msg = ffi.string(string_bytes+extra[msg_at]); 126 | count = extra[msg_at+1]; 127 | src_loc = src_loc(extra[msg_at+2], true); 128 | notes = {} 129 | } 130 | local notes_len = extra[msg_at+3]; 131 | local notes_at = msg_at+4 132 | for i = 1,notes_len do 133 | local note = extra[notes_at+i-1] 134 | if rec then -- format in theory allows recursive notes, assume such maddnes won't be needed for now 135 | note = message(note, false) 136 | end 137 | table.insert(msg.notes, note) 138 | end 139 | return msg 140 | end 141 | 142 | local messages = {} 143 | for msgid = 1,eml_len do 144 | local msg_at = extra[eml_start+msgid-1] 145 | table.insert(messages, message(msg_at, true)) 146 | end 147 | 148 | return messages 149 | end 150 | 151 | function h.parse_test_metadata(body) 152 | string_bytes_len = u32(body) 153 | tests_len = u32(body,1) 154 | name_data = string.sub(body,9,8+tests_len*4) 155 | async_frame_len_data = string.sub(body,9+tests_len*4,8+tests_len*8) 156 | expected_panic_data = string.sub(body,9+tests_len*8,8+tests_len*12) 157 | string_data = string.sub(body,9+tests_len*12,8+tests_len*12+string_bytes_len) 158 | string_bytes = ffi.cast('char*', string_data) 159 | 160 | tests = {} 161 | for i = 1,tests_len do 162 | expected_panic_idx = u32(expected_panic_data, i-1); 163 | table.insert(tests, { 164 | name = ffi.string(string_bytes+u32(name_data, i-1)); 165 | async_frame_len = u32(async_frame_len_data, i-1); 166 | expected_panic_msg = expected_panic_idx > 0 and ffi.string(string_bytes+expected_panic_idx) or nil; 167 | }) 168 | end 169 | return tests 170 | end 171 | 172 | h.__index = h 173 | 174 | local uv = vim.loop 175 | function h.start_server(cmd, args, cb) 176 | local self = setmetatable({}, h) 177 | self.stdin = uv.new_pipe(false) 178 | 179 | self._body = {} 180 | 181 | self.stderr_hnd = uv.pipe() 182 | self.stderr = uv.new_pipe() 183 | self.stderr:open(self.stderr_hnd.read) 184 | self.stderr:read_start(vim.schedule_wrap(function(err,data) 185 | if data then print(data) end 186 | end)) 187 | 188 | self.data = nil 189 | self.cb = cb 190 | 191 | self.stdout_hnd = uv.pipe() 192 | self.stdout = uv.new_pipe() 193 | self.stdout:open(self.stdout_hnd.read) 194 | self.stdout:read_start(function(err,data) 195 | if not data then 196 | return 197 | end 198 | 199 | -- TODO: check if luajit handles ''..data === data w/o copy 200 | if self.data then 201 | self.data = self.data .. data 202 | else 203 | self.data = data 204 | end 205 | vim.schedule(function() self:parse_output() end) 206 | end) 207 | 208 | vim.print(args) 209 | 210 | self.handle, self.pid = uv.spawn(cmd, { 211 | args = args, 212 | stdio = {self.stdin, self.stdout_hnd.write, self.stderr_hnd.write} 213 | }, vim.schedule_wrap(function() 214 | print("server exit") 215 | end)) 216 | 217 | return self 218 | end 219 | 220 | function h.zig_server(entrypoint, test) 221 | local subcmd = test and 'test' or 'build-exe' 222 | args = { 223 | subcmd; 224 | '-freference-trace'; 225 | -- '-fno-emit-bin'; 226 | entrypoint; 227 | "--listen=-"; 228 | } 229 | return h.start_server("zig", args) 230 | end 231 | 232 | function h:send(msg, data) 233 | if type(msg) == 'string' then 234 | msg = h.client_messages[msg] 235 | end 236 | data = data or '' 237 | local header = ffi.new("uint32_t[2]") 238 | header[0] = msg 239 | header[1] = #data 240 | header = ffi.string(header,8) 241 | self.stdin:write(header) 242 | if #data > 0 then 243 | self.stdin:write(data) 244 | end 245 | end 246 | 247 | function h:update() 248 | self:send(h.client_messages.update) 249 | end 250 | 251 | function h:run_test(nr) 252 | local body = ffi.new("uint32_t[1]") 253 | body[0] = nr 254 | body = ffi.string(body,4) 255 | return self:send(h.client_messages.run_test, body) 256 | end 257 | 258 | return h 259 | -------------------------------------------------------------------------------- /scratch.lua: -------------------------------------------------------------------------------- 1 | mod = loadfile 'lua/zigclient/init.lua' () 2 | b = loadfile 'lua/zigclient/bundle.lua' () 3 | s = mod.zig_server('/home/bfredl/dev/forklift/src/run_ir.zig', false) 4 | s = mod.zig_server('/home/bfredl/dev/forklift/src/FLIR.zig', true) 5 | base_path = '/home/bfredl/dev/forklift/src' 6 | s:send(s.client_messages.update) 7 | s._body 8 | e = b.process_bundle(s.err_bundle) 9 | di = b.msg_to_diag(e[1], base_path) 10 | msg = e[1] 11 | t = mod.start_server(s.bin_path, {"--listen=-"}) 12 | s.err_bundle 13 | t:send(t.client_messages.query_test_metadata) 14 | t.test_metadata[19+1] 15 | md = mod.parse_test_metadata(t.test_meta_body) 16 | #md 17 | 18 | string.sub("\0ab", 2) 19 | t:send(t.client_messages.exit) 20 | t:run_test(20) 21 | t 22 | t.test_res_body 23 | 24 | 25 | s:send(s.client_messages.exit) 26 | s:send(s.client_messages.exit) 27 | s:send(s.client_messages.query_test_metadata) 28 | s.err_bundle 29 | -------------------------------------------------------------------------------- /test_plugin.lua: -------------------------------------------------------------------------------- 1 | local h = require 'zigclient' 2 | local b = require 'zigclient.bundle' 3 | local ns = vim.api.nvim_create_namespace 'zigclient' 4 | 5 | base_path = vim.loop.cwd() 6 | diag_bufs = {} 7 | function set_multibuf_diags(diags) 8 | local bufdiag = {} 9 | for k,_ in pairs(diag_bufs) do 10 | bufdiag[k] = {} 11 | end 12 | for _,d in ipairs(diags) do 13 | bufdiag[d.bufnr] = bufdiag[d.bufnr] or {} 14 | table.insert(bufdiag[d.bufnr], d) 15 | end 16 | for b,d in pairs(bufdiag) do 17 | diag_bufs[b] = true 18 | vim.diagnostic.set(ns, b, d, {}) 19 | end 20 | end 21 | 22 | function doit(kind, value) 23 | if kind == h.server_messages.error_bundle then 24 | bundle = b.process_bundle(value) 25 | theerr = b.bundle_to_diags(bundle, base_path) 26 | set_multibuf_diags(theerr) 27 | print("errors!") 28 | elseif kind == h.server_messages.emit_bin_path then 29 | theerr = {} 30 | set_multibuf_diags(theerr) 31 | print("good!") 32 | end 33 | end 34 | s = h.start_server("zig", {'build-exe', '-lc', vim.api.nvim_buf_get_name(0), '-freference-trace', '--listen=-'}, doit) 35 | function update() 36 | vim.cmd 'update' 37 | s:update() 38 | end 39 | vim.keymap.set('n', 'ch:ir', 'lua update()') 40 | --------------------------------------------------------------------------------