mirror of
https://github.com/neovim/neovim
synced 2025-07-16 01:01:49 +00:00
feat(lua): vim.text.indent()
Problem: Indenting text is a common task in plugins/scripts for presentation/formatting, yet vim has no way of doing it (especially "dedent", and especially non-buffer text). Solution: Introduce `vim.text.indent()`. It sets the *exact* indentation because that's a more difficult (and thus more useful) task than merely "increasing the current indent" (which is somewhat easy with a `gsub()` one-liner).
This commit is contained in:
@ -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
|
||||
|
Reference in New Issue
Block a user