Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/luasnip.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2025 November 03
*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2026 January 18

==============================================================================
Table of Contents *luasnip-table-of-contents*
Expand Down
138 changes: 87 additions & 51 deletions lua/luasnip/util/str.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -144,6 +155,7 @@ function M.unescaped_pairs(s, left, right)
end
end

-- FIXME(@L3MON4D3): not used anywhere?
Comment thread
bew marked this conversation as resolved.
Outdated
function M.aupatescape(s)
if vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 then
-- windows: replace \ with / for au-pattern.
Expand All @@ -153,20 +165,26 @@ function M.aupatescape(s)
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.Pos00 From this position, MUST be within `lines`.
---@param to LuaSnip.Pos00 To this position (excluded), MUST be within `lines`
Comment thread
bew marked this conversation as resolved.
Outdated
--- 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.
Expand All @@ -179,35 +197,42 @@ 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()
--- 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
function M.multiline_lower(str)
for i, s in ipairs(str) do
str[i] = s:lower()

--- 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

-- modifies strmod
-- FIXME(@L3MON4D3): not used anywhere?
Comment thread
bew marked this conversation as resolved.
Outdated
function M.multiline_append(strmod, strappend)
strmod[#strmod] = strmod[#strmod] .. strappend[1]
for i = 2, #strappend do
table.insert(strmod, strappend[i])
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.ApiPosition an api-position relative to the start of str.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LuaSnip.ApiPosition type is not defined 🤔 What do you want here?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be RawPos00, I think I'm referring to api-indexing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in bb1c84d

still warning until #1420 is merged

---@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
Expand All @@ -216,12 +241,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
Expand All @@ -233,16 +258,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
Expand All @@ -256,27 +283,36 @@ 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
pascalcased = pascalcased .. capitalize(match)
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