diff --git a/doc/luasnip.txt b/doc/luasnip.txt index b9a8376e..ab6ec9b4 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2026 January 19 +*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2026 January 23 ============================================================================== Table of Contents *luasnip-table-of-contents* diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 8f8a29ec..83a3c1e5 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -1,14 +1,15 @@ -- Some string processing utility functions local M = {} ----In-place dedents strings in lines. ----@param lines string[]. +--- In-place dedents strings in lines. +---@param lines string[] local function dedent(lines) if #lines > 0 then local ind_size = math.huge for i, _ in ipairs(lines) do local i1, i2 = lines[i]:find("^%s*[^%s]") if i1 and i2 < ind_size then + ---@cast i2 -nil ind_size = i2 end end @@ -18,7 +19,7 @@ local function dedent(lines) end end ----Convert string `from` to unit indent +--- In-place convert string `from` to unit indent in lines. ---@param lines string[] ---@param from string ---@param unit_indent string @@ -51,15 +52,17 @@ local function convert_indent(lines, from, unit_indent) end end ----Applies opts to lines. ----lines is modified in-place. ----@param lines string[]. ----@param options table, required, can have values: ---- - trim_empty: removes empty first and last lines. ---- - dedent: removes indent common to all lines. ---- - indent_string: an unit indent at beginning of each line after applying `dedent`, default empty string (disabled) -function M.process_multiline(lines, options) - if options.trim_empty then +---@class LuaSnip.Opts.Str.MultilineProcess +---@field trim_empty? boolean Whether to remove whitespace-only first/last lines +---@field dedent? boolean Whether to remove all common indent in `str`. +---@field indent_string? string When set, will convert `indent_string` at +--- beginning of each line to unit indent ('\t') after applying `dedent`. + +--- In-place process lines with given opts. +---@param lines string[] +---@param opts LuaSnip.Opts.Str.MultilineProcess +function M.process_multiline(lines, opts) + if opts.trim_empty then if lines[1]:match("^%s*$") then table.remove(lines, 1) end @@ -68,21 +71,28 @@ function M.process_multiline(lines, options) end end - if options.dedent then + if opts.dedent then dedent(lines) end - if options.indent_string and #options.indent_string > 0 then - convert_indent(lines, options.indent_string, "\t") + if opts.indent_string and #opts.indent_string > 0 then + convert_indent(lines, opts.indent_string, "\t") end end +--- Remove common indentation from the given string. +---@param s string +---@return string function M.dedent(s) local lst = vim.split(s, "\n") dedent(lst) return table.concat(lst, "\n") end +--- Convert string `indent_string` to unit indent (\t) in given string. +---@param s string +---@param indent_string string +---@return string function M.convert_indent(s, indent_string) local lst = vim.split(s, "\n") convert_indent(lst, indent_string, "\t") @@ -101,11 +111,12 @@ local function is_escaped(s, indx) return count % 2 == 1 end ---- return position of next (relative to `start`) unescaped occurence of +--- Return position of next (relative to `start`) unescaped occurence of --- `target` in `s`. ---@param s string ---@param target string ----@param start number +---@param start integer +---@return integer? local function find_next_unescaped(s, target, start) while true do local from = s:find(target, start, true) @@ -125,7 +136,7 @@ end ---@param s string ---@param left string ---@param right string ----@return function: iterator, returns pairs from,to. +---@return fun(): (integer?, integer?) _ An iterator returning pairs from,to. function M.unescaped_pairs(s, left, right) local search_from = 1 @@ -144,29 +155,26 @@ function M.unescaped_pairs(s, left, right) end end -function M.aupatescape(s) - if vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 then - -- windows: replace \ with / for au-pattern. - s, _ = s:gsub("\\", "/") - end - local escaped, _ = s:gsub(",", "\\,") - return vim.fn.fnameescape(escaped) -end - +--- Sanitize the given string (e.g. \r) +---@param str string +---@return string function M.sanitize(str) - return str:gsub("%\r", "") + local ret = str:gsub("%\r", "") + return ret -- note: local var required for correct typing end --- requires that from and to are within the region of str. --- str is treated as a 0,0-indexed, and the character at `to` is excluded from --- the result. --- `from` may not be before `to`. -function M.multiline_substr(str, from, to) +--- Extract a rectangular block of lines in a multiline string area. +---@param lines string[] +---@param from LuaSnip.RawPos00 From this position, MUST be within `lines`. +---@param to LuaSnip.RawPos00 To this position (excluded), MUST be within `lines` +--- and before `from`. +---@return string[] +function M.multiline_substr(lines, from, to) local res = {} -- include all rows for i = from[1], to[1] do - table.insert(res, str[i + 1]) + table.insert(res, lines[i + 1]) end -- trim text before from and after to. @@ -179,35 +187,33 @@ function M.multiline_substr(str, from, to) return res end -function M.multiline_upper(str) - for i, s in ipairs(str) do - str[i] = s:upper() - end -end -function M.multiline_lower(str) - for i, s in ipairs(str) do - str[i] = s:lower() +--- In-place uppercase all text in `lines` +---@param lines string[] +function M.multiline_upper(lines) + for i, s in ipairs(lines) do + lines[i] = s:upper() end end --- modifies strmod -function M.multiline_append(strmod, strappend) - strmod[#strmod] = strmod[#strmod] .. strappend[1] - for i = 2, #strappend do - table.insert(strmod, strappend[i]) +--- In-place lowercase all text in `lines` +---@param lines string[] +function M.multiline_lower(lines) + for i, s in ipairs(lines) do + lines[i] = s:lower() end end --- turn a row+col-offset for a multiline-string (string[]) (where the column is --- given in bytes and 0-based) into an offset (in bytes, 1-based) for --- the \n-concatenated version of that string. +--- Turns a row+col-offset for a multiline-string (string[]) (where the column is +--- given in bytes and 0-based) into an offset (in bytes, 1-based) for +--- the \n-concatenated version of that string. --- ----@param str string[], a multiline string ----@param pos LuaSnip.ApiPosition, an api-position relative to the start of str. -function M.multiline_to_byte_offset(str, pos) - if pos[1] < 0 or pos[1] + 1 > #str or pos[2] < 0 then +---@param lines string[] a multiline string +---@param pos LuaSnip.RawPos00 position relative to the start of str. +---@return integer? +function M.multiline_to_byte_offset(lines, pos) + if pos[1] < 0 or pos[1] + 1 > #lines or pos[2] < 0 then -- pos is trivially (row negative or beyond str, or col negative) - -- outside of str, can't represent position in str. + -- outside of lines, can't represent position in lines. -- col-wise outside will be determined later, but we want this -- precondition for following code. return nil @@ -216,12 +222,12 @@ function M.multiline_to_byte_offset(str, pos) local byte_pos = 0 for i = 1, pos[1] do -- increase index by full lines, don't forget +1 for \n. - byte_pos = byte_pos + #str[i] + 1 + byte_pos = byte_pos + #lines[i] + 1 end -- allow positions one beyond the last character for all lines (even the -- last line). - if pos[2] >= #str[pos[1] + 1] + 1 then + if pos[2] >= #lines[pos[1] + 1] + 1 then -- in this case, pos is outside of the multiline-region. return nil end @@ -233,16 +239,18 @@ function M.multiline_to_byte_offset(str, pos) return byte_pos + 1 end --- inverse of multiline_to_byte_offset, 1-based byte to 0,0-based row,column. ----@param str string[], the multiline string ----@param byte_pos number, a 1-based index into the \n-concatenated `str`. -function M.byte_to_multiline_offset(str, byte_pos) +--- Convert a 1-based byte index in a multiline string to 0,0-based row,column. +--- (It is functionally the inverse of multiline_to_byte_offset) +---@param lines string[] the multiline string +---@param byte_pos number 1-based index into the \n-concatenated `lines`. +---@return LuaSnip.Pos00? +function M.byte_to_multiline_offset(lines, byte_pos) if byte_pos < 0 then return nil end local byte_pos_so_far = 0 - for i, line in ipairs(str) do + for i, line in ipairs(lines) do -- line-length + \n. local line_i_end = byte_pos_so_far + #line + 1 if byte_pos <= line_i_end then @@ -256,10 +264,15 @@ end -- string-operations implemented according to -- https://github.com/microsoft/vscode/blob/71c221c532996c9976405f62bb888283c0cf6545/src/vs/editor/contrib/snippet/browser/snippetParser.ts#L372-L415 -- such that they can be used for snippet-transformations in vscode-snippets. +---@param str string +---@return string local function capitalize(str) -- uppercase first character. - return str:gsub("^.", string.upper) + local ret = str:gsub("^.", string.upper) + return ret -- note: local var required for correct typing end +---@param str string +---@return string local function pascalcase(str) local pascalcased = "" for match in str:gmatch("[a-zA-Z0-9]+") do @@ -267,16 +280,20 @@ local function pascalcase(str) end return pascalcased end +---@param str string +---@return string +local function camelcase(str) + -- same as pascalcase, but first character lowercased. + local ret = pascalcase(str):gsub("^.", string.lower) + return ret -- note: local var required for correct typing +end M.vscode_string_modifiers = { upcase = string.upper, downcase = string.lower, capitalize = capitalize, pascalcase = pascalcase, - camelcase = function(str) - -- same as pascalcase, but first character lowercased. - return pascalcase(str):gsub("^.", string.lower) - end, + camelcase = camelcase, } return M