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:
@@ -42,13 +42,13 @@ repos:
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: ['--severity=warning']
|
||||
args: ["--severity=warning"]
|
||||
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.7
|
||||
hooks:
|
||||
- id: actionlint
|
||||
args: ['-shellcheck=']
|
||||
args: ["-shellcheck="]
|
||||
|
||||
- repo: https://github.com/renovatebot/pre-commit-hooks
|
||||
rev: 41.97.9
|
||||
@@ -56,8 +56,8 @@ repos:
|
||||
- id: renovate-config-validator
|
||||
|
||||
- repo: https://github.com/bridgecrewio/checkov.git
|
||||
rev: '3.2.469'
|
||||
rev: "3.2.469"
|
||||
hooks:
|
||||
- id: checkov
|
||||
args:
|
||||
- '--quiet'
|
||||
- "--quiet"
|
||||
|
||||
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
203
.serena/memories/code_style_conventions.md
Normal file
203
.serena/memories/code_style_conventions.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Code Style and Conventions
|
||||
|
||||
## EditorConfig Settings
|
||||
|
||||
All files follow these rules from `.editorconfig`:
|
||||
|
||||
- **Charset**: UTF-8
|
||||
- **Line endings**: LF (Unix-style)
|
||||
- **Indentation**: 2 spaces (no tabs)
|
||||
- **Max line length**: 160 characters
|
||||
- **Final newline**: Required
|
||||
- **Trim trailing whitespace**: Yes
|
||||
|
||||
### Special Cases
|
||||
|
||||
- **Markdown files**: Don't trim trailing whitespace (for hard line breaks)
|
||||
- **Makefiles**: Use tabs with width 4
|
||||
|
||||
## Lua Code Conventions (New)
|
||||
|
||||
### Module Structure
|
||||
|
||||
```lua
|
||||
-- Module header with description
|
||||
local M = {}
|
||||
|
||||
-- Import dependencies at top
|
||||
local config = require('shellspec.config')
|
||||
|
||||
-- Private functions (local)
|
||||
local function private_helper() end
|
||||
|
||||
-- Public functions (M.function_name)
|
||||
function M.public_function() end
|
||||
|
||||
return M
|
||||
```
|
||||
|
||||
### Function Names
|
||||
|
||||
- Use `snake_case` for all functions
|
||||
- Private functions: `local function name()`
|
||||
- Public functions: `function M.name()` or `M.name = function()`
|
||||
- Descriptive names, avoid abbreviations
|
||||
|
||||
### Variable Names
|
||||
|
||||
- Local variables: `local variable_name`
|
||||
- Constants: `local CONSTANT_NAME` (uppercase)
|
||||
- Table keys: `snake_case`
|
||||
|
||||
### Documentation
|
||||
|
||||
- Use LuaDoc style comments for public functions
|
||||
- Include parameter and return type information
|
||||
|
||||
```lua
|
||||
--- Format lines with ShellSpec DSL rules
|
||||
-- @param lines table: Array of strings to format
|
||||
-- @return table: Array of formatted strings
|
||||
function M.format_lines(lines) end
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use `pcall()` for operations that might fail
|
||||
- Provide meaningful error messages
|
||||
- Use `vim.notify()` for user-facing messages
|
||||
|
||||
## Vim Script Conventions (Enhanced)
|
||||
|
||||
### Function Names
|
||||
|
||||
- Use `snake_case#function_name()` format
|
||||
- Functions in autoload use namespace prefix: `shellspec#function_name()`
|
||||
- Guard clauses with `abort` keyword: `function! shellspec#format_buffer() abort`
|
||||
- Private functions: `s:function_name()`
|
||||
|
||||
### Variable Names
|
||||
|
||||
- Local variables: `l:variable_name`
|
||||
- Global variables: `g:variable_name`
|
||||
- Buffer-local: `b:variable_name`
|
||||
- Script-local: `s:variable_name`
|
||||
|
||||
### State Management
|
||||
|
||||
- Use descriptive state names: `'normal'`, `'heredoc'`
|
||||
- Document state transitions in comments
|
||||
- Initialize state variables clearly
|
||||
|
||||
### Code Structure
|
||||
|
||||
```vim
|
||||
" File header with description and author
|
||||
if exists('g:loaded_plugin')
|
||||
finish
|
||||
endif
|
||||
let g:loaded_plugin = 1
|
||||
|
||||
" Helper functions (private)
|
||||
function! s:private_function() abort
|
||||
endfunction
|
||||
|
||||
" Public functions
|
||||
function! public#function() abort
|
||||
endfunction
|
||||
|
||||
" Commands, autocommands at end
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
- Use `"` for comments
|
||||
- Include descriptive headers for functions
|
||||
- Comment complex logic blocks and state changes
|
||||
- Document HEREDOC patterns and detection logic
|
||||
|
||||
## Shell Script Style (bin/shellspec-format)
|
||||
|
||||
- Use `#!/bin/bash` shebang
|
||||
- Double quote variables: `"$variable"`
|
||||
- Use `[[ ]]` for conditionals instead of `[ ]`
|
||||
- Proper error handling with exit codes
|
||||
- Function names in `snake_case`
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- **YAML**: 2-space indentation, 200 character line limit
|
||||
- **JSON**: Pretty formatted, no trailing commas
|
||||
- **Markdown**: 200 character line limit (relaxed from default 80)
|
||||
- **Lua**: Follow Neovim Lua style guide
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Files**: lowercase with hyphens (`shellspec-format`)
|
||||
- **Directories**: lowercase (`autoload`, `syntax`, `ftdetect`)
|
||||
- **Lua modules**: lowercase with dots (`shellspec.format`)
|
||||
- **Functions**: namespace#function_name format (VimScript), snake_case (Lua)
|
||||
- **Variables**: descriptive names, avoid abbreviations
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Dual Implementation Pattern
|
||||
|
||||
```vim
|
||||
" Detect environment and choose implementation
|
||||
if has('nvim-0.7')
|
||||
" Use Lua implementation
|
||||
lua require('module').function()
|
||||
else
|
||||
" Fall back to VimScript
|
||||
call legacy#function()
|
||||
endif
|
||||
```
|
||||
|
||||
### State Machine Pattern (Both Lua and VimScript)
|
||||
|
||||
```lua
|
||||
-- Lua version
|
||||
local state = State.NORMAL
|
||||
if state == State.NORMAL then
|
||||
-- handle normal formatting
|
||||
elseif state == State.IN_HEREDOC then
|
||||
-- preserve heredoc content
|
||||
end
|
||||
```
|
||||
|
||||
```vim
|
||||
" VimScript version
|
||||
let l:state = 'normal'
|
||||
if l:state ==# 'normal'
|
||||
" handle normal formatting
|
||||
elseif l:state ==# 'heredoc'
|
||||
" preserve heredoc content
|
||||
endif
|
||||
```
|
||||
|
||||
### Configuration Pattern
|
||||
|
||||
```lua
|
||||
-- Lua: Use vim.tbl_deep_extend for merging
|
||||
local config = vim.tbl_deep_extend("force", defaults, user_opts)
|
||||
```
|
||||
|
||||
```vim
|
||||
" VimScript: Use get() with defaults
|
||||
let l:option = get(g:, 'plugin_option', default_value)
|
||||
```
|
||||
|
||||
## Testing Conventions
|
||||
|
||||
- Create test files with `.spec.sh` extension
|
||||
- Test both Lua and VimScript implementations
|
||||
- Include HEREDOC and comment test cases
|
||||
- Use descriptive test names matching actual ShellSpec patterns
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
- Update README.md with new features
|
||||
- Include both Lua and VimScript configuration examples
|
||||
- Provide clear examples of HEREDOC and comment behavior
|
||||
- Document breaking changes and migration paths
|
||||
138
.serena/memories/codebase_structure.md
Normal file
138
.serena/memories/codebase_structure.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Codebase Structure
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```text
|
||||
nvim-shellspec/
|
||||
├── lua/shellspec/ # Modern Neovim Lua implementation
|
||||
│ ├── init.lua # Main module entry point & setup
|
||||
│ ├── config.lua # Configuration management
|
||||
│ ├── format.lua # Enhanced formatting engine
|
||||
│ ├── autocmds.lua # Neovim-native autocommands
|
||||
│ └── health.lua # Health check support
|
||||
├── autoload/ # Plugin functions (VimScript)
|
||||
│ └── shellspec.vim # Enhanced formatting with HEREDOC support
|
||||
├── bin/ # Standalone executables
|
||||
│ └── shellspec-format # Bash formatter script
|
||||
├── ftdetect/ # Filetype detection
|
||||
│ └── shellspec.vim # Auto-detect ShellSpec files
|
||||
├── indent/ # Indentation rules
|
||||
│ └── shellspec.vim # Smart indentation for ShellSpec DSL
|
||||
├── plugin/ # Main plugin file (loaded at startup)
|
||||
│ └── shellspec.vim # Neovim detection & dual implementation
|
||||
├── syntax/ # Syntax highlighting
|
||||
│ └── shellspec.vim # ShellSpec DSL syntax rules
|
||||
└── .github/ # GitHub workflows and templates
|
||||
```
|
||||
|
||||
## Core Files
|
||||
|
||||
### Lua Implementation (Neovim 0.7+)
|
||||
|
||||
#### lua/shellspec/init.lua
|
||||
|
||||
- Main module entry point with setup() function
|
||||
- Lua configuration interface
|
||||
- Health check integration (:checkhealth support)
|
||||
- Backward compatibility functions for VimScript
|
||||
|
||||
#### lua/shellspec/config.lua
|
||||
|
||||
- Configuration management with defaults
|
||||
- Validation and type checking
|
||||
- Support for:
|
||||
- Auto-format settings
|
||||
- Indentation preferences
|
||||
- HEREDOC pattern customization
|
||||
- Comment indentation options
|
||||
|
||||
#### lua/shellspec/format.lua
|
||||
|
||||
- Advanced formatting engine with state machine
|
||||
- HEREDOC detection and preservation
|
||||
- Smart comment indentation
|
||||
- Context-aware formatting (normal, in-heredoc states)
|
||||
- Async formatting capabilities
|
||||
|
||||
#### lua/shellspec/autocmds.lua
|
||||
|
||||
- Neovim-native autocommands using vim.api
|
||||
- Buffer-local settings and commands
|
||||
- Enhanced filetype detection patterns
|
||||
- Auto-format on save integration
|
||||
|
||||
#### lua/shellspec/health.lua
|
||||
|
||||
- Comprehensive health checks for :checkhealth
|
||||
- Configuration validation
|
||||
- Module loading verification
|
||||
- Project ShellSpec file detection
|
||||
|
||||
### VimScript Implementation (Compatibility)
|
||||
|
||||
#### plugin/shellspec.vim
|
||||
|
||||
- **Dual Implementation Logic**: Detects Neovim 0.7+ and loads appropriate implementation
|
||||
- **Neovim Path**: Loads Lua modules and creates command delegators
|
||||
- **Vim Path**: Falls back to enhanced VimScript implementation
|
||||
- Maintains all existing functionality
|
||||
|
||||
#### autoload/shellspec.vim
|
||||
|
||||
- **Enhanced VimScript formatter** with same features as Lua version
|
||||
- HEREDOC detection patterns and state machine
|
||||
- Smart comment indentation logic
|
||||
- Backward compatibility with older Vim versions
|
||||
|
||||
### Traditional Vim Plugin Structure
|
||||
|
||||
#### ftdetect/shellspec.vim
|
||||
|
||||
- Automatic filetype detection for ShellSpec files
|
||||
- Patterns: `*_spec.sh`, `*.spec.sh`, `spec/*.sh`, `test/*.sh`
|
||||
- Enhanced with nested spec directory support
|
||||
|
||||
#### indent/shellspec.vim
|
||||
|
||||
- Smart indentation based on ShellSpec block structure
|
||||
- Handles `Describe`, `Context`, `It` blocks and their variants
|
||||
- Special handling for `End` keyword and `Data`/`Parameters` blocks
|
||||
|
||||
#### syntax/shellspec.vim
|
||||
|
||||
- Complete syntax highlighting for ShellSpec DSL
|
||||
- Keywords: Block structures, control flow, evaluation, expectations, hooks
|
||||
- Supports nested shell code regions
|
||||
- Proper highlighting for strings, variables, comments
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### Development & Quality
|
||||
|
||||
- `.pre-commit-config.yaml` - Pre-commit hooks configuration
|
||||
- `.mega-linter.yml` - MegaLinter configuration
|
||||
- `.yamllint.yml` - YAML linting rules
|
||||
- `.markdownlint.json` - Markdown linting rules
|
||||
- `.editorconfig` - Editor configuration
|
||||
|
||||
### Git & CI/CD
|
||||
|
||||
- `.github/workflows/` - GitHub Actions for CI
|
||||
- `.gitignore` - Git ignore patterns
|
||||
|
||||
## ShellSpec DSL Keywords Supported
|
||||
|
||||
- **Blocks**: Describe, Context, ExampleGroup, It, Specify, Example
|
||||
- **Prefixed blocks**: xDescribe, fDescribe (skip/focus variants)
|
||||
- **Hooks**: BeforeEach, AfterEach, BeforeAll, AfterAll
|
||||
- **Evaluation**: When, call, run, command, script, source
|
||||
- **Expectations**: The, Assert, should, output, stdout, error, stderr
|
||||
- **Helpers**: Dump, Include, Set, Path, File, Dir, Data, Parameters
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
- **Performance**: Lua implementation for better performance in Neovim
|
||||
- **Modern APIs**: Uses Neovim's native autocmd and formatting APIs
|
||||
- **Maintainability**: Modular structure with clear separation of concerns
|
||||
- **Extensibility**: Easy to add new features through Lua configuration
|
||||
- **Compatibility**: Seamless fallback ensures broad editor support
|
||||
49
.serena/memories/project_overview.md
Normal file
49
.serena/memories/project_overview.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# nvim-shellspec Project Overview
|
||||
|
||||
## Purpose
|
||||
|
||||
This is a Neovim/Vim plugin that provides advanced language support and formatting for the ShellSpec DSL testing framework.
|
||||
ShellSpec is a BDD (Behavior-Driven Development) testing framework for shell scripts.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **🚀 First-class Neovim support** with modern Lua implementation
|
||||
- **🎨 Syntax highlighting** for all ShellSpec DSL keywords
|
||||
- **📐 Smart indentation** for block structures
|
||||
- **📄 Enhanced filetype detection** for `*_spec.sh`, `*.spec.sh`, `spec/*.sh`, `test/*.sh`, and nested spec directories
|
||||
- **✨ Advanced formatting** with HEREDOC and comment support
|
||||
- **⚡ Async formatting** to prevent blocking (Neovim 0.7+)
|
||||
- **🔄 Backward compatibility** with Vim and older Neovim versions
|
||||
|
||||
## Advanced Formatting Features
|
||||
|
||||
- **HEREDOC Preservation**: Maintains original formatting within `<<EOF`, `<<'EOF'`, `<<"EOF"`, and `<<-EOF` blocks
|
||||
- **Smart Comment Indentation**: Comments are indented to match surrounding code level
|
||||
- **Context-Aware Formatting**: State machine tracks formatting context for accurate indentation
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Primary language**: Vim script (VimL) + Lua (Neovim)
|
||||
- **Target environment**: Neovim 0.7+ (with Vim fallback)
|
||||
- **Architecture**: Modular Lua implementation with VimScript compatibility layer
|
||||
- **Shell scripting**: Bash (for standalone formatter in `bin/shellspec-format`)
|
||||
- **Configuration formats**: YAML, JSON, EditorConfig
|
||||
|
||||
## Dual Implementation
|
||||
|
||||
- **Neovim 0.7+**: Modern Lua implementation with native APIs
|
||||
- **Vim/Older Neovim**: Enhanced VimScript with same formatting features
|
||||
|
||||
## Target Files
|
||||
|
||||
Plugin activates for files matching:
|
||||
|
||||
- `*_spec.sh`
|
||||
- `*.spec.sh`
|
||||
- `spec/*.sh`
|
||||
- `test/*.sh`
|
||||
- Files in nested `spec/` directories
|
||||
|
||||
## Related Project
|
||||
|
||||
- [ShellSpec](https://github.com/shellspec/shellspec) - BDD testing framework for shell scripts
|
||||
203
.serena/memories/suggested_commands.md
Normal file
203
.serena/memories/suggested_commands.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Development Commands for nvim-shellspec
|
||||
|
||||
## Quality Assurance & Linting Commands
|
||||
|
||||
### Primary Linting Command
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
This runs all configured linters and formatters including:
|
||||
|
||||
- ShellCheck for shell scripts
|
||||
- shfmt for shell script formatting
|
||||
- yamllint for YAML files
|
||||
- markdownlint for Markdown files
|
||||
- Various pre-commit hooks
|
||||
|
||||
### Individual Linters
|
||||
|
||||
```bash
|
||||
# YAML linting
|
||||
yamllint .
|
||||
|
||||
# Markdown linting (via npx)
|
||||
npx markdownlint-cli -c .markdownlint.json --fix README.md
|
||||
|
||||
# Shell script linting
|
||||
shellcheck bin/shellspec-format
|
||||
|
||||
# Shell script formatting
|
||||
shfmt -w bin/shellspec-format
|
||||
|
||||
# Lua linting (if available)
|
||||
luacheck lua/shellspec/
|
||||
```
|
||||
|
||||
## Code Formatting
|
||||
|
||||
### ShellSpec DSL Formatting
|
||||
|
||||
```bash
|
||||
# Using standalone formatter
|
||||
./bin/shellspec-format file.spec.sh
|
||||
|
||||
# Or in Neovim/Vim
|
||||
:ShellSpecFormat
|
||||
:ShellSpecFormatRange (for selected lines)
|
||||
```
|
||||
|
||||
### Testing New Lua Implementation (Neovim)
|
||||
|
||||
```lua
|
||||
-- Test in Neovim command line
|
||||
:lua require('shellspec').setup({ auto_format = true })
|
||||
:lua require('shellspec').format_buffer()
|
||||
|
||||
-- Health check
|
||||
:checkhealth shellspec
|
||||
```
|
||||
|
||||
## Development Testing
|
||||
|
||||
### Manual Plugin Testing
|
||||
|
||||
```bash
|
||||
# Create test file
|
||||
touch test_example.spec.sh
|
||||
|
||||
# Test in Neovim
|
||||
nvim test_example.spec.sh
|
||||
# Verify filetype: :set filetype?
|
||||
# Test formatting: :ShellSpecFormat
|
||||
# Test health check: :checkhealth shellspec
|
||||
```
|
||||
|
||||
### HEREDOC and Comment Testing
|
||||
|
||||
Create test content with:
|
||||
|
||||
```shellspec
|
||||
Describe "test"
|
||||
# Comment that should be indented
|
||||
It "should preserve HEREDOC"
|
||||
cat <<EOF
|
||||
This should not be reformatted
|
||||
Even with nested indentation
|
||||
EOF
|
||||
End
|
||||
End
|
||||
```
|
||||
|
||||
## Git Integration
|
||||
|
||||
```bash
|
||||
# Pre-commit hooks are automatically installed
|
||||
pre-commit install
|
||||
|
||||
# Run pre-commit on all files
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
## Neovim-Specific Development
|
||||
|
||||
### Lua Module Testing
|
||||
|
||||
```bash
|
||||
# Test individual modules in Neovim
|
||||
:lua print(vim.inspect(require('shellspec.config').defaults))
|
||||
:lua require('shellspec.format').format_buffer()
|
||||
:lua require('shellspec.autocmds').setup()
|
||||
```
|
||||
|
||||
### Health Diagnostics
|
||||
|
||||
```bash
|
||||
# Comprehensive health check
|
||||
:checkhealth shellspec
|
||||
|
||||
# Check if modules load correctly
|
||||
:lua require('shellspec.health').check()
|
||||
```
|
||||
|
||||
## File System Utilities (macOS/Darwin)
|
||||
|
||||
```bash
|
||||
# File operations
|
||||
ls -la # List files with details
|
||||
find . -name # Find files by pattern
|
||||
grep -r # Search in files (or use rg for ripgrep)
|
||||
|
||||
# Better alternatives available on system:
|
||||
rg # ripgrep for faster searching
|
||||
fd # faster find alternative
|
||||
|
||||
# Find all ShellSpec files in project
|
||||
fd -e spec.sh
|
||||
fd "_spec.sh$"
|
||||
rg -t sh "Describe|Context|It" spec/
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Standard Development
|
||||
|
||||
1. Make changes to Vim script or Lua files
|
||||
2. Test with sample ShellSpec files (`test_example.spec.sh`)
|
||||
3. Run `pre-commit run --all-files` before committing
|
||||
4. Fix any linting issues
|
||||
5. Test in both Neovim (Lua path) and Vim (VimScript path)
|
||||
6. Commit changes
|
||||
|
||||
### Feature Development
|
||||
|
||||
1. Update Lua implementation in `lua/shellspec/`
|
||||
2. Update VimScript compatibility in `autoload/shellspec.vim`
|
||||
3. Test dual implementation paths
|
||||
4. Update health checks if needed
|
||||
5. Update documentation
|
||||
|
||||
### Configuration Testing
|
||||
|
||||
```lua
|
||||
-- Test different configurations
|
||||
require('shellspec').setup({
|
||||
auto_format = true,
|
||||
indent_size = 4,
|
||||
indent_comments = false,
|
||||
heredoc_patterns = {"<<[A-Z_]+", "<<'[^']*'"}
|
||||
})
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
```bash
|
||||
# Test with large ShellSpec files
|
||||
time nvim +':ShellSpecFormat' +':wq' large_spec_file.spec.sh
|
||||
|
||||
# Compare Lua vs VimScript performance
|
||||
# (Use older Neovim version to force VimScript path)
|
||||
```
|
||||
|
||||
## Plugin Integration Testing
|
||||
|
||||
```lua
|
||||
-- Test with lazy.nvim
|
||||
{
|
||||
dir = "/path/to/local/nvim-shellspec",
|
||||
config = function()
|
||||
require("shellspec").setup({ auto_format = true })
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
## Memory and State Debugging
|
||||
|
||||
```lua
|
||||
-- Debug configuration state
|
||||
:lua print(vim.inspect(require('shellspec.config').config))
|
||||
|
||||
-- Debug formatting state
|
||||
:lua require('shellspec.format').format_lines({"Describe 'test'", " It 'works'", " End", "End"})
|
||||
```
|
||||
242
.serena/memories/task_completion_checklist.md
Normal file
242
.serena/memories/task_completion_checklist.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Task Completion Checklist
|
||||
|
||||
When completing any development task in the nvim-shellspec project, follow this checklist:
|
||||
|
||||
## 1. Code Quality Checks (MANDATORY)
|
||||
|
||||
```bash
|
||||
# Run all pre-commit hooks
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
- **ShellCheck** - Shell script linting and static analysis
|
||||
- **shfmt** - Shell script formatting
|
||||
- **yamllint** - YAML file validation
|
||||
- **markdownlint** - Markdown linting and formatting
|
||||
- **Various pre-commit hooks** - Trailing whitespace, end-of-file, etc.
|
||||
|
||||
## 2. EditorConfig Compliance (BLOCKING)
|
||||
|
||||
- All files must follow `.editorconfig` rules
|
||||
- 2-space indentation, LF line endings, UTF-8 encoding
|
||||
- 160 character line limit
|
||||
- Trim trailing whitespace (except Markdown)
|
||||
- End files with newline
|
||||
|
||||
## 3. Dual Implementation Testing (NEW - CRITICAL)
|
||||
|
||||
### 3a. Neovim Lua Implementation Testing
|
||||
|
||||
```bash
|
||||
# Test in Neovim 0.7+
|
||||
nvim test_example.spec.sh
|
||||
|
||||
# Verify Lua path is used
|
||||
:lua print("Using Lua implementation")
|
||||
:checkhealth shellspec
|
||||
|
||||
# Test formatting with HEREDOC
|
||||
:ShellSpecFormat
|
||||
|
||||
# Test configuration
|
||||
:lua require('shellspec').setup({auto_format = true})
|
||||
```
|
||||
|
||||
### 3b. VimScript Fallback Testing
|
||||
|
||||
```bash
|
||||
# Test in older Neovim or Vim
|
||||
vim test_example.spec.sh # or nvim --clean with older version
|
||||
|
||||
# Verify VimScript path is used
|
||||
:echo "Using VimScript implementation"
|
||||
|
||||
# Test same formatting features work
|
||||
:ShellSpecFormat
|
||||
```
|
||||
|
||||
## 4. Advanced Formatting Feature Testing
|
||||
|
||||
### 4a. HEREDOC Preservation Testing
|
||||
|
||||
Create test content:
|
||||
|
||||
```shellspec
|
||||
Describe "HEREDOC test"
|
||||
It "preserves formatting"
|
||||
cat <<EOF
|
||||
This should stay as-is
|
||||
Even with nested indentation
|
||||
Back to normal
|
||||
EOF
|
||||
End
|
||||
End
|
||||
```
|
||||
|
||||
Apply `:ShellSpecFormat` and verify HEREDOC content is unchanged.
|
||||
|
||||
### 4b. Comment Indentation Testing
|
||||
|
||||
Create test content:
|
||||
|
||||
```shellspec
|
||||
Describe "Comment test"
|
||||
# Top level comment
|
||||
It "handles comments"
|
||||
# This should be indented to It level
|
||||
When call echo "test"
|
||||
# This should be indented to When level
|
||||
End
|
||||
# Back to top level
|
||||
End
|
||||
```
|
||||
|
||||
Apply `:ShellSpecFormat` and verify comments align with code levels.
|
||||
|
||||
## 5. Configuration Testing
|
||||
|
||||
### 5a. Lua Configuration (Neovim)
|
||||
|
||||
```lua
|
||||
-- Test different configurations
|
||||
require('shellspec').setup({
|
||||
auto_format = true,
|
||||
indent_size = 4,
|
||||
indent_comments = false,
|
||||
})
|
||||
```
|
||||
|
||||
### 5b. VimScript Configuration (Vim/Legacy)
|
||||
|
||||
```vim
|
||||
let g:shellspec_auto_format = 1
|
||||
let g:shellspec_indent_comments = 0
|
||||
```
|
||||
|
||||
## 6. Plugin-Specific Testing
|
||||
|
||||
### Manual Testing Steps
|
||||
|
||||
1. **Create test ShellSpec file**:
|
||||
|
||||
```bash
|
||||
cp test_example.spec.sh my_test.spec.sh
|
||||
```
|
||||
|
||||
2. **Test filetype detection**:
|
||||
|
||||
```vim
|
||||
# In Neovim/Vim, open the file and verify:
|
||||
:set filetype? # Should show "filetype=shellspec"
|
||||
```
|
||||
|
||||
3. **Test syntax highlighting**:
|
||||
- Add ShellSpec DSL content and verify highlighting
|
||||
- Test with HEREDOC blocks
|
||||
- Test with various comment styles
|
||||
|
||||
4. **Test formatting commands**:
|
||||
|
||||
```vim
|
||||
:ShellSpecFormat
|
||||
:ShellSpecFormatRange (in visual mode)
|
||||
```
|
||||
|
||||
5. **Test auto-format on save** (if enabled):
|
||||
- Make changes and save file
|
||||
- Verify automatic formatting occurs
|
||||
|
||||
6. **Test health check** (Neovim only):
|
||||
|
||||
```vim
|
||||
:checkhealth shellspec
|
||||
```
|
||||
|
||||
## 7. Standalone Formatter Testing
|
||||
|
||||
```bash
|
||||
# Test the standalone formatter
|
||||
echo 'Describe "test"
|
||||
# Comment
|
||||
It "works"
|
||||
cat <<EOF
|
||||
preserved
|
||||
EOF
|
||||
When call echo
|
||||
The output should equal
|
||||
End
|
||||
End' | ./bin/shellspec-format
|
||||
```
|
||||
|
||||
## 8. Performance Testing (NEW)
|
||||
|
||||
```bash
|
||||
# Test with larger files
|
||||
time nvim +':ShellSpecFormat' +':wq' large_spec_file.spec.sh
|
||||
|
||||
# Compare implementations if possible
|
||||
```
|
||||
|
||||
## 9. Module Integration Testing (Neovim)
|
||||
|
||||
```lua
|
||||
-- Test module loading
|
||||
:lua local ok, mod = pcall(require, 'shellspec'); print(ok)
|
||||
:lua print(vim.inspect(require('shellspec.config').defaults))
|
||||
:lua require('shellspec.format').format_lines({"test"})
|
||||
```
|
||||
|
||||
## 10. Git Workflow
|
||||
|
||||
```bash
|
||||
# Stage changes
|
||||
git add .
|
||||
|
||||
# Commit (pre-commit hooks run automatically)
|
||||
git commit -m "descriptive commit message"
|
||||
|
||||
# Ensure both implementations are included in commit
|
||||
git log --name-status -1
|
||||
```
|
||||
|
||||
## 11. Documentation Updates
|
||||
|
||||
- Update README.md if adding new features
|
||||
- Include both Lua and VimScript examples
|
||||
- Document any breaking changes
|
||||
- Update health check descriptions if modified
|
||||
- Ensure all configuration examples are correct
|
||||
|
||||
## Error Resolution Priority
|
||||
|
||||
1. **EditorConfig violations** - Fix immediately (blocking)
|
||||
2. **Dual implementation failures** - Both Lua and VimScript must work
|
||||
3. **HEREDOC/Comment formatting issues** - Core feature failures
|
||||
4. **ShellCheck errors** - Fix all warnings and errors
|
||||
5. **Health check failures** - Neovim integration issues
|
||||
6. **YAML/JSON syntax errors** - Must be valid
|
||||
7. **Markdownlint issues** - Fix formatting and style issues
|
||||
|
||||
## Before Pull Request
|
||||
|
||||
- [ ] All linting passes without errors
|
||||
- [ ] Both Lua (Neovim) and VimScript (Vim) implementations tested
|
||||
- [ ] HEREDOC preservation verified
|
||||
- [ ] Comment indentation working correctly
|
||||
- [ ] Health check passes (`:checkhealth shellspec`)
|
||||
- [ ] Manual plugin testing completed
|
||||
- [ ] Documentation is updated with dual examples
|
||||
- [ ] Commit messages are descriptive
|
||||
- [ ] No sensitive information in commits
|
||||
|
||||
## Regression Testing
|
||||
|
||||
When modifying core formatting logic:
|
||||
|
||||
- [ ] Test with complex nested ShellSpec structures
|
||||
- [ ] Test with mixed HEREDOC types (`<<EOF`, `<<'EOF'`, `<<"EOF"`)
|
||||
- [ ] Test with edge cases (empty files, comment-only files)
|
||||
- [ ] Test auto-format behavior
|
||||
- [ ] Test with different indent_size configurations
|
||||
2
.serena/project.yml
Normal file
2
.serena/project.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
project_name: nvim-shellspec
|
||||
language: bash
|
||||
107
README.md
107
README.md
@@ -1,6 +1,6 @@
|
||||
# Neovim ShellSpec DSL Support
|
||||
|
||||
Language support and formatter for ShellSpec DSL testing framework.
|
||||
Advanced language support and formatter for ShellSpec DSL testing framework with first-class Neovim support.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -10,6 +10,13 @@ Language support and formatter for ShellSpec DSL testing framework.
|
||||
{
|
||||
"ivuorinen/nvim-shellspec",
|
||||
ft = "shellspec",
|
||||
config = function()
|
||||
require("shellspec").setup({
|
||||
auto_format = true,
|
||||
indent_size = 2,
|
||||
indent_comments = true,
|
||||
})
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -27,10 +34,19 @@ git clone https://github.com/ivuorinen/nvim-shellspec.git ~/.config/nvim/pack/pl
|
||||
|
||||
## Features
|
||||
|
||||
- **Syntax highlighting** for all ShellSpec DSL keywords
|
||||
- **Automatic indentation** for block structures
|
||||
- **Filetype detection** for `*_spec.sh`, `*.spec.sh`, and `spec/*.sh`
|
||||
- **Formatting commands** with proper indentation
|
||||
- **🚀 First-class Neovim support** with modern Lua implementation
|
||||
- **🎨 Syntax highlighting** for all ShellSpec DSL keywords
|
||||
- **📐 Smart indentation** for block structures
|
||||
- **📄 Enhanced filetype detection** for `*_spec.sh`, `*.spec.sh`, `spec/*.sh`, and `test/*.sh`
|
||||
- **✨ Advanced formatting** with HEREDOC and comment support
|
||||
- **⚡ Async formatting** to prevent blocking (Neovim 0.7+)
|
||||
- **🔄 Backward compatibility** with Vim and older Neovim versions
|
||||
|
||||
### Advanced Formatting Features
|
||||
|
||||
- **HEREDOC Preservation**: Maintains original formatting within `<<EOF`, `<<'EOF'`, `<<"EOF"`, and `<<-EOF` blocks
|
||||
- **Smart Comment Indentation**: Comments are indented to match surrounding code level
|
||||
- **Context-Aware Formatting**: State machine tracks formatting context for accurate indentation
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -39,14 +55,6 @@ git clone https://github.com/ivuorinen/nvim-shellspec.git ~/.config/nvim/pack/pl
|
||||
- `:ShellSpecFormat` - Format entire buffer
|
||||
- `:ShellSpecFormatRange` - Format selected lines
|
||||
|
||||
### Auto-format
|
||||
|
||||
Add to your config to enable auto-format on save:
|
||||
|
||||
```vim
|
||||
let g:shellspec_auto_format = 1
|
||||
```
|
||||
|
||||
### File Types
|
||||
|
||||
Plugin activates for files matching:
|
||||
@@ -55,18 +63,91 @@ Plugin activates for files matching:
|
||||
- `*.spec.sh`
|
||||
- `spec/*.sh`
|
||||
- `test/*.sh`
|
||||
- Files in nested `spec/` directories
|
||||
|
||||
## Configuration
|
||||
|
||||
### Neovim (Lua Configuration) - Recommended
|
||||
|
||||
```lua
|
||||
require("shellspec").setup({
|
||||
-- Auto-format on save
|
||||
auto_format = true,
|
||||
|
||||
-- Indentation settings
|
||||
indent_size = 2,
|
||||
use_spaces = true,
|
||||
|
||||
-- Comment indentation (align with code level)
|
||||
indent_comments = true,
|
||||
|
||||
-- HEREDOC patterns (customizable)
|
||||
heredoc_patterns = {
|
||||
"<<[A-Z_][A-Z0-9_]*", -- <<EOF, <<DATA, etc.
|
||||
"<<'[^']*'", -- <<'EOF'
|
||||
'<<"[^"]*"', -- <<"EOF"
|
||||
"<<-[A-Z_][A-Z0-9_]*", -- <<-EOF
|
||||
},
|
||||
|
||||
-- Other options
|
||||
preserve_empty_lines = true,
|
||||
max_line_length = 160,
|
||||
})
|
||||
|
||||
-- Custom keybindings
|
||||
vim.keymap.set('n', '<leader>sf', '<cmd>ShellSpecFormat<cr>', { desc = 'Format ShellSpec buffer' })
|
||||
vim.keymap.set('v', '<leader>sf', '<cmd>ShellSpecFormatRange<cr>', { desc = 'Format ShellSpec selection' })
|
||||
```
|
||||
|
||||
### Vim/Legacy Configuration
|
||||
|
||||
```vim
|
||||
" Enable auto-formatting on save
|
||||
let g:shellspec_auto_format = 1
|
||||
|
||||
" Enable comment indentation (default: 1)
|
||||
let g:shellspec_indent_comments = 1
|
||||
|
||||
" Custom keybindings
|
||||
autocmd FileType shellspec nnoremap <buffer> <leader>f :ShellSpecFormat<CR>
|
||||
autocmd FileType shellspec vnoremap <buffer> <leader>f :ShellSpecFormatRange<CR>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### HEREDOC Formatting
|
||||
|
||||
The formatter intelligently handles HEREDOC blocks:
|
||||
|
||||
```shellspec
|
||||
Describe "HEREDOC handling"
|
||||
It "preserves original formatting within HEREDOC"
|
||||
When call cat <<EOF
|
||||
This indentation is preserved
|
||||
Even nested indentation
|
||||
And this too
|
||||
EOF
|
||||
The output should equal expected
|
||||
End
|
||||
End
|
||||
```
|
||||
|
||||
### Comment Indentation
|
||||
|
||||
Comments are properly aligned with surrounding code:
|
||||
|
||||
```shellspec
|
||||
Describe "Comment handling"
|
||||
# This comment is indented to match the block level
|
||||
It "should handle comments correctly"
|
||||
# This comment matches the It block indentation
|
||||
When call echo "test"
|
||||
The output should equal "test"
|
||||
End
|
||||
# Back to Describe level indentation
|
||||
End
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please open issues and pull requests at:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
" ShellSpec DSL formatter functions
|
||||
" ShellSpec DSL formatter functions with HEREDOC and comment support
|
||||
|
||||
function! shellspec#format_buffer() abort
|
||||
let l:pos = getpos('.')
|
||||
@@ -10,25 +10,83 @@ function! shellspec#format_buffer() abort
|
||||
call setpos('.', l:pos)
|
||||
endfunction
|
||||
|
||||
" Detect HEREDOC start and return delimiter
|
||||
function! s:detect_heredoc_start(line) abort
|
||||
let l:trimmed = trim(a:line)
|
||||
|
||||
" Check for various HEREDOC patterns
|
||||
let l:patterns = [
|
||||
\ '<<\([A-Z_][A-Z0-9_]*\)',
|
||||
\ "<<'\([^']*\)'",
|
||||
\ '<<"\([^"]*\)"',
|
||||
\ '<<-\([A-Z_][A-Z0-9_]*\)'
|
||||
\ ]
|
||||
|
||||
for l:pattern in l:patterns
|
||||
let l:match = matchlist(l:trimmed, l:pattern)
|
||||
if !empty(l:match)
|
||||
return l:match[1]
|
||||
endif
|
||||
endfor
|
||||
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
" Check if line ends a HEREDOC
|
||||
function! s:is_heredoc_end(line, delimiter) abort
|
||||
if empty(a:delimiter)
|
||||
return 0
|
||||
endif
|
||||
return trim(a:line) ==# a:delimiter
|
||||
endfunction
|
||||
|
||||
" Enhanced format_lines with HEREDOC and comment support
|
||||
function! shellspec#format_lines(lines) abort
|
||||
let l:result = []
|
||||
let l:indent = 0
|
||||
let l:state = 'normal' " States: normal, heredoc
|
||||
let l:heredoc_delimiter = ''
|
||||
let l:indent_comments = get(g:, 'shellspec_indent_comments', 1)
|
||||
|
||||
for l:line in a:lines
|
||||
let l:trimmed = trim(l:line)
|
||||
|
||||
" Skip empty lines and comments
|
||||
if l:trimmed == '' || l:trimmed =~ '^#'
|
||||
" Handle empty lines
|
||||
if l:trimmed == ''
|
||||
call add(l:result, l:line)
|
||||
continue
|
||||
endif
|
||||
|
||||
" Decrease indent for End
|
||||
if l:trimmed =~ '^End\s*$'
|
||||
let l:indent = max([0, l:indent - 1])
|
||||
" State machine for HEREDOC handling
|
||||
if l:state ==# 'normal'
|
||||
" Check for HEREDOC start
|
||||
let l:delimiter = s:detect_heredoc_start(l:line)
|
||||
if !empty(l:delimiter)
|
||||
let l:state = 'heredoc'
|
||||
let l:heredoc_delimiter = l:delimiter
|
||||
" Apply current indentation to HEREDOC start line
|
||||
let l:formatted = repeat(' ', l:indent) . l:trimmed
|
||||
call add(l:result, l:formatted)
|
||||
continue
|
||||
endif
|
||||
|
||||
" Apply current indentation
|
||||
" Handle comments with proper indentation
|
||||
if l:trimmed =~ '^#' && l:indent_comments
|
||||
let l:formatted = repeat(' ', l:indent) . l:trimmed
|
||||
call add(l:result, l:formatted)
|
||||
continue
|
||||
endif
|
||||
|
||||
" Handle End keyword (decrease indent first)
|
||||
if l:trimmed =~ '^End\s*$'
|
||||
let l:indent = max([0, l:indent - 1])
|
||||
let l:formatted = repeat(' ', l:indent) . l:trimmed
|
||||
call add(l:result, l:formatted)
|
||||
continue
|
||||
endif
|
||||
|
||||
" Apply normal indentation for other lines
|
||||
if l:trimmed !~ '^#' || !l:indent_comments
|
||||
let l:formatted = repeat(' ', l:indent) . l:trimmed
|
||||
call add(l:result, l:formatted)
|
||||
|
||||
@@ -40,6 +98,24 @@ function! shellspec#format_lines(lines) abort
|
||||
elseif l:trimmed =~ '^\(Data\|Parameters\)\s*$'
|
||||
let l:indent += 1
|
||||
endif
|
||||
else
|
||||
" Preserve original comment formatting if indent_comments is false
|
||||
call add(l:result, l:line)
|
||||
endif
|
||||
|
||||
elseif l:state ==# 'heredoc'
|
||||
" Check for HEREDOC end
|
||||
if s:is_heredoc_end(l:line, l:heredoc_delimiter)
|
||||
let l:state = 'normal'
|
||||
let l:heredoc_delimiter = ''
|
||||
" Apply current indentation to HEREDOC end line
|
||||
let l:formatted = repeat(' ', l:indent) . l:trimmed
|
||||
call add(l:result, l:formatted)
|
||||
else
|
||||
" Preserve original indentation within HEREDOC
|
||||
call add(l:result, l:line)
|
||||
endif
|
||||
endif
|
||||
endfor
|
||||
|
||||
return l:result
|
||||
|
||||
@@ -6,7 +6,8 @@ format_shellspec() {
|
||||
local line
|
||||
|
||||
while IFS= read -r line; do
|
||||
local trimmed=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
local trimmed
|
||||
trimmed=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
# Skip empty lines and comments
|
||||
if [[ -z "$trimmed" || "$trimmed" =~ ^# ]]; then
|
||||
@@ -37,7 +38,7 @@ if [[ $# -eq 0 ]]; then
|
||||
else
|
||||
for file in "$@"; do
|
||||
if [[ -f "$file" ]]; then
|
||||
format_shellspec < "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
|
||||
format_shellspec <"$file" >"${file}.tmp" && mv "${file}.tmp" "$file"
|
||||
else
|
||||
echo "Error: File not found: $file" >&2
|
||||
fi
|
||||
|
||||
102
lua/shellspec/autocmds.lua
Normal file
102
lua/shellspec/autocmds.lua
Normal file
@@ -0,0 +1,102 @@
|
||||
-- Neovim-native autocommands for ShellSpec
|
||||
local config = require("shellspec.config")
|
||||
local format = require("shellspec.format")
|
||||
local M = {}
|
||||
|
||||
-- Autocommand group
|
||||
local augroup = vim.api.nvim_create_augroup("ShellSpec", { clear = true })
|
||||
|
||||
-- Setup buffer-local settings
|
||||
local function setup_buffer(bufnr)
|
||||
-- Set buffer options
|
||||
vim.api.nvim_set_option_value("commentstring", "# %s", { buf = bufnr })
|
||||
vim.api.nvim_set_option_value("foldmethod", "indent", { buf = bufnr })
|
||||
vim.api.nvim_set_option_value("shiftwidth", config.get("indent_size"), { buf = bufnr })
|
||||
vim.api.nvim_set_option_value("tabstop", config.get("indent_size"), { buf = bufnr })
|
||||
vim.api.nvim_set_option_value("expandtab", config.get("use_spaces"), { buf = bufnr })
|
||||
|
||||
-- Buffer-local commands
|
||||
vim.api.nvim_buf_create_user_command(bufnr, "ShellSpecFormat", function()
|
||||
format.format_buffer(bufnr)
|
||||
end, { desc = "Format ShellSpec buffer" })
|
||||
|
||||
vim.api.nvim_buf_create_user_command(bufnr, "ShellSpecFormatRange", function(opts)
|
||||
format.format_selection(bufnr, opts.line1, opts.line2)
|
||||
end, {
|
||||
range = true,
|
||||
desc = "Format ShellSpec selection",
|
||||
})
|
||||
|
||||
-- Optional: Set up LSP-style formatting
|
||||
if vim.fn.has("nvim-0.8") == 1 then
|
||||
vim.api.nvim_buf_set_option(bufnr, "formatexpr", 'v:lua.require("shellspec.format").format_buffer()')
|
||||
end
|
||||
end
|
||||
|
||||
-- Create all autocommands
|
||||
function M.setup()
|
||||
-- FileType detection and setup
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
group = augroup,
|
||||
pattern = "shellspec",
|
||||
callback = function(args)
|
||||
setup_buffer(args.buf)
|
||||
end,
|
||||
desc = "Setup ShellSpec buffer",
|
||||
})
|
||||
|
||||
-- Auto-format on save (if enabled)
|
||||
if config.get("auto_format") then
|
||||
vim.api.nvim_create_autocmd("BufWritePre", {
|
||||
group = augroup,
|
||||
pattern = { "*.spec.sh", "*_spec.sh" },
|
||||
callback = function(args)
|
||||
-- Only format if it's a shellspec buffer
|
||||
local filetype = vim.api.nvim_get_option_value("filetype", { buf = args.buf })
|
||||
if filetype == "shellspec" then
|
||||
format.format_buffer(args.buf)
|
||||
end
|
||||
end,
|
||||
desc = "Auto-format ShellSpec files on save",
|
||||
})
|
||||
end
|
||||
|
||||
-- Enhanced filetype detection with better patterns
|
||||
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
|
||||
group = augroup,
|
||||
pattern = {
|
||||
"*_spec.sh",
|
||||
"*.spec.sh",
|
||||
"spec/*.sh",
|
||||
"test/*.sh",
|
||||
},
|
||||
callback = function(args)
|
||||
-- Set filetype to shellspec
|
||||
vim.api.nvim_set_option_value("filetype", "shellspec", { buf = args.buf })
|
||||
end,
|
||||
desc = "Detect ShellSpec files",
|
||||
})
|
||||
|
||||
-- Additional pattern for nested spec directories
|
||||
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
|
||||
group = augroup,
|
||||
pattern = "**/spec/**/*.sh",
|
||||
callback = function(args)
|
||||
vim.api.nvim_set_option_value("filetype", "shellspec", { buf = args.buf })
|
||||
end,
|
||||
desc = "Detect ShellSpec files in nested spec directories",
|
||||
})
|
||||
end
|
||||
|
||||
-- Cleanup function
|
||||
function M.cleanup()
|
||||
vim.api.nvim_clear_autocmds({ group = augroup })
|
||||
end
|
||||
|
||||
-- Update configuration and refresh autocommands
|
||||
function M.refresh()
|
||||
M.cleanup()
|
||||
M.setup()
|
||||
end
|
||||
|
||||
return M
|
||||
51
lua/shellspec/config.lua
Normal file
51
lua/shellspec/config.lua
Normal file
@@ -0,0 +1,51 @@
|
||||
-- ShellSpec configuration management
|
||||
local M = {}
|
||||
|
||||
-- Default configuration
|
||||
M.defaults = {
|
||||
-- Auto-format on save
|
||||
auto_format = false,
|
||||
|
||||
-- Indentation settings
|
||||
indent_size = 2,
|
||||
use_spaces = true,
|
||||
|
||||
-- HEREDOC handling
|
||||
heredoc_patterns = {
|
||||
"<<[A-Z_][A-Z0-9_]*", -- <<EOF, <<DATA, etc.
|
||||
"<<'[^']*'", -- <<'EOF'
|
||||
'<<"[^"]*"', -- <<"EOF"
|
||||
"<<-[A-Z_][A-Z0-9_]*", -- <<-EOF (with leading tab removal)
|
||||
},
|
||||
|
||||
-- Comment indentation
|
||||
indent_comments = true,
|
||||
|
||||
-- Formatting options
|
||||
preserve_empty_lines = true,
|
||||
max_line_length = 160,
|
||||
}
|
||||
|
||||
-- Current configuration
|
||||
M.config = {}
|
||||
|
||||
-- Setup function
|
||||
function M.setup(opts)
|
||||
M.config = vim.tbl_deep_extend("force", M.defaults, opts or {})
|
||||
|
||||
-- Validate configuration
|
||||
if type(M.config.indent_size) ~= "number" or M.config.indent_size < 1 then
|
||||
vim.notify("shellspec: indent_size must be a positive number", vim.log.levels.WARN)
|
||||
M.config.indent_size = M.defaults.indent_size
|
||||
end
|
||||
end
|
||||
|
||||
-- Get configuration value
|
||||
function M.get(key)
|
||||
return M.config[key]
|
||||
end
|
||||
|
||||
-- Initialize with defaults
|
||||
M.setup()
|
||||
|
||||
return M
|
||||
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
|
||||
112
lua/shellspec/health.lua
Normal file
112
lua/shellspec/health.lua
Normal file
@@ -0,0 +1,112 @@
|
||||
-- Health check for shellspec.nvim
|
||||
local M = {}
|
||||
|
||||
function M.check()
|
||||
local health = vim.health or require("health")
|
||||
|
||||
health.report_start("ShellSpec.nvim")
|
||||
|
||||
-- Check Neovim version
|
||||
local nvim_version = vim.version()
|
||||
if nvim_version.major > 0 or nvim_version.minor >= 7 then
|
||||
health.report_ok(string.format("Neovim version %d.%d.%d >= 0.7.0", nvim_version.major, nvim_version.minor, nvim_version.patch))
|
||||
else
|
||||
health.report_warn(string.format("Neovim version %d.%d.%d < 0.7.0, some features may not work", nvim_version.major, nvim_version.minor, nvim_version.patch))
|
||||
end
|
||||
|
||||
-- Check if module can be loaded
|
||||
local ok, config = pcall(require, "shellspec.config")
|
||||
if ok then
|
||||
health.report_ok("ShellSpec configuration module loaded successfully")
|
||||
|
||||
-- Report current configuration
|
||||
local current_config = config.config
|
||||
if current_config then
|
||||
health.report_info("Configuration:")
|
||||
health.report_info(" Auto-format: " .. tostring(current_config.auto_format))
|
||||
health.report_info(" Indent size: " .. tostring(current_config.indent_size))
|
||||
health.report_info(" Use spaces: " .. tostring(current_config.use_spaces))
|
||||
health.report_info(" Indent comments: " .. tostring(current_config.indent_comments))
|
||||
end
|
||||
else
|
||||
health.report_error("Failed to load ShellSpec configuration: " .. config)
|
||||
return
|
||||
end
|
||||
|
||||
-- Check formatting module
|
||||
local ok_format, format = pcall(require, "shellspec.format")
|
||||
if ok_format then
|
||||
health.report_ok("ShellSpec formatting module loaded successfully")
|
||||
else
|
||||
health.report_error("Failed to load ShellSpec formatting module: " .. format)
|
||||
end
|
||||
|
||||
-- Check autocommands module
|
||||
local ok_autocmds, autocmds = pcall(require, "shellspec.autocmds")
|
||||
if ok_autocmds then
|
||||
health.report_ok("ShellSpec autocommands module loaded successfully")
|
||||
else
|
||||
health.report_error("Failed to load ShellSpec autocommands module: " .. autocmds)
|
||||
end
|
||||
|
||||
-- Check if we're in a ShellSpec buffer
|
||||
local filetype = vim.bo.filetype
|
||||
if filetype == "shellspec" then
|
||||
health.report_ok("Current buffer is ShellSpec filetype")
|
||||
|
||||
-- Check buffer-local settings
|
||||
local shiftwidth = vim.bo.shiftwidth
|
||||
local expandtab = vim.bo.expandtab
|
||||
local commentstring = vim.bo.commentstring
|
||||
|
||||
health.report_info("Buffer settings:")
|
||||
health.report_info(" shiftwidth: " .. tostring(shiftwidth))
|
||||
health.report_info(" expandtab: " .. tostring(expandtab))
|
||||
health.report_info(" commentstring: " .. tostring(commentstring))
|
||||
else
|
||||
health.report_info("Current buffer filetype: " .. (filetype or "none"))
|
||||
health.report_info("Open a ShellSpec file (*.spec.sh) to test buffer-specific features")
|
||||
end
|
||||
|
||||
-- Check for common ShellSpec files in project
|
||||
local cwd = vim.fn.getcwd()
|
||||
local spec_dirs = { "spec", "test" }
|
||||
local found_specs = false
|
||||
|
||||
for _, dir in ipairs(spec_dirs) do
|
||||
local spec_dir = cwd .. "/" .. dir
|
||||
if vim.fn.isdirectory(spec_dir) == 1 then
|
||||
local files = vim.fn.glob(spec_dir .. "/*.sh", false, true)
|
||||
if #files > 0 then
|
||||
found_specs = true
|
||||
health.report_ok("Found " .. #files .. " ShellSpec files in " .. dir .. "/")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not found_specs then
|
||||
local spec_files = vim.fn.glob("**/*_spec.sh", false, true)
|
||||
local spec_files2 = vim.fn.glob("**/*.spec.sh", false, true)
|
||||
local total_specs = #spec_files + #spec_files2
|
||||
|
||||
if total_specs > 0 then
|
||||
health.report_ok("Found " .. total_specs .. " ShellSpec files in project")
|
||||
else
|
||||
health.report_info("No ShellSpec files found in current directory")
|
||||
health.report_info("ShellSpec files typically match: *_spec.sh, *.spec.sh, spec/*.sh")
|
||||
end
|
||||
end
|
||||
|
||||
-- Check commands availability
|
||||
local commands = { "ShellSpecFormat", "ShellSpecFormatRange" }
|
||||
for _, cmd in ipairs(commands) do
|
||||
if vim.fn.exists(":" .. cmd) == 2 then
|
||||
health.report_ok("Command :" .. cmd .. " is available")
|
||||
else
|
||||
health.report_error("Command :" .. cmd .. " is not available")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
90
lua/shellspec/init.lua
Normal file
90
lua/shellspec/init.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
-- Main ShellSpec module for Neovim
|
||||
local M = {}
|
||||
|
||||
-- Lazy-load submodules
|
||||
local config = require("shellspec.config")
|
||||
local format = require("shellspec.format")
|
||||
local autocmds = require("shellspec.autocmds")
|
||||
|
||||
-- Version info
|
||||
M._VERSION = "2.0.0"
|
||||
|
||||
-- Setup function for Lua configuration
|
||||
function M.setup(opts)
|
||||
opts = opts or {}
|
||||
|
||||
-- Setup configuration
|
||||
config.setup(opts)
|
||||
|
||||
-- Setup autocommands
|
||||
autocmds.setup()
|
||||
|
||||
-- Create global commands for compatibility
|
||||
vim.api.nvim_create_user_command("ShellSpecFormat", function()
|
||||
format.format_buffer()
|
||||
end, { desc = "Format current ShellSpec buffer" })
|
||||
|
||||
vim.api.nvim_create_user_command("ShellSpecFormatRange", function(cmd_opts)
|
||||
format.format_selection(0, cmd_opts.line1, cmd_opts.line2)
|
||||
end, {
|
||||
range = true,
|
||||
desc = "Format ShellSpec selection",
|
||||
})
|
||||
|
||||
-- Optional: Enable auto-format if configured
|
||||
if config.get("auto_format") then
|
||||
autocmds.refresh() -- Refresh to pick up auto-format settings
|
||||
end
|
||||
end
|
||||
|
||||
-- Format functions (for external use)
|
||||
M.format_buffer = format.format_buffer
|
||||
M.format_selection = format.format_selection
|
||||
M.format_lines = format.format_lines
|
||||
|
||||
-- Configuration access
|
||||
M.config = config
|
||||
|
||||
-- Health check function for :checkhealth
|
||||
function M.health()
|
||||
local health = vim.health or require("health")
|
||||
|
||||
health.report_start("ShellSpec.nvim")
|
||||
|
||||
-- Check Neovim version
|
||||
if vim.fn.has("nvim-0.7") == 1 then
|
||||
health.report_ok("Neovim version >= 0.7.0")
|
||||
else
|
||||
health.report_warn("Neovim version < 0.7.0, some features may not work")
|
||||
end
|
||||
|
||||
-- Check configuration
|
||||
local current_config = config.config
|
||||
if current_config then
|
||||
health.report_ok("Configuration loaded successfully")
|
||||
health.report_info("Auto-format: " .. tostring(current_config.auto_format))
|
||||
health.report_info("Indent size: " .. tostring(current_config.indent_size))
|
||||
health.report_info("Use spaces: " .. tostring(current_config.use_spaces))
|
||||
else
|
||||
health.report_error("Configuration not loaded")
|
||||
end
|
||||
|
||||
-- Check if in ShellSpec buffer
|
||||
local filetype = vim.bo.filetype
|
||||
if filetype == "shellspec" then
|
||||
health.report_ok("Current buffer is ShellSpec filetype")
|
||||
else
|
||||
health.report_info("Current buffer filetype: " .. (filetype or "none"))
|
||||
end
|
||||
end
|
||||
|
||||
-- Backward compatibility function for VimScript
|
||||
function M.format_buffer_compat()
|
||||
format.format_buffer()
|
||||
end
|
||||
|
||||
function M.format_selection_compat(start_line, end_line)
|
||||
format.format_selection(0, start_line, end_line)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -8,22 +8,37 @@ if exists('g:loaded_shellspec')
|
||||
endif
|
||||
let g:loaded_shellspec = 1
|
||||
|
||||
" Commands
|
||||
command! ShellSpecFormat call shellspec#format_buffer()
|
||||
command! -range ShellSpecFormatRange call shellspec#format_selection()
|
||||
" Detect Neovim and use appropriate implementation
|
||||
if has('nvim-0.7')
|
||||
" Use modern Neovim Lua implementation
|
||||
lua require('shellspec.autocmds').setup()
|
||||
|
||||
" Auto commands
|
||||
augroup ShellSpec
|
||||
" Create commands that delegate to Lua
|
||||
command! ShellSpecFormat lua require('shellspec').format_buffer()
|
||||
command! -range ShellSpecFormatRange lua require('shellspec').format_selection(0, <line1>, <line2>)
|
||||
|
||||
" Optional: Auto-format on save (handled in Lua)
|
||||
" This is now managed by the Lua autocmds module based on configuration
|
||||
|
||||
else
|
||||
" Fallback to VimScript implementation for older Vim
|
||||
" Commands
|
||||
command! ShellSpecFormat call shellspec#format_buffer()
|
||||
command! -range ShellSpecFormatRange call shellspec#format_selection()
|
||||
|
||||
" Auto commands
|
||||
augroup ShellSpec
|
||||
autocmd!
|
||||
autocmd FileType shellspec setlocal commentstring=#\ %s
|
||||
autocmd FileType shellspec setlocal foldmethod=indent
|
||||
autocmd FileType shellspec setlocal shiftwidth=2 tabstop=2 expandtab
|
||||
augroup END
|
||||
augroup END
|
||||
|
||||
" Optional: Auto-format on save
|
||||
if get(g:, 'shellspec_auto_format', 0)
|
||||
" Optional: Auto-format on save
|
||||
if get(g:, 'shellspec_auto_format', 0)
|
||||
augroup ShellSpecAutoFormat
|
||||
autocmd!
|
||||
autocmd BufWritePre *.spec.sh,*_spec.sh ShellSpecFormat
|
||||
augroup END
|
||||
endif
|
||||
endif
|
||||
|
||||
35
test_example.spec.sh
Executable file
35
test_example.spec.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
Describe "ShellSpec formatting test"
|
||||
# This is a top-level comment
|
||||
Context "when testing HEREDOC support"
|
||||
# Comment inside Context
|
||||
It "should preserve HEREDOC formatting"
|
||||
# Comment inside It block
|
||||
When call cat <<EOF
|
||||
This indentation should be preserved
|
||||
Even with deeper indentation
|
||||
Back to original level
|
||||
EOF
|
||||
The output should include "preserved"
|
||||
End
|
||||
|
||||
It "should handle quoted HEREDOC"
|
||||
When call cat <<'DATA'
|
||||
# This comment inside HEREDOC should not be touched
|
||||
Some $variable should not be expanded
|
||||
DATA
|
||||
The output should include "variable"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing regular formatting"
|
||||
# Another context comment
|
||||
It "should indent comments properly"
|
||||
# This comment should be indented to It level
|
||||
When call echo "test"
|
||||
# Another comment at It level
|
||||
The output should equal "test"
|
||||
End
|
||||
End
|
||||
End
|
||||
Reference in New Issue
Block a user