diff options
author | Justin M. Keyes <justinkz@gmail.com> | 2025-02-26 14:31:03 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-02-26 14:31:03 -0800 |
commit | 4f42b69b4ad0d0034581d756ef9bcb0e55f3491d (patch) | |
tree | 0c40b6f8c3569e339d375cb5842820590c5f3103 /runtime/lua/vim/text.lua | |
parent | f4921e2b7deb4812414998a521c33f920f571c20 (diff) | |
parent | 4a997a1732fdb7a1c99cadf780b789f1ac84d990 (diff) | |
download | rneovim-4f42b69b4ad0d0034581d756ef9bcb0e55f3491d.tar.gz rneovim-4f42b69b4ad0d0034581d756ef9bcb0e55f3491d.tar.bz2 rneovim-4f42b69b4ad0d0034581d756ef9bcb0e55f3491d.zip |
Merge #32601 vim.text.indent()
Diffstat (limited to 'runtime/lua/vim/text.lua')
-rw-r--r-- | runtime/lua/vim/text.lua | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/runtime/lua/vim/text.lua b/runtime/lua/vim/text.lua index f910ab3a1d..93220a2e42 100644 --- a/runtime/lua/vim/text.lua +++ b/runtime/lua/vim/text.lua @@ -50,4 +50,91 @@ function M.hexdecode(enc) return table.concat(str), nil end +--- Sets the indent (i.e. the common leading whitespace) of non-empty lines in `text` to `size` +--- spaces/tabs. +--- +--- Indent is calculated by number of consecutive indent chars. +--- - The first indented, non-empty line decides the indent char (space/tab): +--- - `SPC SPC TAB …` = two-space indent. +--- - `TAB SPC …` = one-tab indent. +--- - Set `opts.expandtab` to treat tabs as spaces. +--- +--- To "dedent" (remove the common indent), pass `size=0`: +--- ```lua +--- vim.print(vim.text.indent(0, ' a\n b\n')) +--- ``` +--- +--- To adjust relative-to an existing indent, call indent() twice: +--- ```lua +--- local indented, old_indent = vim.text.indent(0, ' a\n b\n') +--- indented = vim.text.indent(old_indent + 2, indented) +--- vim.print(indented) +--- ``` +--- +--- To ignore the final, blank line when calculating the indent, use gsub() before calling indent(): +--- ```lua +--- local text = ' a\n b\n ' +--- vim.print(vim.text.indent(0, (text:gsub('\n[\t ]+\n?$', '\n')))) +--- ``` +--- +--- @param size integer Number of spaces. +--- @param text string Text to indent. +--- @param opts? { expandtab?: number } +--- @return string # Indented text. +--- @return integer # Indent size _before_ modification. +function M.indent(size, text, opts) + vim.validate('size', size, 'number') + vim.validate('text', text, 'string') + vim.validate('opts', opts, 'table', true) + -- TODO(justinmk): `opts.prefix`, `predicate` like python https://docs.python.org/3/library/textwrap.html + opts = opts or {} + local tabspaces = opts.expandtab and (' '):rep(opts.expandtab) or nil + + --- Minimum common indent shared by all lines. + local old_indent --[[@type number?]] + local prefix = tabspaces and ' ' or nil -- Indent char (space or tab). + --- Check all non-empty lines, capturing leading whitespace (if any). + --- @diagnostic disable-next-line: no-unknown + for line_ws, extra in text:gmatch('([\t ]*)([^\n]+)') do + line_ws = tabspaces and line_ws:gsub('[\t]', tabspaces) or line_ws + -- XXX: blank line will miss the last whitespace char in `line_ws`, so we need to check `extra`. + line_ws = line_ws .. (extra:match('^%s+$') or '') + if 0 == #line_ws then + -- Optimization: If any non-empty line has indent=0, there is no common indent. + old_indent = 0 + break + end + prefix = prefix and prefix or line_ws:sub(1, 1) + local _, end_ = line_ws:find('^[' .. prefix .. ']+') + old_indent = math.min(old_indent or math.huge, end_ or 0) + end + -- Default to 0 if all lines are empty. + old_indent = old_indent or 0 + prefix = prefix and prefix or ' ' + + if old_indent == size then + -- Optimization: if the indent is the same, return the text unchanged. + return text, old_indent + end + + local new_indent = prefix:rep(size) + + --- Replaces indentation of a line. + --- @param line string + local function replace_line(line) + -- Match the existing indent exactly; avoid over-matching any following whitespace. + local pat = prefix:rep(old_indent) + -- Expand tabs before replacing indentation. + line = not tabspaces and line + or line:gsub('^[\t ]+', function(s) + return s:gsub('\t', tabspaces) + end) + -- Text following the indent. + local line_text = line:match('^' .. pat .. '(.*)') or line + return new_indent .. line_text + end + + return (text:gsub('[^\n]+', replace_line)), old_indent +end + return M |