From f2f7cad98368380e25e41a2b2a8a7645df3c6a2d Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Tue, 7 Jan 2025 12:32:16 +0000 Subject: [PATCH] feat(command): add `uriencode` & `uriencodecomponents` sub commands --- lua/rest-nvim/commands.lua | 52 +++++++++++++++++++++++++++++++ lua/rest-nvim/utils.lua | 64 +++++++++++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/lua/rest-nvim/commands.lua b/lua/rest-nvim/commands.lua index 055e69fb..1bf98283 100644 --- a/lua/rest-nvim/commands.lua +++ b/lua/rest-nvim/commands.lua @@ -38,6 +38,14 @@ --- but directly insert curl command as a comment --- instead. --- +--- encodeuri Encodes selected URI text while preserving +--- characters that are part of the URI syntax. +--- +--- encodeuricomponents Encodes selected URI text while including +--- characters that are part of the URI syntax. +--- +--- decodeuri Decodes selected URI text. +--- ---NOTE: All `:Rest` commands opening new window support |command-modifiers|. ---For example, you can run `:hor Rest open` to open result pane in horizontal ---split or run `:tab Rest logs` to open logs file in a new tab. @@ -61,6 +69,7 @@ local function logger() return require("rest-nvim.logger") end local function parser() return require("rest-nvim.parser") end local function ui() return require("rest-nvim.ui.result") end local function config() return require("rest-nvim.config") end +local function utils() return require("rest-nvim.utils") end -- stylua: ignore end ---Open window based on command mods and return new window identifier @@ -90,6 +99,24 @@ local function open_result_ui(opts) vim.cmd.wincmd("p") end +local function get_selected(buf) + local _, srow, scol = unpack(vim.fn.getpos("'<")) + local _, erow, ecol = unpack(vim.fn.getpos("'>")) + return vim.fn.getregion({ buf, srow, scol, 0 }, { buf, erow, ecol, 0 }), + vim.fn.getregionpos({ buf, srow, scol, 0 }, { buf, erow, ecol, 0 }, { + selection = true + }) +end + +local function encode_selected(encode) + local texts, ranges = get_selected(0) + local range = ranges[1] + local rep = vim.tbl_map(encode, texts) + local _, srow, scol = unpack(range[1]) + local _, erow, ecol = unpack(range[2]) + vim.api.nvim_buf_set_text(0, srow - 1, scol - 1, erow - 1, ecol, rep) +end + ---@type table local rest_command_tbl = { open = { @@ -98,6 +125,30 @@ local rest_command_tbl = { ui().enter(winnr) end, }, + encodeuri = { + impl = function(_, opts) + if opts.range < 2 then + return + end + encode_selected(utils().encodeURI) + end, + }, + encodeuricomponents = { + impl = function(_, opts) + if opts.range < 2 then + return + end + encode_selected(utils().encodeURIComponents) + end + }, + decodeuri = { + impl = function(_, opts) + if opts.range < 2 then + return + end + encode_selected(utils().url_decode) + end, + }, run = { impl = function(args, opts) if vim.bo.filetype ~= "http" then @@ -315,6 +366,7 @@ function commands.setup() nargs = "+", desc = "Run your HTTP requests", bar = true, + range = true, complete = function(arg_lead, cmdline, _) local rest_commands = vim.tbl_keys(rest_command_tbl) local subcmd, subcmd_arg_lead = cmdline:match("Rest*%s(%S+)%s(.*)$") diff --git a/lua/rest-nvim/utils.lua b/lua/rest-nvim/utils.lua index e9cf85df..22238872 100644 --- a/lua/rest-nvim/utils.lua +++ b/lua/rest-nvim/utils.lua @@ -14,12 +14,19 @@ local utils = {} -- NOTE: vim.loop has been renamed to vim.uv in Neovim >= 0.10 and will be removed later local uv = vim.uv or vim.loop +-- TODO: refactor the escape algorithm +-- - grab string after `?` +-- - split them with `&`, `=` +-- - url-encode each uricomponents +-- - concat again to original text +-- - rename option name to `smart_encode_uri` + ---Encodes a string into its escaped hexadecimal representation ---@param str string Binary string to be encoded ---@param only_necessary? boolean Encode only necessary characters ---@return string function utils.escape(str, only_necessary) - local ignore = "%w%-%.%_%~%+" + local ignore = "%w%-%.%_%~%+%%" if only_necessary then ignore = ignore .. "%:%/%?%=%&%#%@" end @@ -34,6 +41,8 @@ function utils.escape(str, only_necessary) return encoded end +-- TODO: proper decodeURI from https://chromium.googlesource.com/v8/v8/+/3.26.4/src/uri.js#213 + ---@param str string function utils.url_decode(str) str = string.gsub(str, "%+", " ") @@ -43,6 +52,59 @@ function utils.url_decode(str) return str end +---@param uri string +---@param unescape fun(c:string):string +---@return string +local function encode(uri, unescape) + uri = uri:gsub(".", function(c) + c = unescape(c) and c or string.format("%%%02X", c:byte()) + return c + end) + return uri +end + +---@param c string +---@return boolean +local function is_alphanumeric(c) + -- stylua: ignore + return ('A' <= c and c <= 'Z') + or ('a' <= c and c <= 'z') + or ('0' <= c and c <= '9') +end + +---reference: https://chromium.googlesource.com/v8/v8/+/3.26.4/src/uri.js#329 +---@param uri string +---@return string +function utils.encodeURI(uri) + local function unescape_predicate(c) + return is_alphanumeric(c) + or c == "!" + or ("#" <= c and c <= "$") -- #$ + or ("&" <= c and c <= "/") -- &'()*+,-./ + or (":" <= c and c <= ";") -- :; + or c == "=" + or ("?" <= c and c <= "@") -- ?@ + or c == "_" + or c == "~" + end + return encode(uri, unescape_predicate) +end + +---reference: https://chromium.googlesource.com/v8/v8/+/3.26.4/src/uri.js#358 +---@param uri string +---@return string +function utils.encodeURIComponents(uri) + local function unescape_predicate(c) + return is_alphanumeric(c) + or c == "!" + or ("(" <= c and c <= "*") -- ()* + or ("-" <= c and c <= ".") -- -. + or c == "_" + or c == "~" + end + return encode(uri, unescape_predicate) +end + ---Check if a file exists in the given `path` ---@param path string file path ---@return boolean