diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/health.lua | 9 | ||||
-rw-r--r-- | runtime/lua/vim/text.lua | 87 |
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 |