diff --git a/README.md b/README.md index 087cce8c..db8e9995 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ lua require('Comment').setup() First you need to call the `setup()` method to create the default mappings. -> NOTE: If you are facing **Keybindings are mapped but they are not working** issue then please try [this](https://github.com/numToStr/Comment.nvim/issues/115#issuecomment-1032290098) +> **Note** - If you are facing **Keybindings are mapped but they are not working** issue then please try [this](https://github.com/numToStr/Comment.nvim/issues/115#issuecomment-1032290098) - Lua @@ -243,6 +243,8 @@ This plugin has native **treesitter** support for calculating `commentstring` wh For advance use cases, use [nvim-ts-context-commentstring](https://github.com/JoosepAlviste/nvim-ts-context-commentstring). See [`pre_hook`](#pre-hook) section for the integration. +> **Note** - This plugin does not depend on [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) however it is recommended in order to easily install tree-sitter parsers. + ### 🎣 Hooks @@ -355,6 +357,8 @@ vim.api.nvim_command('set commentstring=//%s') > Run `:h commentstring` for more help + + 2. You can also use this plugin interface to store both line and block commentstring for the filetype. You can treat this as a more powerful version of the `commentstring` ```lua @@ -394,7 +398,7 @@ Although, `Comment.nvim` supports neovim's `commentstring` but unfortunately it - [`pre_hook`](#hooks) - If a string is returned from this method then it will be used for commenting. -- [`ft_table`](#languages) - If the current filetype is found in the table, then the string there will be used. +- [`ft.lua`](#ft-lua) - If the current filetype is found in the table, then the string there will be used. - `commentstring` - Neovim's native commentstring for the filetype @@ -420,10 +424,10 @@ The following object is provided as an argument to `pre_hook` and `post_hook` fu ---Range of the selection that needs to be commented ---@class CommentRange ----@field srow number Starting row ----@field scol number Starting column ----@field erow number Ending row ----@field ecol number Ending column +---@field srow integer Starting row +---@field scol integer Starting column +---@field erow integer Ending row +---@field ecol integer Ending column ``` `CommentType`, `CommentMode` and `CommentMotion` all of them are exported from the plugin's utils for reuse diff --git a/lua/Comment/api.lua b/lua/Comment/api.lua index 9b1aeefc..f04b8fbb 100644 --- a/lua/Comment/api.lua +++ b/lua/Comment/api.lua @@ -197,7 +197,7 @@ end ---@param opmode OpMode ---@param cfg? CommentConfig function api.uncomment_current_blockwise_op(opmode, cfg) - Op.opfunc(opmode, cfg, U.cmode.uncomment, U.ctype.block, U.cmotion.line) + Op.opfunc(opmode, cfg or Config:get(), U.cmode.uncomment, U.ctype.block, U.cmotion.line) end --========================================== diff --git a/lua/Comment/config.lua b/lua/Comment/config.lua index 718e9d28..13614494 100644 --- a/lua/Comment/config.lua +++ b/lua/Comment/config.lua @@ -44,8 +44,8 @@ ---@private ---@class RootConfig ---@field config CommentConfig ----@field position number[] To be used to restore cursor position ----@field count number Helps with dot-repeat support for count prefix +---@field position integer[] To be used to restore cursor position +---@field count integer Helps with dot-repeat support for count prefix local Config = { state = {}, config = { @@ -88,6 +88,7 @@ function Config:get() return self.config end +---@export ft return setmetatable(Config, { __index = function(this, k) return this.state[k] diff --git a/lua/Comment/extra.lua b/lua/Comment/extra.lua index 6a2fff23..e22c969c 100644 --- a/lua/Comment/extra.lua +++ b/lua/Comment/extra.lua @@ -5,8 +5,17 @@ local A = vim.api local extra = {} ----@param count number Line index ----@param ctype CommentType +-- FIXME This prints `a` in i_CTRL-o +---Moves the cursor and enters INSERT mode +---@param row integer Starting row +---@param col integer Ending column +local function move_n_insert(row, col) + A.nvim_win_set_cursor(0, { row, col }) + A.nvim_feedkeys('a', 'ni', true) +end + +---@param count integer Line index +---@param ctype integer ---@param cfg CommentConfig local function ins_on_line(count, ctype, cfg) local row, col = unpack(A.nvim_win_get_cursor(0)) @@ -18,40 +27,39 @@ local function ins_on_line(count, ctype, cfg) ctype = ctype, range = { srow = row, scol = col, erow = row, ecol = col }, } - local lcs, rcs = U.parse_cstr(cfg, ctx) + local srow = row + count + local lcs, rcs = U.parse_cstr(cfg, ctx) local line = A.nvim_get_current_line() - local indent = U.grab_indent(line) - local padding = U.get_padding(cfg.padding) + local indent = U.indent_len(line) + local padding = U.get_pad(cfg.padding) -- We need RHS of cstr, if we are doing block comments or if RHS exists -- because even in line comment RHS do exists for some filetypes like jsx_element, ocaml - local if_rcs = (ctype == U.ctype.block or rcs) and padding .. rcs or '' + local if_rcs = U.is_empty(rcs) and rcs or padding .. rcs - local srow = row + count - local ll = indent .. lcs .. padding - A.nvim_buf_set_lines(0, srow, srow, false, { ll .. if_rcs }) - local erow, ecol = srow + 1, #ll - 1 - U.move_n_insert(erow, ecol) + A.nvim_buf_set_lines(0, srow, srow, false, { table.concat({ string.rep(' ', indent), lcs, padding, if_rcs }) }) + + move_n_insert(srow + 1, indent + #lcs + #padding - 1) U.is_fn(cfg.post_hook, ctx) end ---Add a comment below the current line and goes to INSERT mode ----@param ctype CommentType +---@param ctype integer See |comment.utils.ctype| ---@param cfg CommentConfig function extra.insert_below(ctype, cfg) ins_on_line(0, ctype, cfg) end ---Add a comment above the current line and goes to INSERT mode ----@param ctype CommentType +---@param ctype integer See |comment.utils.ctype| ---@param cfg CommentConfig function extra.insert_above(ctype, cfg) ins_on_line(-1, ctype, cfg) end ---Add a comment at the end of current line and goes to INSERT mode ----@param ctype CommentType +---@param ctype integer See |comment.utils.ctype| ---@param cfg CommentConfig function extra.insert_eol(ctype, cfg) local srow, scol = unpack(A.nvim_win_get_cursor(0)) @@ -66,16 +74,16 @@ function extra.insert_eol(ctype, cfg) local lcs, rcs = U.parse_cstr(cfg, ctx) local line = A.nvim_get_current_line() - local padding = U.get_padding(cfg.padding) + local padding = U.get_pad(cfg.padding) -- We need RHS of cstr, if we are doing block comments or if RHS exists -- because even in line comment RHS do exists for some filetypes like jsx_element, ocaml - local if_rcs = rcs and padding .. rcs or '' + local if_rcs = U.is_empty(rcs) and rcs or padding .. rcs local ecol if U.is_empty(line) then -- If line is empty, start comment at the correct indentation level - A.nvim_buf_set_lines(0, srow - 1, srow, false, { lcs .. padding .. if_rcs }) + A.nvim_set_current_line(lcs .. padding .. if_rcs) A.nvim_command('normal! ==') ecol = #A.nvim_get_current_line() - #if_rcs - 1 else @@ -84,12 +92,11 @@ function extra.insert_eol(ctype, cfg) -- 2. Other than that, I am assuming that the users wants a space b/w the end of line and start of the comment local space = vim.bo.filetype == 'python' and ' ' or ' ' local ll = line .. space .. lcs .. padding - + A.nvim_set_current_line(ll .. if_rcs) ecol = #ll - 1 - A.nvim_buf_set_lines(0, srow - 1, srow, false, { ll .. if_rcs }) end - U.move_n_insert(srow, ecol) + move_n_insert(srow, ecol) U.is_fn(cfg.post_hook, ctx) end diff --git a/lua/Comment/ft.lua b/lua/Comment/ft.lua index c5bb5e87..09b40ef2 100644 --- a/lua/Comment/ft.lua +++ b/lua/Comment/ft.lua @@ -19,7 +19,8 @@ local M = { } ---Lang table that contains commentstring (linewise/blockwise) for mutliple filetypes ----@type table { filetype = { linewise, blockwise } } +---Structure = { filetype = { linewise, blockwise } } +---@type table local L = { arduino = { M.cxx_l, M.cxx_b }, bash = { M.hash }, @@ -115,7 +116,7 @@ end ---Get a commentstring from the filtype list ---@param lang CommentLang ----@param ctype CommentType +---@param ctype integer See |comment.utils.ctype| ---@return string function ft.get(lang, ctype) local l = ft.lang(lang) @@ -124,16 +125,16 @@ end ---Get the commentstring(s) from the filtype list ---@param lang CommentLang ----@return string +---@return string[] function ft.lang(lang) return L[lang] end ---Get the tree in range by walking the whole tree recursively ----NOTE: This ignores `comment` parser as this is useless +---NOTE: This ignores `comment` parser as this is not needed ---@param tree userdata Tree to be walked ----@param range number[] Range to check for ----@return userdata +---@param range integer[] Range to check - {start_line, s_col, end_line, end_col} +---@return userdata _ Returns a 'treesitter-languagetree' function ft.contains(tree, range) for lang, child in pairs(tree:children()) do if lang ~= 'comment' and child:contains(range) then @@ -166,6 +167,7 @@ function ft.calculate(ctx) return ft.get(lang, ctx.ctype) or default end +---@export ft return setmetatable(ft, { __newindex = function(this, k, v) this.set(k, v) diff --git a/lua/Comment/opfunc.lua b/lua/Comment/opfunc.lua index 099ed3ca..3a1302d3 100644 --- a/lua/Comment/opfunc.lua +++ b/lua/Comment/opfunc.lua @@ -6,18 +6,24 @@ local A = vim.api local Op = {} ----@alias OpMode 'line'|'char'|'v'|'V' Vim operator-mode motions. Read `:h map-operator` +---Vim operator-mode motions. +---Read `:h :map-operator` +---@alias OpMode +---| 'line' # Vertical motion +---| 'char' # Horizontal motion +---| 'v' # Visual Block motion +---| 'V' # Visual Line motion ---@class CommentCtx Comment context ----@field ctype CommentType ----@field cmode CommentMode ----@field cmotion CommentMotion +---@field ctype integer See |comment.utils.ctype| +---@field cmode integer See |comment.utils.cmode| +---@field cmotion integer See |comment.utils.cmotion| ---@field range CommentRange ---@class OpFnParams Operator-mode function parameters ---@field cfg CommentConfig ----@field cmode CommentMode ----@field lines table List of lines +---@field cmode integer See |comment.utils.cmode| +---@field lines string[] List of lines ---@field rcs string RHS of commentstring ---@field lcs string LHS of commentstring ---@field range CommentRange @@ -26,34 +32,23 @@ local Op = {} ---This function contains the core logic for comment/uncomment ---@param opmode OpMode ---@param cfg CommentConfig ----@param cmode CommentMode ----@param ctype CommentType ----@param cmotion CommentMotion +---@param cmode integer See |comment.utils.cmode| +---@param ctype integer See |comment.utils.ctype| +---@param cmotion integer See |comment.utils.cmotion| function Op.opfunc(opmode, cfg, cmode, ctype, cmotion) - -- comment/uncomment logic - -- - -- 1. type == line - -- * decide whether to comment or not, if all the lines are commented then uncomment otherwise comment - -- * also, store the minimum indent from all the lines (exclude empty line) - -- * if comment the line, use cstr LHS and also considering the min indent - -- * if uncomment the line, remove cstr LHS from lines - -- * update the lines - -- 2. type == block - -- * check if the first and last is commented or not with cstr LHS and RHS respectively. - -- * if both lines commented - -- - remove cstr LHS from the first line - -- - remove cstr RHS to end of the last line - -- * if both lines uncommented - -- - add cstr LHS after the leading whitespace and before the first char of the first line - -- - add cstr RHS to end of the last line - -- * update the lines - cmotion = cmotion == U.cmotion._ and U.cmotion[opmode] or cmotion local range = U.get_region(opmode) - local same_line = range.srow == range.erow - local partial_block = cmotion == U.cmotion.char or cmotion == U.cmotion.v - local block_x = partial_block and same_line + local partial = cmotion == U.cmotion.char or cmotion == U.cmotion.v + local block_x = partial and range.srow == range.erow + + local lines = U.get_lines(range) + + -- sometimes there might be a case when there are no lines + -- like, executing a text object returns nothing + if U.is_empty(lines) then + return + end ---@type CommentCtx local ctx = { @@ -64,22 +59,19 @@ function Op.opfunc(opmode, cfg, cmode, ctype, cmotion) } local lcs, rcs = U.parse_cstr(cfg, ctx) - local lines = U.get_lines(range) ---@type OpFnParams local params = { cfg = cfg, - cmode = cmode, lines = lines, lcs = lcs, rcs = rcs, + cmode = cmode, range = range, } - if block_x then - ctx.cmode = Op.blockwise_x(params) - elseif ctype == U.ctype.block and not same_line then - ctx.cmode = Op.blockwise(params, partial_block) + if block_x or ctype == U.ctype.block then + ctx.cmode = Op.blockwise(params, partial) else ctx.cmode = Op.linewise(params) end @@ -99,152 +91,106 @@ end ---Line commenting ---@param param OpFnParams ----@return integer CMode +---@return integer _ Returns a calculated comment mode function Op.linewise(param) - local lcs_esc, rcs_esc = U.escape(param.lcs), U.escape(param.rcs) local pattern = U.is_fn(param.cfg.ignore) - local padding, pp = U.get_padding(param.cfg.padding) - local is_commented = U.is_commented(lcs_esc, rcs_esc, pp) + local padding = U.is_fn(param.cfg.padding) + local check = U.is_commented(param.lcs, param.rcs, padding) -- While commenting a region, there could be lines being both commented and non-commented -- So, if any line is uncommented then we should comment the whole block or vise-versa local cmode = U.cmode.uncomment - -- When commenting multiple line, it is to be expected that indentation should be preserved - -- So, When looping over multiple lines we need to store the indentation of the mininum length (except empty line) - -- Which will be used to semantically comment rest of the lines - local min_indent = nil + ---When commenting multiple line, it is to be expected that indentation should be preserved + ---So, When looping over multiple lines we need to store the indentation of the mininum length (except empty line) + ---Which will be used to semantically comment rest of the lines + ---@type integer + local min_indent = -1 - -- If the given comde is uncomment then we actually don't want to compute the cmode or min_indent + -- If the given cmode is uncomment then we actually don't want to compute the cmode or min_indent if param.cmode ~= U.cmode.uncomment then for _, line in ipairs(param.lines) do -- I wish lua had `continue` statement [sad noises] if not U.ignore(line, pattern) then - if cmode == U.cmode.uncomment and param.cmode == U.cmode.toggle then - local is_cmt = is_commented(line) - if not is_cmt then - cmode = U.cmode.comment - end + if cmode == U.cmode.uncomment and param.cmode == U.cmode.toggle and (not check(line)) then + cmode = U.cmode.comment end -- If local `cmode` == comment or the given cmode ~= uncomment, then only calculate min_indent -- As calculating min_indent only makes sense when we actually want to comment the lines if not U.is_empty(line) and (cmode == U.cmode.comment or param.cmode == U.cmode.comment) then - local indent = U.grab_indent(line) - if not min_indent or #min_indent > #indent then - min_indent = indent + local len = U.indent_len(line) + if min_indent == -1 or min_indent > len then + min_indent = len end end end end end - -- If the comment mode given is not toggle than force that mode - if param.cmode ~= U.cmode.toggle then - cmode = param.cmode - end - - local uncomment = cmode == U.cmode.uncomment - for i, line in ipairs(param.lines) do - if not U.ignore(line, pattern) then - if uncomment then - param.lines[i] = U.uncomment_str(line, lcs_esc, rcs_esc, pp) - else - param.lines[i] = U.comment_str(line, param.lcs, param.rcs, padding, min_indent) + if cmode == U.cmode.uncomment then + local uncomment = U.uncommenter(param.lcs, param.rcs, padding) + for i, line in ipairs(param.lines) do + if not U.ignore(line, pattern) then + param.lines[i] = uncomment(line) + end + end + else + local comment = U.commenter(param.lcs, param.rcs, padding, min_indent) + for i, line in ipairs(param.lines) do + if not U.ignore(line, pattern) then + param.lines[i] = comment(line) end end end + A.nvim_buf_set_lines(0, param.range.srow - 1, param.range.erow, false, param.lines) return cmode end ----Full/Partial Block commenting +---Full/Partial/Current-Line Block commenting ---@param param OpFnParams ---@param partial? boolean Comment the partial region (visual mode) ----@return integer CMode +---@return integer _ Returns a calculated comment mode function Op.blockwise(param, partial) - -- Block wise, only when there are more than 1 lines - local sln, eln = param.lines[1], param.lines[#param.lines] - local lcs_esc, rcs_esc = U.escape(param.lcs), U.escape(param.rcs) - local padding, pp = U.get_padding(param.cfg.padding) - - -- These string should be checked for comment/uncomment - local sln_check, eln_check - if partial then - sln_check = sln:sub(param.range.scol + 1) - eln_check = eln:sub(0, param.range.ecol + 1) - else - sln_check, eln_check = sln, eln - end - - -- If given mode is toggle then determine whether to comment or not - local cmode - if param.cmode == U.cmode.toggle then - local s_cmt = U.is_commented(lcs_esc, nil, pp)(sln_check) - local e_cmt = U.is_commented(nil, rcs_esc, pp)(eln_check) - cmode = (s_cmt and e_cmt) and U.cmode.uncomment or U.cmode.comment - else - cmode = param.cmode - end + local is_x = #param.lines == 1 -- current-line blockwise + local lines = is_x and param.lines[1] or param.lines - local l1, l2 + local padding = U.is_fn(param.cfg.padding) - if cmode == U.cmode.uncomment then - l1 = U.uncomment_str(sln_check, lcs_esc, nil, pp) - l2 = U.uncomment_str(eln_check, nil, rcs_esc, pp) - else - l1 = U.comment_str(sln_check, param.lcs, nil, padding) - l2 = U.comment_str(eln_check, nil, param.rcs, padding) + local scol, ecol = nil, nil + if is_x or partial then + scol, ecol = param.range.scol, param.range.ecol end - if partial then - l1 = sln:sub(0, param.range.scol) .. l1 - l2 = l2 .. eln:sub(param.range.ecol + 2) + -- If given mode is toggle then determine whether to comment or not + local cmode = param.cmode + if cmode == U.cmode.toggle then + local is_cmt = U.is_commented(param.lcs, param.rcs, padding, scol, ecol)(lines) + cmode = is_cmt and U.cmode.uncomment or U.cmode.comment end - A.nvim_buf_set_lines(0, param.range.srow - 1, param.range.srow, false, { l1 }) - A.nvim_buf_set_lines(0, param.range.erow - 1, param.range.erow, false, { l2 }) - - return cmode -end - ----Block (left-right motion) commenting ----@param param OpFnParams ----@return integer CMode -function Op.blockwise_x(param) - local line = param.lines[1] - local first = line:sub(0, param.range.scol) - local mid = line:sub(param.range.scol + 1, param.range.ecol + 1) - local last = line:sub(param.range.ecol + 2) - - local padding, pp = U.get_padding(param.cfg.padding) - - local yes, _, stripped = U.is_commented(U.escape(param.lcs), U.escape(param.rcs), pp)(mid) - - local cmode - if param.cmode == U.cmode.toggle then - cmode = yes and U.cmode.uncomment or U.cmode.comment + if cmode == U.cmode.uncomment then + lines = U.uncommenter(param.lcs, param.rcs, padding, scol, ecol)(lines) else - cmode = param.cmode + lines = U.commenter(param.lcs, param.rcs, padding, scol, ecol)(lines) end - if cmode == U.cmode.uncomment then - A.nvim_set_current_line(first .. (stripped or mid) .. last) + if is_x then + A.nvim_set_current_line(lines) else - local lcs = param.lcs and param.lcs .. padding or '' - local rcs = param.rcs and padding .. param.rcs or '' - A.nvim_set_current_line(first .. lcs .. mid .. rcs .. last) + A.nvim_buf_set_lines(0, param.range.srow - 1, param.range.erow, false, lines) end return cmode end ----Toggle line comment with count i.e vim.v.count ----Example: `10gl` will comment 10 lines +---Line commenting with count i.e vim.v.count +---Example: '10gl' will comment 10 lines ---@param count integer Number of lines ---@param cfg CommentConfig ----@param ctype CommentType +---@param ctype integer See |comment.utils.ctype| function Op.count(count, cfg, ctype) local lines, range = U.get_count_lines(count) diff --git a/lua/Comment/utils.lua b/lua/Comment/utils.lua index 73344589..39d57747 100644 --- a/lua/Comment/utils.lua +++ b/lua/Comment/utils.lua @@ -7,15 +7,15 @@ local U = {} ---@alias CommentLines string[] List of lines inside the start and end index ---@class CommentRange Range of the selection that needs to be commented ----@field srow number Starting row ----@field scol number Starting column ----@field erow number Ending row ----@field ecol number Ending column +---@field srow integer Starting row +---@field scol integer Starting column +---@field erow integer Ending row +---@field ecol integer Ending column ---@class CommentMode Comment modes - Can be manual or computed via operator-mode ----@field toggle number Toggle action ----@field comment number Comment action ----@field uncomment number Uncomment action +---@field toggle integer Toggle action +---@field comment integer Comment action +---@field uncomment integer Uncomment action ---An object containing comment modes ---@type CommentMode @@ -26,8 +26,8 @@ U.cmode = { } ---@class CommentType Comment types ----@field line number Use linewise commentstring ----@field block number Use blockwise commentstring +---@field line integer Use linewise commentstring +---@field block integer Use blockwise commentstring ---An object containing comment types ---@type CommentType @@ -37,12 +37,12 @@ U.ctype = { } ---@class CommentMotion Comment motion types ----@field private _ number Compute from vim mode. See |OpMode| ----@field line number Line motion (ie. `gc2j`) ----@field char number Character/left-right motion (ie. `gc2j`) ----@field block number Visual operator-pending motion ----@field v number Visual motion ----@field V number Visual-line motion +---@field private _ integer Compute from vim mode. See |OpMode| +---@field line integer Line motion (ie. 'gc2j') +---@field char integer Character/left-right motion (ie. 'gc2w') +---@field block integer Visual operator-pending motion +---@field v integer Visual motion (ie. 'v3jgc') +---@field V integer Visual-line motion (ie. 'V10kgc') ---An object containing comment motions ---@type CommentMotion @@ -55,53 +55,43 @@ U.cmotion = { V = 5, } +---@private ---Check whether the line is empty ----@param ln string +---@param iter string|string[] ---@return boolean -function U.is_empty(ln) - return #ln == 0 +function U.is_empty(iter) + return #iter == 0 end ----Takes out the leading indent from lines ----@param s string ----@return string string Indent chars ----@return number string Length of the indent chars -function U.grab_indent(s) - local _, len, indent = s:find('^(%s*)') - return indent, len -end - ----Helper to get padding character and regex pattern ----NOTE: Use a function for conditional padding ----@param flag boolean|fun():boolean ----@return string string Padding chars ----@return string string Padding pattern -function U.get_padding(flag) - if not U.is_fn(flag) then - return '', '' - end - return ' ', '%s?' +---@private +---Get the length of the indentation +---@param str string +---@return integer integer Length of indent chars +function U.indent_len(str) + local _, len = string.find(str, '^%s*') + return len end --- FIXME This prints `a` in i_CTRL-o ----Moves the cursor and enters INSERT mode ----@param row number Starting row ----@param col number Ending column -function U.move_n_insert(row, col) - A.nvim_win_set_cursor(0, { row, col }) - A.nvim_feedkeys('a', 'ni', true) +---@private +---Helper to get padding character +---@param flag boolean +---@return string string +function U.get_pad(flag) + return flag and ' ' or '' end ----Convert the string to a escaped string, if given ----@param str string ----@return string|boolean -function U.escape(str) - return str and vim.pesc(str) +---@private +---Helper to get padding pattern +---@param flag boolean +---@return string string +function U.get_padpat(flag) + return flag and '%s?' or '' end +---@private ---Call a function if exists ----@param fn function Wanna be function ----@return boolean|string +---@param fn unknown|fun():unknown Wanna be function +---@return unknown function U.is_fn(fn, ...) if type(fn) == 'function' then return fn(...) @@ -109,12 +99,13 @@ function U.is_fn(fn, ...) return fn end +---@private ---Check if the given line is ignored or not with the given pattern ---@param ln string Line to be ignored ---@param pat string Lua regex ---@return boolean function U.ignore(ln, pat) - return pat and ln:find(pat) ~= nil + return pat and string.find(ln, pat) ~= nil end ---Get region for line movement or visual selection @@ -127,26 +118,18 @@ function U.get_region(opmode) return { srow = row, scol = 0, erow = row, ecol = 0 } end - local m = A.nvim_buf_get_mark - local buf = 0 - local sln, eln - - if string.match(opmode, '[vV]') then - sln, eln = m(buf, '<'), m(buf, '>') - else - sln, eln = m(buf, '['), m(buf, ']') - end + local marks = string.match(opmode, '[vV]') and { '<', '>' } or { '[', ']' } + local sln, eln = A.nvim_buf_get_mark(0, marks[1]), A.nvim_buf_get_mark(0, marks[2]) return { srow = sln[1], scol = sln[2], erow = eln[1], ecol = eln[2] } end ---Get lines from the current position to the given count ----@param count number +---@param count integer Probably 'vim.v.count' ---@return CommentLines ---@return CommentRange function U.get_count_lines(count) - local pos = A.nvim_win_get_cursor(0) - local srow = pos[1] + local srow = unpack(A.nvim_win_get_cursor(0)) local erow = (srow + count) - 1 local lines = A.nvim_buf_get_lines(0, srow - 1, erow, false) @@ -166,28 +149,24 @@ function U.get_lines(range) end ---Validates and unwraps the given commentstring ----@param cstr string ----@return string|boolean ----@return string|boolean +---@param cstr string See 'commentstring' +---@return string string Left side of the commentstring +---@return string string Right side of the commentstring function U.unwrap_cstr(cstr) - local lcs, rcs = cstr:match('(.*)%%s(.*)') + local left, right = string.match(cstr, '(.*)%%s(.*)') - if not (lcs or rcs) then - return vim.notify( - ("[Comment] Invalid commentstring - %q. Run ':h commentstring' for help."):format(cstr), - vim.log.levels.ERROR - ) - end + assert( + (left or right), + string.format("[Comment] Invalid commentstring - %q. Run ':h commentstring' for help.", cstr) + ) - -- Return false if a part is empty, otherwise trim it - -- Bcz it is better to deal with boolean rather than checking empty string length everywhere - return not U.is_empty(lcs) and vim.trim(lcs), not U.is_empty(rcs) and vim.trim(rcs) + return vim.trim(left), vim.trim(right) end ----Unwraps the commentstring by taking it from the following places ---- 1. pre_hook (optionally a string can be returned) ---- 2. ft_table (extra commentstring table in the plugin) ---- 3. commentstring (already set or added in pre_hook) +---Parses commentstring from the following places in the respective order +--- 1. pre_hook - commentstring returned from the function +--- 2. ft.lua - commentstring table bundled with the plugin +--- 3. commentstring - Neovim's native. See 'commentstring' ---@param cfg CommentConfig ---@param ctx CommentCtx ---@return string string Left side of the commentstring @@ -203,62 +182,170 @@ function U.parse_cstr(cfg, ctx) return U.unwrap_cstr(cstr) end ----Converts the given string into a commented string ----@param ln string String that needs to be commented ----@param lcs string Left side of the commentstring ----@param rcs string Right side of the commentstring ----@param padding string Padding chars b/w comment and line ----@param min_indent? string Minimum indent to use with multiple lines ----@return string string Commented string -function U.comment_str(ln, lcs, rcs, padding, min_indent) - if U.is_empty(ln) then - return (min_indent or '') .. ((lcs or '') .. (rcs or '')) - end - - local indent, chars = ln:match('^(%s*)(.*)') - - local lcs_new = lcs and lcs .. padding or '' - local rcs_new = rcs and padding .. rcs or '' +---Returns a closure which is used to do comments +--- +---If given {string[]} to the closure then it will do blockwise comment +---else linewise comment will be done with the given {string} +---@param left string Left side of the commentstring +---@param right string Right side of the commentstring +---@param padding boolean Is padding enabled? +---@param scol? integer Starting column +---@param ecol? integer Ending column +---@return fun(line:string|string[]):string +function U.commenter(left, right, padding, scol, ecol) + local pad = U.get_pad(padding) + local ll = U.is_empty(left) and left or (left .. pad) + local rr = U.is_empty(right) and right or (pad .. right) + local empty = string.rep(' ', scol or 0) .. left .. right + local is_lw = scol and not ecol - local pos = #(min_indent or indent) - local l_indent = indent:sub(0, pos) .. lcs_new .. indent:sub(pos + 1) - - return l_indent .. chars .. rcs_new + return function(line) + ------------------ + -- for linewise -- + ------------------ + if is_lw then + if U.is_empty(line) then + return empty + end + -- line == 0 -> start from 0 col + if scol == 0 then + return (ll .. line .. rr) + end + local first = string.sub(line, 0, scol) + local last = string.sub(line, scol + 1, -1) + return table.concat({ first, ll, last, rr }) + end + + ------------------- + -- for blockwise -- + ------------------- + if type(line) == 'table' then + local first, last = line[1], line[#line] + -- If both columns are given then we can assume it's a partial block + if scol and ecol then + local sfirst = string.sub(first, 0, scol) + local slast = string.sub(first, scol + 1, -1) + local efirst = string.sub(last, 0, ecol + 1) + local elast = string.sub(last, ecol + 2, -1) + line[1] = sfirst .. ll .. slast + line[#line] = efirst .. rr .. elast + else + line[1] = U.is_empty(first) and left or string.gsub(first, '^(%s*)', '%1' .. ll) + line[#line] = U.is_empty(last) and right or (last .. rr) + end + return line + end + + -------------------------------- + -- for current-line blockwise -- + -------------------------------- + local first = string.sub(line, 0, scol) + local mid = string.sub(line, scol + 1, ecol + 1) + local last = string.sub(line, ecol + 2, -1) + return table.concat({ first, ll, mid, rr, last }) + end end ----Converts the given string into a uncommented string ----@param ln string Line that needs to be uncommented ----@param lcs_esc string (Escaped) Left side of the commentstring ----@param rcs_esc string (Escaped) Right side of the commentstring ----@param pp string Padding pattern. See |U.get_padding| ----@return string string Uncommented string -function U.uncomment_str(ln, lcs_esc, rcs_esc, pp) - local ll = lcs_esc and lcs_esc .. pp or '' - local rr = rcs_esc and rcs_esc .. '$?' or '' - - local indent, chars = ln:match('(%s*)' .. ll .. '(.*)' .. rr) - - -- If the line (after cstring) is empty then just return '' - -- bcz when uncommenting multiline this also doesn't preserve leading whitespace as the line was previously empty - if U.is_empty(chars) then - return '' - end +---Returns a closure which is used to uncomment a line +--- +---If given {string[]} to the closure then it will block uncomment +---else linewise uncomment will be done with the given {string} +---@param left string Left side of the commentstring +---@param right string Right side of the commentstring +---@param padding boolean Is padding enabled? +---@param scol? integer Starting column +---@param ecol? integer Ending column +---@return fun(line:string|string[]):string +function U.uncommenter(left, right, padding, scol, ecol) + local pp, plen = U.get_padpat(padding), padding and 1 or 0 + local left_len, right_len = #left + plen, #right + plen + local ll = U.is_empty(left) and left or vim.pesc(left) .. pp + local rr = U.is_empty(right) and right or pp .. vim.pesc(right) + local is_lw = not (scol and scol) + local pattern = is_lw and '^(%s*)' .. ll .. '(.-)' .. rr .. '$' - -- When padding is enabled then trim one trailing space char - return indent .. chars:gsub(pp .. '$', '') + return function(line) + ------------------- + -- for blockwise -- + ------------------- + if type(line) == 'table' then + local first, last = line[1], line[#line] + -- If both columns are given then we can assume it's a partial block + if scol and ecol then + local sfirst = string.sub(first, 0, scol) + local slast = string.sub(first, scol + left_len + 1, -1) + local efirst = string.sub(last, 0, ecol - right_len + 1) + local elast = string.sub(last, ecol + 2, -1) + line[1] = sfirst .. slast + line[#line] = efirst .. elast + else + line[1] = string.gsub(first, '^(%s*)' .. ll, '%1') + line[#line] = string.gsub(last, rr .. '$', '') + end + return line + end + + ------------------ + -- for linewise -- + ------------------ + if is_lw then + local a, b, c = string.match(line, pattern) + -- If there is nothing after LHS then just return '' + -- bcz the line previously (before comment) was empty + return U.is_empty(b) and b or a .. b .. (c or '') + end + + -------------------------------- + -- for current-line blockwise -- + -------------------------------- + local first = string.sub(line, 0, scol) + local mid = string.sub(line, scol + left_len + 1, ecol - right_len + 1) + local last = string.sub(line, ecol + 2, -1) + return first .. mid .. last + end end ---Check if the given string is commented or not ----@param lcs_esc string (Escaped) Left side of the commentstring ----@param rcs_esc string (Escaped) Right side of the commentstring ----@param pp string Padding pattern. See |U.get_padding| ----@return fun(line:string):boolean -function U.is_commented(lcs_esc, rcs_esc, pp) - local ll = lcs_esc and '^%s*' .. lcs_esc .. pp or '' - local rr = rcs_esc and pp .. rcs_esc .. '$' or '' +--- +---If given {string[]} to the closure, it will check the first and last line +---with LHS and RHS of commentstring respectively else it will check the given +---line with LHS and RHS (if given) of the commenstring +---@param left string Left side of the commentstring +---@param right string Right side of the commentstring +---@param padding boolean Is padding enabled? +---@param scol? integer Starting column +---@param ecol? integer Ending column +---@return fun(line:string|string[]):boolean +function U.is_commented(left, right, padding, scol, ecol) + local pp = U.get_padpat(padding) + local ll = U.is_empty(left) and left or '^%s*' .. vim.pesc(left) .. pp + local rr = U.is_empty(right) and right or pp .. vim.pesc(right) .. '$' + local pattern = ll .. '.-' .. rr + local is_full = scol == nil or ecol == nil return function(line) - return line:find(ll .. '(.-)' .. rr) + ------------------- + -- for blockwise -- + ------------------- + if type(line) == 'table' then + local first, last = line[1], line[#line] + if is_full then + return string.find(first, ll) and string.find(last, rr) + end + return string.find(string.sub(first, scol + 1, -1), ll) and string.find(string.sub(last, 0, ecol + 1), rr) + end + + ------------------ + -- for linewise -- + ------------------ + if is_full then + return string.find(line, pattern) + end + + -------------------------------- + -- for current-line blockwise -- + -------------------------------- + return string.find(string.sub(line, scol + 1, ecol + 1), pattern) end end