From 3ca6add89f9e2e40e0b9886df779051ea6cfc7c5 Mon Sep 17 00:00:00 2001 From: theKnightsOfRohan Date: Fri, 24 Jan 2025 10:06:57 -0600 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .github/workflows/docs.yml | 30 +++++++ .stylua.toml | 3 + LICENSE | 21 +++++ Makefile | 19 ++++ lua/hexer/init.lua | 93 +++++++++++++++++++ lua/hexer/parser.lua | 148 +++++++++++++++++++++++++++++++ lua/hexer/tests/minimal_init.lua | 21 +++++ lua/hexer/tests/parser_spec.lua | 126 ++++++++++++++++++++++++++ 9 files changed, 463 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/docs.yml create mode 100644 .stylua.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 lua/hexer/init.lua create mode 100644 lua/hexer/parser.lua create mode 100644 lua/hexer/tests/minimal_init.lua create mode 100644 lua/hexer/tests/parser_spec.lua diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..cb27fde --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,30 @@ +name: Generate Vimdoc + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: panvimdoc + uses: kdheepak/panvimdoc@main + with: + vimdoc: venison.nvim + version: "Neovim >= 0.8.0" + demojify: true + treesitter: true + - name: Push changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "auto-generate vimdoc" + commit_user_name: "github-actions[bot]" + commit_user_email: "github-actions[bot]@users.noreply.github.com" + commit_author: "github-actions[bot] " + diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..3825fd6 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,3 @@ +indent_type = "Spaces" +indent_width = 4 +quote_style = "AutoPreferDouble" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4e8c80d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 theKnightsOfRohan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..40190c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +fmt: + echo "===> Formatting" + stylua lua/ --config-path=.stylua.toml + +lint: + echo "===> Linting" + luacheck lua/ --globals vim + +test: + echo "===> Testing" + nvim --headless --noplugin -u lua/hexer/tests/minimal_init.lua -c "PlenaryBustedDirectory lua/hexer/tests/ { minimal_init = 'lua/hexer/tests/minimal_init.lua' }" + +clean: + echo "===> Cleaning testing dependencies" + rm -rf /tmp/hexer_test/plenary.nvim + rm -rf /tmp/hexer_test/nui.nvim + +all: + make fmt lint test diff --git a/lua/hexer/init.lua b/lua/hexer/init.lua new file mode 100644 index 0000000..551818c --- /dev/null +++ b/lua/hexer/init.lua @@ -0,0 +1,93 @@ +local Popup = require("nui.popup") +local Table = require("nui.table") +local Event = require("nui.utils.autocmd").event +local Parser = require("hexer.parser") + +---@param data HexerItem +---@return NuiTable +local function new_table(data) + return Table({ + bufnr = 0, + ns_id = "HexerWindow", + columns = { + { accessor_key = "ascii", header = "Ascii" }, + { accessor_key = "value", header = "Value" }, + { accessor_key = "hex", header = "Hex" }, + { accessor_key = "binary", header = "Binary" }, + { accessor_key = "octal", header = "Octal" }, + }, + data = data, + }) +end + +local M = { + ---@type HexerItem + current_value = nil, + _window = Popup({ + enter = true, + focusable = true, + border = { + style = "rounded", + }, + position = { + row = "100%", + col = "100%", + }, + size = { + width = 33, + height = 11, + }, + }), + -- _table = new_table(Parser.parse_input("x69")), +} + +function M.hide_window(self) + self._window:unmount() +end + +function M.show_window(self) + self._window:mount() + self._window:on(Event.BufLeave, function() + self:hide_window() + end) + + self._window:map("n", "", function() self:hide_window() end, {}) + self._window:map("n", "q", function() self:hide_window() end, {}) + + self._table.bufnr = self._window.bufnr + + vim.api.nvim_buf_set_lines(self._window.bufnr, 0, -1, false, {}) + + M._table:render() + self._window:update_layout({ + size = { + width = 33, + height = 20, + } + }) +end + +-- Create a hexer buffer; if the value is null, will display the previously displayed value. +---@param self table +function M.toggle_window(self) + if self.window ~= nil then + self:show_window() + else + self:hide_window() + end +end + +function M.update_table(self, arg) +end + +function M.setup() + vim.api.nvim_create_user_command("Hexer", function(opts) + if opts.args ~= nil then + M:update_table(arg) + end + + M:show_window() + end, {}) +end + +return M diff --git a/lua/hexer/parser.lua b/lua/hexer/parser.lua new file mode 100644 index 0000000..da84c24 --- /dev/null +++ b/lua/hexer/parser.lua @@ -0,0 +1,148 @@ +local M = {} + +---@class HexerChar +---@field ascii string +---@field value integer +---@field hex string +---@field binary string +---@field octal string + +---@alias HexerItem HexerChar[] + +-- Check whether a passed string is wrapped by quotes +---@param str string +---@return boolean +function M._is_string_wrapped(str) + return #str >= 3 and ( + (str:sub(1, 1) == '"' and str:sub(str:len()) == '"') or + (str:sub(1, 1) == "'" and str:sub(str:len()) == "'") + ) +end + +---Parse an input given by the user into a HexerItem, last resorts to string +---@param input string +---@return HexerItem +function M.parse_input(input) + local value = tonumber(input) + if value ~= nil then + return { M._parse_from_int(value) } + end + + if #input == 1 then return { M._parse_from_int(input:byte(1)) } end + + local head, tail = 1, input:len() + if input:sub(1, 1) == '0' then head = head + 2 end + local orig_head = head + + + head = M._check_header(input, { 'x', 'X' }) + if head ~= orig_head then return { M._parse_from_int(M._xtoi(input:sub(head))) } end + + head = M._check_header(input, { 'b', 'B' }) + if head ~= orig_head then return { M._parse_from_int(M._btoi(input:sub(head))) } end + + head = M._check_header(input, { 'o', 'O' }) + if head ~= orig_head then return { M._parse_from_int(M._otoi(input:sub(head))) } end + + ---@type HexerItem + local item = {} + + if M._is_string_wrapped(input) then + head = head + 1 + tail = tail - 1 + end + + for i = head, tail do + item[i] = M._parse_from_int(input:byte(i)) + end + + return item +end + +---String in decimal form to integer +---@param input string +---@return integer +function M._stoi(input) + return tonumber(input, 10); +end + +---String in decimal form to integer +---@param input string +---@return integer +function M._xtoi(input) + return tonumber(input, 16); +end + +---String in decimal form to integer +---@param input string +---@return integer +function M._btoi(input) + return tonumber(input, 2); +end + +---String in decimal form to integer +---@param input string +---@return integer +function M._otoi(input) + return tonumber(input, 8); +end + +---Integer in decimal form to binary string +---@param input integer +---@return string +function M._itob(input) + if input == 0 then + return "0" + end + + if input < 0 then + input = math.abs(input) + end + + local result = "" + + while input > 0 do + local remainder = input % 2 + result = remainder .. result + input = math.floor(input / 2) + end + + return result +end + +---Checks if the string has the given number format header and returns the start of the value if it does, otherwise 0 +---@param str string +---@param header string[] the list of single-character format specifiers +---@return integer +function M._check_header(str, header) + for _, ch in ipairs(header) do + if str:sub(1, 1) == ch then + return 2 + end + + -- Should never occur, but just in case + if str:sub(1, 1) == '0' and str:sub(2, 2) == ch then + return 3 + end + end + + return 0 +end + +---Create a HexerChar from a given integer +---@param value integer +---@return HexerChar +function M._parse_from_int(value) + ---@type HexerChar + local item = { + value = value, + hex = "0x" .. string.format("%X", value), + octal = "0o" .. string.format("%o", value), + ascii = string.format("%c", value), + binary = "0b" .. M._itob(value), + } + + return item +end + +return M diff --git a/lua/hexer/tests/minimal_init.lua b/lua/hexer/tests/minimal_init.lua new file mode 100644 index 0000000..a027bf7 --- /dev/null +++ b/lua/hexer/tests/minimal_init.lua @@ -0,0 +1,21 @@ +local plenary_dir = "/tmp/hexer_test/plenary.nvim" +local is_not_a_directory = vim.fn.isdirectory(plenary_dir) == 0 +if is_not_a_directory then + print("===> Cloning testing dependency Plenary") + vim.fn.system({ "git", "clone", "https://github.com/nvim-lua/plenary.nvim", plenary_dir }) +end + +local nui_dir = "/tmp/hexer_test/nui.nvim" +is_not_a_directory = vim.fn.isdirectory(nui_dir) == 0 + +if is_not_a_directory then + print("===> Cloning testing dependency Nui") + vim.fn.system({ "git", "clone", "https://github.com/MunifTanjim/nui.nvim", nui_dir }) +end + +vim.opt.rtp:append(".") +vim.opt.rtp:append(plenary_dir) +vim.opt.rtp:append(nui_dir) + +vim.cmd("runtime plugin/plenary.vim") +vim.cmd("runtime plugin/nui.vim") diff --git a/lua/hexer/tests/parser_spec.lua b/lua/hexer/tests/parser_spec.lua new file mode 100644 index 0000000..582833d --- /dev/null +++ b/lua/hexer/tests/parser_spec.lua @@ -0,0 +1,126 @@ +local Parser = require("hexer.parser") + +describe("Parser", function() + it("should be able to detect whether a passed string is quoted", function() + local strs = { + ["''"] = false, + ['""'] = false, + ["potato"] = false, + ["'p'"] = true, + ['"p"'] = true, + ["'potato'"] = true, + ['"potato"'] = true + } + + local res + + for str, q in pairs(strs) do + res = Parser._is_string_wrapped(str) == q + assert(res, ("%s expected to be %s but got %s"):format(str, q, res)) + end + end) + + it("should correctly see if a given string contains the header", function() + local strs = { "0b0011", "B0011" } + local headers = { { "b", "B" }, { "x", "X" } } + + local val_pos = Parser._check_header(strs[1], headers[1]) + + assert(val_pos == 3, + ("String %s with headers %s expected pos %s but got %s"):format(strs[1], vim.inspect(headers[1]), 3, val_pos) + ) + + val_pos = Parser._check_header(strs[1], headers[2]) + + assert(val_pos == 0, + ("String %s with headers %s expected pos %s but got %s"):format(strs[1], vim.inspect(headers[2]), 0, val_pos) + ) + + val_pos = Parser._check_header(strs[2], headers[1]) + + assert(val_pos == 2, + ("String %s with headers %s expected pos %s but got %s"):format(strs[2], vim.inspect(headers[1]), 2, val_pos) + ) + + val_pos = Parser._check_header(strs[2], headers[2]) + + assert(val_pos == 0, + ("String %s with headers %s expected pos %s but got %s"):format(strs[2], vim.inspect(headers[2]), 0, val_pos) + ) + end) + + it("should be able to correctly parse from a decimal value", function() + local parsed = Parser._parse_from_int(69) + + ---@type HexerChar + local target = { + value = 69, + ascii = "E", + binary = "0b1000101", + hex = "0x45", + octal = "0o105", + } + + for k, v in pairs(target) do + assert(v == parsed[k], string.format("Key %s: %s != %s", tostring(k), tostring(v), tostring(parsed[k]))) + end + end) + + it("should be able to correctly parse from string inputs", function() + local target = { + value = 69, + ascii = "E", + binary = "0b1000101", + hex = "0x45", + octal = "0o105", + } + + local parsed = Parser.parse_input("69") + + assert(#parsed == 1, string.format("Decimal string: expected length 1, is instead %s", tostring(#parsed))) + + for k, v in pairs(target) do + assert(v == parsed[1][k], string.format("Decimal string: %s != %s", tostring(v), tostring(parsed[1][k]))) + end + + parsed = Parser.parse_input("E") + assert(parsed) + + assert(#parsed == 1, string.format("Ascii character: expected length 1, is instead %s", tostring(#parsed))) + + for k, v in pairs(target) do + assert(v == parsed[1][k], + string.format("Ascii character: expected %s, actual %s", tostring(v), tostring(parsed[1][k]))) + end + + parsed = Parser.parse_input("0b1000101") + assert(parsed) + + assert(#parsed == 1, string.format("Binary: expected length 1, is instead %s", tostring(#parsed))) + + for k, v in pairs(target) do + assert(v == parsed[1][k], + string.format("Binary: expected %s, actual %s", tostring(v), tostring(parsed[1][k]))) + end + + parsed = Parser.parse_input("0x45") + assert(parsed) + + assert(#parsed == 1, string.format("Hexadecimal: expected length 1, is instead %s", tostring(#parsed))) + + for k, v in pairs(target) do + assert(v == parsed[1][k], + string.format("Hexadecimal: expected %s, actual %s", tostring(v), tostring(parsed[1][k]))) + end + + parsed = Parser.parse_input("0o105") + assert(parsed) + + assert(#parsed == 1, string.format("Octal: expected length 1, is instead %s", tostring(#parsed))) + + for k, v in pairs(target) do + assert(v == parsed[1][k], + string.format("Octal: expected %s, actual %s", tostring(v), tostring(parsed[1][k]))) + end + end) +end)