aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/health.lua9
-rw-r--r--runtime/lua/vim/text.lua87
2 files changed, 89 insertions, 7 deletions
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index a265e2b901..0d42e8fed6 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -186,18 +186,13 @@ local function get_healthcheck(plugin_names)
return healthchecks
end
---- Indents lines *except* line 1 of a string if it contains newlines.
+--- Indents lines *except* line 1 of a multiline string.
---
--- @param s string
--- @param columns integer
--- @return string
local function indent_after_line1(s, columns)
- local lines = vim.split(s, '\n')
- local indent = string.rep(' ', columns)
- for i = 2, #lines do
- lines[i] = indent .. lines[i]
- end
- return table.concat(lines, '\n')
+ return (vim.text.indent(columns, s):gsub('^%s+', ''))
end
--- Changes ':h clipboard' to ':help |clipboard|'.
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