diff --git a/README.md b/README.md index 83bd4c8..22c7ce6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ - [Installing](#installing) - [Setup](#setup) +- [Commands](#commands) +- [JSON view](#json-view) - [Example keymaps](#example-keymaps) - [Supported Codecs](#supported-codecs) - [base32](#base32) @@ -100,6 +102,32 @@ require("decipher").setup({ }) ``` +## Commands + +### `DecipherVersion` + +Prints the current version. + +### `[range]DecipherEncode [options]` + +Encode the selected visual range or the last visual selection with the codec +given on the command line. Prompts for a codec if not given. Also takes an +option for previewing i.e. `preview=true|false`. + +The command supports command previewing, see `:h 'inccommand'`. + +Example: `:'<,'>DecipherEncode crockford preview=false` + +### `[range]DecipherDecode [options]` + +Decode the selected visual range or the last visual selection with the codec +given on the command line. Prompts for a codec if not given. Also takes an +option for previewing i.e. `preview=true|false`. + +The command supports command previewing, see `:h 'inccommand'`. + +Example: `:'<,'>DecipherDecode base64 preview=true` + ## JSON view ### Applying diff --git a/doc/decipher.txt b/doc/decipher.txt index 4bb5299..538e647 100644 --- a/doc/decipher.txt +++ b/doc/decipher.txt @@ -19,6 +19,7 @@ decipher *decipher* Setup ....................................................... |decipher.setup| Types ....................................................... |decipher.types| Functions ............................................... |decipher.functions| + Commands ................................................. |decipher.commands| Mappings ................................................. |decipher.mappings| Motions ................................................... |decipher.motions| Highlights ............................................. |decipher.highlights| @@ -213,6 +214,40 @@ decode_motion_prompt({options}) *decipher.decode_motion_prompt* Parameters: • {options} (`decipher.Options`) Options to use +============================================================================== +Commands *decipher.commands* + +:DecipherVersion *:DecipherVersion* + Prints the current version. + +:[range]DecipherEncode [options] *:DecipherEncode* + Encode the selected visual range or the last + visual selection with the codec given on the + command line. Prompts for a codec if not + given. Also takes an option for previewing + i.e. `preview=true|false`. + + The command supports command previewing, see + |'inccommand'|. + + Example: + > + :'<,'>DecipherEncode crockford preview=false + +:[range]DecipherDecode [options] *:DecipherDecode* + Decode the selected visual range or the last + visual selection with the codec given on the + command line. Prompts for a codec if not + given. Also takes an option for previewing + i.e. `preview=true|false`. + + The command supports command previewing, see + |'inccommand'|. + + Example: + > + :'<,'>DecipherDecode base64 preview=true + ============================================================================== Mappings *decipher.mappings* @@ -257,6 +292,10 @@ Highlights *decipher.highlights* Highlight group for the title of the floating window preview. Defaults to *Title*. +*DecipherCommandPreview* + Highlight group used in command previews for the encoded/decoded text. + Defaults to *Title*. + ============================================================================== Codecs *decipher.codecs* diff --git a/lua/decipher/init.lua b/lua/decipher/init.lua index e1ef5bc..957d36b 100644 --- a/lua/decipher/init.lua +++ b/lua/decipher/init.lua @@ -19,6 +19,13 @@ decipher.codec = codecs.codec ---@field selection_type decipher.SelectionType ---@field codec_type "encode" | "decode" +---@class decipher.SetTextRegionOptions +---@field codec_name string +---@field codec_type "encode" | "decode" +---@field status boolean +---@field value string +---@field selection_type decipher.SelectionType + ---@alias decipher.CodecArg string | decipher.Codecs ---@return string @@ -26,7 +33,7 @@ function decipher.version() return decipher_version end ----@return decipher.Codecs[] a list of the names of supported codecs +---@return decipher.Codecs[] # a list of the names of supported codecs function decipher.supported_codecs() return codecs.supported() end @@ -34,7 +41,7 @@ end ---@param codec_name decipher.CodecArg ---@param value string ---@param func_key "encode" | "decode" ----@return string | nil +---@return string? local function handle_codec(codec_name, value, func_key) local codec = codecs.get(codec_name) @@ -53,14 +60,14 @@ end ---@param codec_name decipher.CodecArg ---@param value string value to encode ----@return string | nil the encoded value or nil if encoding failed +---@return string? the encoded value or nil if encoding failed function decipher.encode(codec_name, value) return handle_codec(codec_name, value, "encode") end ---@param codec_name decipher.CodecArg ---@param value string value to decode ----@return string | nil the decoded value or nil if decoding failed +---@return string? the decoded value or nil if decoding failed function decipher.decode(codec_name, value) return handle_codec(codec_name, value, "decode") end @@ -96,24 +103,31 @@ local function open_float_handler(codec_name, status, value, selection_type, cod end --- Handler for setting a text region to a value ----@param codec_name string ----@param status boolean ----@param value string ----@param selection_type decipher.SelectionType ----@diagnostic disable-next-line:unused-local -local function set_text_region_handler(codec_name, status, value, selection_type) - if not status then - notifications.error(("%s: %s"):format(codec_name, value)) +---@param options decipher.SetTextRegionOptions +local function set_text_region_handler(options) + local value = options.value + + if not options.status then + notifications.error(("%s: %s"):format(options.codec_name, value)) return end if value == nil then - notifications.codec_not_found("Codec not found: " .. codec_name) + notifications.codec_not_found("Codec not found: " .. options.codec_name) return end + ---@type string[] + local lines + + if options.codec_type == "decode" then + lines = vim.split(value, "\r?\n") + else + lines = { value } + end + -- Escape the string since you cannot set lines in a buffer if it contains newlines - selection.set_text(0, selection_type, str_utils.escape_newlines({ value })) + selection.set_text(0, options.selection_type, str_utils.escape_newlines(lines)) end --- Process a codec action @@ -134,7 +148,13 @@ local function process_codec(codec_name, codec_func, selection_type, codec_type, if do_preview then open_float_handler(_codec_name, status, value, selection_type, codec_type) else - set_text_region_handler(_codec_name, status, value, selection_type) + set_text_region_handler({ + codec_name = _codec_name, + status = status, + value = value, + selection_type = selection_type, + codec_type = codec_type, + }) end end @@ -227,6 +247,8 @@ function decipher.setup(user_config) config.setup(user_config) ui.float.setup() + + vim.cmd([[hi default link DecipherCommandPreview Title]]) end return decipher diff --git a/plugin/decipher.lua b/plugin/decipher.lua new file mode 100644 index 0000000..c02ae47 --- /dev/null +++ b/plugin/decipher.lua @@ -0,0 +1,157 @@ +---@alias decipher.CommandPreviewFunc fun( +---options: vim.api.keyset.create_user_command.command_args, +---ns_id: integer, +---preview_buffer: integer): integer + +---@type string[]? +local command_arguments + +---@param arg_lead string +---@param cmdline string +---@param cursor_pos integer +---@return string[] +local function command_complete(arg_lead, cmdline, cursor_pos) + if not command_arguments then + command_arguments = vim.list_extend({ "preview=true", "preview=false" }, require("decipher").supported_codecs()) + end + + return command_arguments +end + +---@param args string +---@return string[], string[], string[], string[] +local function parse_command_args(args) + local decipher = require("decipher") + local codecs, options, unrecognised_args, errors = {}, {}, {}, {} + local supported_codecs = decipher.supported_codecs() + + for _, arg in ipairs(vim.split(args, " ", { plain = true, trimempty = true })) do + ---@type string? + local preview_match = arg:match("preview=(%S+)") + + if preview_match then + if preview_match ~= "true" and preview_match ~= "false" then + table.insert(errors, "Invalid value for option 'preview', expected 'true' or 'false'") + else + options.preview = preview_match == "true" + end + elseif vim.tbl_contains(supported_codecs, arg) then + table.insert(codecs, arg) + else + table.insert(unrecognised_args, arg) + end + end + + return codecs, options, unrecognised_args, errors +end + +---@param codecs string[] +---@param unrecognised_args string[] +---@param errors string[] +local function process_command_args(codecs, unrecognised_args, errors) + if #codecs > 1 then + error("Multiple valid codecs given, specify only one") + end + + if #errors > 0 then + error(("Found one or more errors: %s"):format(vim.fn.join(errors, ", "))) + end + + if #unrecognised_args > 0 then + error(("One or more unrecognised arguments: %s"):format(vim.fn.join(unrecognised_args, ", "))) + end +end + +---@param codec_func_name "encode" | "decode" +---@return decipher.CommandPreviewFunc +local function create_preview_func(codec_func_name) + return function(options, ns_id, preview_buffer) + local codecs, _, unrecognised_args, errors = parse_command_args(options.args) + + if #codecs ~= 1 or #unrecognised_args > 0 or #errors > 0 then + -- Do not show preview + return 0 + end + + local buffer = vim.api.nvim_get_current_buf() + local region = require("decipher.selection").get_selection("visual") + local start_lnum, end_lnum = region.start.lnum - 1, region["end"].lnum - 1 + local start_col, end_col = region.start.col - 1, region["end"].col - 1 + local text = vim.api.nvim_buf_get_text(buffer, start_lnum, start_col, end_lnum, end_col + 1, {}) + local ok, result = pcall(require("decipher")[codec_func_name], codecs[1], table.concat(text, "\n")) + + if not ok then + -- Do not show preview + return 0 + end + + local new_lines = vim.split(result, "\r?\n") + local new_end_lnum = end_lnum + #new_lines - 1 + local new_end_col = #new_lines[#new_lines] + + if codec_func_name == "decode" and #new_lines == 1 then + -- If there is only one decoded line offset the new end column by + -- the original start column + new_end_col = start_col + new_end_col + end + + -- Set preview text and highlight in buffer + vim.api.nvim_buf_set_text(buffer, start_lnum, start_col, end_lnum, end_col + 1, new_lines) + vim.hl.range(buffer, ns_id, "DecipherCommandPreview", { start_lnum, start_col }, { new_end_lnum, new_end_col }) + + -- Set preview text and highlight in preview buffer if enabled + if preview_buffer then + local lines = vim.api.nvim_buf_get_lines(buffer, start_lnum, new_end_lnum + 1, true) + + vim.api.nvim_buf_set_lines(preview_buffer, 0, -1, false, lines) + vim.hl.range(preview_buffer, ns_id, "DecipherCommandPreview", { 0, start_col }, { #new_lines, new_end_col }) + end + + return 2 + end +end + +vim.api.nvim_create_user_command("DecipherVersion", function() + local version = require("decipher").version() + vim.cmd.echo("'" .. version .. "'") +end, { nargs = 0 }) + +vim.api.nvim_create_user_command("DecipherEncode", function(args) + local codecs, options, unrecognised_args, errors = parse_command_args(args.args) + + process_command_args(codecs, unrecognised_args, errors) + + local decipher = require("decipher") + + if #codecs == 0 then + decipher.encode_selection_prompt(options) + else + decipher.encode_selection(codecs[1], options) + end +end, { + range = true, + nargs = "?", + complete = command_complete, + desc = "Encode a visual selection", + preview = create_preview_func("encode"), +}) + +vim.api.nvim_create_user_command("DecipherDecode", function(args) + local codecs, options, unrecognised_args, errors = parse_command_args(args.args) + + process_command_args(codecs, unrecognised_args, errors) + + local decipher = require("decipher") + + if #codecs == 0 then + decipher.decode_selection_prompt(options) + else + decipher.decode_selection(codecs[1], options) + end +end, { + range = true, + nargs = "?", + complete = command_complete, + desc = "Decode a visual selection", + preview = create_preview_func("decode"), +}) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index fb62299..3470249 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,4 +1,5 @@ vim.opt.rtp:append(".") vim.opt.rtp:append("~/.local/share/nvim/lazy/plenary.nvim") +vim.cmd.runtime({ "plugin/decipher.lua", bang = true }) vim.cmd.runtime({ "plugin/plenary.vim", bang = true }) diff --git a/tests/plugin_spec.lua b/tests/plugin_spec.lua new file mode 100644 index 0000000..da5614a --- /dev/null +++ b/tests/plugin_spec.lua @@ -0,0 +1,65 @@ +local vader = require("decipher.util.vader") + +local given, normal, expect = vader.given, vader.normal, vader.expect + +describe("commands", function() + it("runs DecipherVersion", function() + assert.are.same(vim.fn.exists(":DecipherVersion"), 2) + + local output = vim.fn.execute("DecipherVersion", "") + local version = vim.split(output, "\n", { plain = true, trimempty = true })[1] + + ---@diagnostic disable-next-line: undefined-field + assert.has_match("^%d+%.%d+%.%d+$", version) + end) + + it("runs DecipherEncode", function() + assert.are.same(vim.fn.exists(":DecipherEncode"), 2) + + given({ "light work" }, function(context) + normal("gg0vg_") + vim.cmd("DecipherEncode base64") + expect({ "bGlnaHQgd29yaw==" }) + end) + end) + + it("handles invalid options for 'preview'", function() + local ok, err = pcall(vim.fn.execute, "DecipherEncode preview=nope", "") + + assert.is_false(ok) + + ---@diagnostic disable-next-line: undefined-field + assert.has_match( + "Found one or more errors: Invalid value for option 'preview', expected 'true' or 'false'", + err + ) + end) + + it("handles more than one codec", function() + local ok, err = pcall(vim.fn.execute, "DecipherEncode base64 base32", "") + + assert.is_false(ok) + + ---@diagnostic disable-next-line: undefined-field + assert.has_match("Multiple valid codecs given, specify only one", err) + end) + + it("handles unknown arguments", function() + local ok, err = pcall(vim.fn.execute, "DecipherEncode nope preview=true", "") + + assert.is_false(ok) + + ---@diagnostic disable-next-line: undefined-field + assert.has_match("One or more unrecognised arguments: nope", err) + end) + + it("runs DecipherDecode", function() + assert.are.same(vim.fn.exists(":DecipherDecode"), 2) + + given({ "bGlnaHQgd29yaw==" }, function(context) + normal("gg0vg_") + vim.cmd("DecipherDecode base64") + expect({ "light work" }) + end) + end) +end)