mirror of
https://github.com/ivuorinen/nvim-shellspec.git
synced 2026-01-26 03:24:00 +00:00
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:
203
lua/shellspec/format.lua
Normal file
203
lua/shellspec/format.lua
Normal 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
|
||||
Reference in New Issue
Block a user