feat: add first-class Neovim support with enhanced formatting

- Add modern Lua implementation with modular architecture
- Implement HEREDOC preservation and smart comment indentation
- Create dual implementation (Neovim Lua + VimScript fallback)
- Add comprehensive health check and configuration system
- Enhance formatting engine with state machine for context awareness
- Update documentation with Lua configuration examples
- Add memory files for development workflow and conventions
This commit is contained in:
2025-09-09 21:13:38 +03:00
parent 710f68a6e5
commit ce620cd035
18 changed files with 1656 additions and 52 deletions

203
lua/shellspec/format.lua Normal file
View File

@@ -0,0 +1,203 @@
-- Enhanced ShellSpec DSL formatter with HEREDOC support
local config = require("shellspec.config")
local M = {}
-- Formatting state
local State = {
NORMAL = 1,
IN_HEREDOC = 2,
IN_DATA_BLOCK = 3,
}
-- HEREDOC detection patterns
local function get_heredoc_patterns()
return config.get("heredoc_patterns")
end
-- Check if line starts a HEREDOC
local function detect_heredoc_start(line)
local trimmed = vim.trim(line)
for _, pattern in ipairs(get_heredoc_patterns()) do
local match = string.match(trimmed, pattern)
if match then
-- Extract the delimiter
local delimiter = string.match(match, "<<-?['\"]?([A-Z_][A-Z0-9_]*)['\"]?")
return delimiter
end
end
return nil
end
-- Check if line ends a HEREDOC
local function is_heredoc_end(line, delimiter)
if not delimiter then
return false
end
local trimmed = vim.trim(line)
return trimmed == delimiter
end
-- Check if line is a ShellSpec block keyword
local function is_block_keyword(line)
local trimmed = vim.trim(line)
-- Standard block keywords
if string.match(trimmed, "^(Describe|Context|ExampleGroup|It|Specify|Example)") then
return true
end
-- Prefixed block keywords (x for skip, f for focus)
if string.match(trimmed, "^[xf](Describe|Context|ExampleGroup|It|Specify|Example)") then
return true
end
-- Data and Parameters blocks
if string.match(trimmed, "^(Data|Parameters)%s*$") then
return true
end
return false
end
-- Check if line is an End keyword
local function is_end_keyword(line)
local trimmed = vim.trim(line)
return string.match(trimmed, "^End%s*$") ~= nil
end
-- Check if line is a comment
local function is_comment(line)
local trimmed = vim.trim(line)
return string.match(trimmed, "^#") ~= nil
end
-- Generate indentation string
local function make_indent(level)
local indent_size = config.get("indent_size")
local use_spaces = config.get("use_spaces")
if use_spaces then
return string.rep(" ", level * indent_size)
else
return string.rep("\t", level)
end
end
-- Main formatting function
function M.format_lines(lines)
local result = {}
local indent_level = 0
local state = State.NORMAL
local heredoc_delimiter = nil
local indent_comments = config.get("indent_comments")
for _, line in ipairs(lines) do
local trimmed = vim.trim(line)
-- Handle empty lines
if trimmed == "" then
table.insert(result, line)
goto continue
end
-- State machine for HEREDOC handling
if state == State.NORMAL then
-- Check for HEREDOC start
local delimiter = detect_heredoc_start(line)
if delimiter then
state = State.IN_HEREDOC
heredoc_delimiter = delimiter
-- Apply current indentation to HEREDOC start line
local formatted_line = make_indent(indent_level) .. trimmed
table.insert(result, formatted_line)
goto continue
end
-- Handle comments with proper indentation
if is_comment(line) and indent_comments then
local formatted_line = make_indent(indent_level) .. trimmed
table.insert(result, formatted_line)
goto continue
end
-- Handle End keyword (decrease indent first)
if is_end_keyword(line) then
indent_level = math.max(0, indent_level - 1)
local formatted_line = make_indent(indent_level) .. trimmed
table.insert(result, formatted_line)
goto continue
end
-- Apply normal indentation for other lines
if not is_comment(line) or not indent_comments then
local formatted_line = make_indent(indent_level) .. trimmed
table.insert(result, formatted_line)
-- Increase indent after block keywords
if is_block_keyword(line) then
indent_level = indent_level + 1
end
else
-- Preserve original comment formatting if indent_comments is false
table.insert(result, line)
end
elseif state == State.IN_HEREDOC then
-- Check for HEREDOC end
if is_heredoc_end(line, heredoc_delimiter) then
state = State.NORMAL
heredoc_delimiter = nil
-- Apply current indentation to HEREDOC end line
local formatted_line = make_indent(indent_level) .. trimmed
table.insert(result, formatted_line)
else
-- Preserve original indentation within HEREDOC
table.insert(result, line)
end
end
::continue::
end
return result
end
-- Format entire buffer
function M.format_buffer(bufnr)
bufnr = bufnr or 0
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local formatted = M.format_lines(lines)
-- Store cursor position
local cursor_pos = vim.api.nvim_win_get_cursor(0)
-- Replace buffer content
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, formatted)
-- Restore cursor position
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
end
-- Format selection
function M.format_selection(bufnr, start_line, end_line)
bufnr = bufnr or 0
local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false)
local formatted = M.format_lines(lines)
-- Replace selection
vim.api.nvim_buf_set_lines(bufnr, start_line - 1, end_line, false, formatted)
end
-- Async format function for performance
function M.format_buffer_async(bufnr, callback)
bufnr = bufnr or 0
-- Use vim.schedule to avoid blocking
vim.schedule(function()
M.format_buffer(bufnr)
if callback then
callback()
end
end)
end
return M