feat: add interactive attribute block toggle with keybindings and commands
Add ability to expand/collapse SCL variable attribute blocks like {EXTERNALACCESSIBLE := 'false'} to {...}
Features:
- Per-variable collapse rules via collapseVariableRules option
- Interactive toggle with <Leader>xa keybinding
- Commands: SCLToggleAttrBlock, SCLExpandAllAttrBlocks, SCLCollapseAllAttrBlocks
- Keybindings: <Leader>xa (toggle), <Leader>xae (expand all), <Leader>xac (collapse all)
- Supports both formatter-collapsed and manually collapsed blocks
Also update AGENTS.md with new documentation and troubleshooting guide
This commit is contained in:
@@ -1,59 +1,63 @@
|
|||||||
# AGENTS.md - SCL Language Server (Unified)
|
# AGENTS.md - SCL Language Server
|
||||||
|
|
||||||
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
|
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
|
||||||
|
|
||||||
## Reference Project for Testing
|
|
||||||
|
|
||||||
For manual testing and SCL/UDT/DB file examples, use:
|
|
||||||
```
|
|
||||||
~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build/Lint/Test Commands
|
## Build/Lint/Test Commands
|
||||||
|
|
||||||
### LSP Server
|
|
||||||
```bash
|
```bash
|
||||||
|
# LSP Server
|
||||||
make start # Start LSP server
|
make start # Start LSP server
|
||||||
make test # Run LSP in test mode
|
make test # Run LSP in test mode
|
||||||
lua src/main.lua --test # Direct test mode execution
|
lua src/main.lua --test # Direct test execution
|
||||||
```
|
|
||||||
|
|
||||||
### Tree-sitter Parser
|
# Tree-sitter Parser
|
||||||
```bash
|
|
||||||
tree-sitter generate # Generate parser from grammar.js
|
tree-sitter generate # Generate parser from grammar.js
|
||||||
tree-sitter test # Run tree-sitter tests
|
tree-sitter test # Run parser tests
|
||||||
tree-sitter parse <file.scl> # Parse a single SCL file
|
tree-sitter parse <file.scl> # Parse single SCL file
|
||||||
|
|
||||||
|
# Lua Linting
|
||||||
|
luacheck src/ # Lint LSP server code
|
||||||
|
luacheck lua/scl/ # Lint plugin modules
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
scl_lsp/
|
scl_lsp/
|
||||||
├── src/ # Standalone LSP server (uses dofile)
|
├── src/ # LSP server (uses dofile)
|
||||||
│ ├── main.lua # Entry point, JSON-RPC protocol
|
│ ├── main.lua # Entry point, JSON-RPC protocol
|
||||||
│ ├── parser.lua # Regex-based SCL parser
|
│ ├── parser.lua # SCL parser
|
||||||
│ ├── diagnostics.lua # Linter diagnostics
|
│ ├── diagnostics.lua # Linter
|
||||||
│ └── formatter.lua # Document formatter
|
│ ├── formatter.lua # Document formatter
|
||||||
├── lua/scl/ # Neovim plugin modules (uses require)
|
│ ├── treesitter.lua # Tree-sitter integration
|
||||||
│ ├── blink_cmp_source.lua # blink.cmp completion source
|
│ ├── plc_json.lua # External UDT loading
|
||||||
│ ├── udt_parser.lua # UDT (.udt) parser
|
│ └── json.lua # JSON encoder/decoder
|
||||||
│ ├── db_parser.lua # Global DB (.db) parser
|
├── lua/scl/ # Neovim plugin (uses require)
|
||||||
|
│ ├── init.lua # Main plugin setup
|
||||||
|
│ ├── blink_cmp_source.lua
|
||||||
|
│ ├── udt_parser.lua # UDT parser
|
||||||
|
│ ├── db_parser.lua # Global DB parser
|
||||||
│ ├── fb_parser.lua # Function Block parser
|
│ ├── fb_parser.lua # Function Block parser
|
||||||
│ ├── variables.lua # Local variable extraction
|
│ ├── variables.lua # Variable extraction
|
||||||
│ ├── workspace_types.lua # Workspace scanning
|
│ ├── workspace_types.lua # Workspace scanning
|
||||||
│ └── multiline_params.lua # FB parameter filling
|
│ ├── multiline_params.lua
|
||||||
└── queries/ # Tree-sitter queries
|
│ ├── auto_prefix.lua
|
||||||
|
│ └── attr_toggle.lua # Interactive attribute block toggle
|
||||||
|
├── queries/ # Tree-sitter queries
|
||||||
|
│ ├── highlights.scm
|
||||||
|
│ ├── indents.scm
|
||||||
|
│ └── folds.scm
|
||||||
|
└── grammar.js # Tree-sitter grammar
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### Module Pattern
|
### Module Pattern
|
||||||
All modules follow the standard Lua module pattern:
|
|
||||||
```lua
|
```lua
|
||||||
-- Module description comment at top
|
-- File header comment
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Private module-level cache
|
-- Private cache
|
||||||
local cache = {}
|
local cache = {}
|
||||||
|
|
||||||
function M.public_function()
|
function M.public_function()
|
||||||
@@ -66,37 +70,37 @@ return M
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
- Functions/variables: `snake_case` (e.g., `get_udt_members`, `var_types`)
|
- Functions/variables: `snake_case` (e.g., `get_udt_members`)
|
||||||
- Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`)
|
- Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`)
|
||||||
- Private functions: declare as `local function` before public functions
|
- Private functions: declare as `local function` before public functions
|
||||||
- Boolean variables: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`)
|
- Booleans: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`)
|
||||||
|
|
||||||
### Imports
|
### Imports
|
||||||
- `lua/scl/` modules: use `require("scl.module_name")`
|
- `lua/scl/` modules: use `require("scl.module_name")`
|
||||||
- `src/` modules: use `dofile(script_path .. "/module.lua")`
|
- `src/` modules: use `dofile(script_path .. "/module.lua")`
|
||||||
- External dependencies: wrap in `pcall()` for safety
|
- External dependencies: wrap in `pcall()` for safety
|
||||||
|
|
||||||
### Comments
|
### Code Formatting
|
||||||
- Every file must have a header comment explaining its purpose
|
- `lua/scl/`: 2 spaces indentation
|
||||||
- All functions should have a brief comment describing what they do
|
- `src/`: tab-based indentation
|
||||||
- Complex logic requires inline comments explaining the "why"
|
- Max line length: 120 characters
|
||||||
- Non-obvious regex patterns must have explanatory comments
|
- No trailing whitespace
|
||||||
|
|
||||||
### Parser Module API Convention
|
### Parser Module API
|
||||||
All parser modules (udt_parser, db_parser, fb_parser) must implement:
|
All parser modules must implement:
|
||||||
- `parse_*_file(filepath)` - Parse a file and cache result
|
- `parse_*_file(filepath)` - Parse and cache
|
||||||
- `parse_*_content(content, filename)` - Parse string content
|
- `parse_*_content(content, filename)` - Parse string
|
||||||
- `get_*(name)` - Get single cached item by name
|
- `get_*(name)` - Get cached item
|
||||||
- `get_all_*_names()` - Return list of all cached names
|
- `get_all_*_names()` - List all cached names
|
||||||
- `get_*_members(name)` - Get members/fields for a type
|
- `get_*_members(name)` - Get members/fields
|
||||||
- `is_*_type(name)` - Check if name is a known type
|
- `is_*_type(name)` - Check if known type
|
||||||
- `clear_cache()` - Clear all cached data
|
- `clear_cache()` - Clear cached data
|
||||||
- `get_cache_count()` - Return number of cached items
|
- `get_cache_count()` - Return cache size
|
||||||
|
|
||||||
### Cache Management
|
### Cache Management
|
||||||
- Use module-level local tables for caching: `local cache = {}`
|
|
||||||
- Clear caches by iterating and setting to nil, not by reassigning:
|
|
||||||
```lua
|
```lua
|
||||||
|
local cache = {}
|
||||||
|
|
||||||
function M.clear_cache()
|
function M.clear_cache()
|
||||||
for k in pairs(cache) do
|
for k in pairs(cache) do
|
||||||
cache[k] = nil
|
cache[k] = nil
|
||||||
@@ -106,21 +110,20 @@ end
|
|||||||
|
|
||||||
## Lua Reserved Keywords
|
## Lua Reserved Keywords
|
||||||
|
|
||||||
Lua reserved keywords (like `end`, `for`, `in`, etc.) cannot be used as table keys directly. Use bracket notation:
|
Cannot use reserved keywords as table keys directly:
|
||||||
```lua
|
```lua
|
||||||
-- WRONG: causes syntax error
|
-- WRONG: syntax error
|
||||||
local range = { start = pos1, end = pos2 }
|
local range = { start = pos1, end = pos2 }
|
||||||
|
|
||||||
-- CORRECT: use bracket notation
|
-- CORRECT: bracket notation
|
||||||
local range = { start = pos1, ["end"] = pos2 }
|
local range = { start = pos1, ["end"] = pos2 }
|
||||||
```
|
```
|
||||||
|
|
||||||
This is especially important for LSP range objects which require an `end` field per the LSP specification.
|
Critical for LSP range objects with `end` field.
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
### Return Pattern
|
### Return Pattern
|
||||||
Functions that can fail return `nil, "error message"`:
|
|
||||||
```lua
|
```lua
|
||||||
function M.parse_file(filepath)
|
function M.parse_file(filepath)
|
||||||
local file = io.open(filepath, "r")
|
local file = io.open(filepath, "r")
|
||||||
@@ -133,7 +136,6 @@ end
|
|||||||
```
|
```
|
||||||
|
|
||||||
### External Dependencies
|
### External Dependencies
|
||||||
Always wrap external requires in `pcall()`:
|
|
||||||
```lua
|
```lua
|
||||||
local ok, module = pcall(require, "some_module")
|
local ok, module = pcall(require, "some_module")
|
||||||
if ok and module then
|
if ok and module then
|
||||||
@@ -141,72 +143,187 @@ if ok and module then
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Validation
|
## Completion Triggers
|
||||||
Check required parameters early and return sensible defaults:
|
|
||||||
```lua
|
|
||||||
function M.get_members(type_name)
|
|
||||||
if not type_name then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
-- ... continue
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Completion System
|
|
||||||
|
|
||||||
### Trigger Characters
|
|
||||||
- `#` - Local variables (after BEGIN)
|
- `#` - Local variables (after BEGIN)
|
||||||
- `.` - Member access (UDT/DB members)
|
- `.` - Member access (UDT/DB)
|
||||||
- `"` - Global DB names
|
- `"` - Global DB names
|
||||||
- `(` - FB/Function parameters
|
- `(` - FB/Function parameters
|
||||||
- ` ` (space) - General completion
|
- ` ` (space) - General completion
|
||||||
|
|
||||||
### Active Portion Detection
|
## Formatter Options
|
||||||
When matching patterns in completion, use the "active portion" of the line (after the last operator) to correctly handle expressions like:
|
|
||||||
```scl
|
The formatter supports collapsing verbose attribute blocks:
|
||||||
"DB1".member := "DB2".member
|
|
||||||
|
```lua
|
||||||
|
-- Before formatting:
|
||||||
|
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
|
||||||
|
|
||||||
|
-- After formatting:
|
||||||
|
statCntrPartsIn{...} : Int;
|
||||||
```
|
```
|
||||||
Find the last `:=`, `=>`, `=`, `<>`, `>=`, `<=`, `>`, `<`, `AND`, `OR`, `NOT` and only match patterns after it.
|
|
||||||
|
### Formatter Configuration
|
||||||
|
```lua
|
||||||
|
local options = {
|
||||||
|
insertSpaces = false,
|
||||||
|
tabSize = 1,
|
||||||
|
collapseAttributes = true, -- Enable attribute collapsing (default: true)
|
||||||
|
collapsePatterns = { -- Override default patterns
|
||||||
|
"^%s*{%s*EXTERNAL",
|
||||||
|
"^%s*{ S7_",
|
||||||
|
},
|
||||||
|
extendCollapsePatterns = { -- Add custom patterns
|
||||||
|
"^%s*{%s*CUSTOM",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default Collapse Patterns
|
||||||
|
- `^%s*{%s*EXTERNAL` - Variable attributes: `{EXTERNALACCESSIBLE := 'false'; ...}`
|
||||||
|
- `^%s*{ S7_` - Block attributes: `{ S7_Optimized_Access := 'TRUE' }`
|
||||||
|
- `^%s*{%s*%w+%s*:=` - Generic attributes with assignments
|
||||||
|
|
||||||
|
### Per-Variable Collapse Rules
|
||||||
|
Control collapsing per variable using rules (evaluated before global patterns):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local options = {
|
||||||
|
collapseVariableRules = {
|
||||||
|
-- Collapse attributes for variables starting with "stat"
|
||||||
|
{
|
||||||
|
variablePattern = "^stat",
|
||||||
|
collapse = true,
|
||||||
|
},
|
||||||
|
-- Never collapse for "temp" prefix variables
|
||||||
|
{
|
||||||
|
variablePattern = "^temp",
|
||||||
|
collapse = false,
|
||||||
|
},
|
||||||
|
-- Collapse only if both variable AND attribute match
|
||||||
|
{
|
||||||
|
variablePattern = "^config",
|
||||||
|
attributePattern = "EXTERNAL",
|
||||||
|
collapse = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule properties:**
|
||||||
|
- `variablePattern` - Lua pattern to match variable name (optional)
|
||||||
|
- `attributePattern` - Lua pattern to match attribute content (optional)
|
||||||
|
- `collapse` - `true` to collapse to `{...}`, `false` to keep expanded
|
||||||
|
|
||||||
|
Rules are checked in order; first match wins.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `:SCLShowVariables` | Show local variables in current file |
|
| `:SCLShowVariables` | Show local variables |
|
||||||
| `:SCLShowWorkspaceTypes` | Show workspace UDTs, FBs, and Global DBs count |
|
| `:SCLShowWorkspaceTypes` | Show workspace types count |
|
||||||
| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs |
|
| `:SCLRescanWorkspaceTypes` | Rescan workspace |
|
||||||
| `:SCLPrefixWord` | Manually prefix current word with `#` |
|
| `:SCLPrefixWord` | Prefix word with `#` |
|
||||||
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
|
| `:SCLMultilineParams` | Fill FB parameters |
|
||||||
| `:LspSCLFormat` | Format current SCL file |
|
| `:LspSCLFormat` | Format SCL file |
|
||||||
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ |
|
| `:SCLToggleAttrBlock` | Toggle attribute block under cursor |
|
||||||
|
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
||||||
|
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
||||||
|
|
||||||
## LSP Server Implementation Notes
|
## Keybindings (SCL/UDT files)
|
||||||
|
|
||||||
|
### Attribute Block Toggle
|
||||||
|
Interactive expand/collapse of individual `{...}` blocks (uses `<Leader>x` prefix):
|
||||||
|
|
||||||
|
| Key | Mode | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `<Leader>xa` | Normal/Insert | Toggle block under cursor |
|
||||||
|
| `<Leader>xae` | Normal | Expand all collapsed blocks |
|
||||||
|
| `<Leader>xac` | Normal | Collapse all attribute blocks |
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
1. Place cursor inside any `{...}` block
|
||||||
|
2. Press `<Leader>xa` to toggle between collapsed `{...}` and expanded content
|
||||||
|
3. Original content is stored per-buffer and persists until buffer is closed
|
||||||
|
4. Works on both formatter-collapsed blocks and manually collapsed ones
|
||||||
|
|
||||||
|
### Other Keybindings
|
||||||
|
|
||||||
|
| Key | Mode | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `<Leader>xa` | Normal/Insert | Toggle attribute block under cursor |
|
||||||
|
| `<Leader>xae` | Normal | Expand all attribute blocks |
|
||||||
|
| `<Leader>xac` | Normal | Collapse all attribute blocks |
|
||||||
|
| `<Space>mp` | Insert | Fill multiline FB parameters |
|
||||||
|
| `<Tab>` | Insert | Jump to next parameter or regular Tab |
|
||||||
|
|
||||||
|
Note: `<Leader>` is typically `\` (backslash) or `<Space>` depending on your configuration.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**If commands or keybindings don't work:**
|
||||||
|
|
||||||
|
1. Check if setup() was called:
|
||||||
|
```lua
|
||||||
|
:lua print(vim.inspect(require("scl_lsp")))
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify commands exist:
|
||||||
|
```vim
|
||||||
|
:command SCLToggleAttrBlock
|
||||||
|
```
|
||||||
|
Should show the command definition.
|
||||||
|
|
||||||
|
3. Check for errors during setup:
|
||||||
|
```lua
|
||||||
|
:lua require("scl_lsp").setup({})
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Verify leader key:
|
||||||
|
```vim
|
||||||
|
:echo mapleader
|
||||||
|
```
|
||||||
|
If empty, your leader is `\` (backslash).
|
||||||
|
|
||||||
|
5. Manual test keybinding:
|
||||||
|
```vim
|
||||||
|
:nmap <Leader>xa
|
||||||
|
```
|
||||||
|
Should show the mapping.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Manual testing using: `~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project`
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Open `.scl` file in Neovim
|
||||||
|
2. Test LSP features (hover, completion, go-to-definition)
|
||||||
|
3. Verify diagnostics
|
||||||
|
4. Test formatting
|
||||||
|
5. Check workspace scanning
|
||||||
|
|
||||||
|
## LSP Implementation Notes
|
||||||
|
|
||||||
### Method Name Conversion
|
### Method Name Conversion
|
||||||
LSP methods like `textDocument/didOpen` are converted to handler names like `textDocument_didOpen`:
|
|
||||||
```lua
|
```lua
|
||||||
local method_name = message.method:gsub("/", "_")
|
local method_name = message.method:gsub("/", "_")
|
||||||
local handler = handlers[method_name]
|
local handler = handlers[method_name]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Request vs Notification
|
### Request vs Notification
|
||||||
- Requests have `id` field and require a response
|
|
||||||
- Notifications have no `id` and should not receive a response
|
|
||||||
```lua
|
```lua
|
||||||
if message.id then
|
if message.id then
|
||||||
-- Only send response for requests
|
-- Request: send response
|
||||||
return { id = message.id, result = result }
|
return { id = message.id, result = result }
|
||||||
end
|
end
|
||||||
|
-- Notification: no response needed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Line Ending Handling
|
### Script Path Pattern
|
||||||
The LSP server strips CRLF (`\r\n`) line endings for Windows compatibility:
|
|
||||||
```lua
|
```lua
|
||||||
line = line:gsub("\r$", "")
|
local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
|
||||||
|
if script_path == "" then
|
||||||
|
script_path = "."
|
||||||
|
end
|
||||||
|
package.path = package.path .. ";" .. script_path .. "/?.lua"
|
||||||
```
|
```
|
||||||
|
|
||||||
### JSON Parser
|
|
||||||
The standalone JSON parser in `src/json.lua` handles:
|
|
||||||
- Objects, arrays, strings, numbers, booleans, null
|
|
||||||
- Escape sequences in strings
|
|
||||||
- Whitespace skipping
|
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
-- Attribute block toggle - Interactive expand/collapse of {...} blocks
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- Store expanded content per buffer
|
||||||
|
-- Format: { [bufnr] = { [line_num] = { collapsed = "...", expanded = "actual content", is_collapsed = boolean } } }
|
||||||
|
local buffer_attr_state = {}
|
||||||
|
|
||||||
|
-- Find attribute block under cursor
|
||||||
|
-- Returns: start_col, end_col, content (or nil if not found)
|
||||||
|
function M.find_attr_block_under_cursor()
|
||||||
|
local line = vim.api.nvim_get_current_line()
|
||||||
|
local col = vim.api.nvim_win_get_cursor(0)[2] + 1 -- 1-indexed
|
||||||
|
|
||||||
|
-- Find all {...} blocks in the line
|
||||||
|
local pos = 1
|
||||||
|
while pos <= #line do
|
||||||
|
local start_pos, end_pos = line:find("{(.-)}", pos)
|
||||||
|
if not start_pos then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if cursor is within this block (including braces)
|
||||||
|
if col >= start_pos and col <= end_pos then
|
||||||
|
local content = line:sub(start_pos + 1, end_pos - 1)
|
||||||
|
return start_pos, end_pos, content
|
||||||
|
end
|
||||||
|
|
||||||
|
pos = end_pos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil, nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Toggle the attribute block under cursor
|
||||||
|
function M.toggle_attr_block()
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
local line_num = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
local line = vim.api.nvim_get_current_line()
|
||||||
|
|
||||||
|
local start_col, end_col, content = M.find_attr_block_under_cursor()
|
||||||
|
if not start_col then
|
||||||
|
vim.notify("No attribute block found under cursor", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize buffer state if needed
|
||||||
|
if not buffer_attr_state[bufnr] then
|
||||||
|
buffer_attr_state[bufnr] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local state_key = line_num .. "_" .. start_col
|
||||||
|
local state = buffer_attr_state[bufnr][state_key]
|
||||||
|
|
||||||
|
-- Check if this is currently collapsed
|
||||||
|
local is_collapsed = content == "..."
|
||||||
|
|
||||||
|
if is_collapsed then
|
||||||
|
-- Expand: restore original content
|
||||||
|
if state and state.expanded then
|
||||||
|
local new_line = line:sub(1, start_col) .. state.expanded .. line:sub(end_col)
|
||||||
|
vim.api.nvim_set_current_line(new_line)
|
||||||
|
buffer_attr_state[bufnr][state_key].is_collapsed = false
|
||||||
|
vim.notify("Attribute block expanded", vim.log.levels.INFO)
|
||||||
|
else
|
||||||
|
vim.notify("Cannot expand: original content not stored", vim.log.levels.WARN)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Collapse: store original and show {...}
|
||||||
|
local new_line = line:sub(1, start_col) .. "..." .. line:sub(end_col)
|
||||||
|
vim.api.nvim_set_current_line(new_line)
|
||||||
|
buffer_attr_state[bufnr][state_key] = {
|
||||||
|
collapsed = "...",
|
||||||
|
expanded = content,
|
||||||
|
is_collapsed = true,
|
||||||
|
line_num = line_num,
|
||||||
|
start_col = start_col,
|
||||||
|
}
|
||||||
|
vim.notify("Attribute block collapsed", vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Expand all collapsed blocks in current buffer
|
||||||
|
function M.expand_all_attr_blocks()
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
if not buffer_attr_state[bufnr] then
|
||||||
|
vim.notify("No collapsed blocks to expand", vim.log.levels.INFO)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for state_key, state in pairs(buffer_attr_state[bufnr]) do
|
||||||
|
if state.is_collapsed then
|
||||||
|
-- state.line_num is 1-indexed, but nvim_buf_get_lines returns 0-indexed array
|
||||||
|
local line = lines[state.line_num]
|
||||||
|
|
||||||
|
if line then
|
||||||
|
-- Find the collapsed {...} and replace with expanded content
|
||||||
|
local new_line = line:sub(1, state.start_col) .. state.expanded .. line:sub(state.start_col + 4)
|
||||||
|
lines[state.line_num] = new_line
|
||||||
|
state.is_collapsed = false
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
vim.notify("Expanded " .. count .. " attribute block(s)", vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Collapse all attribute blocks matching patterns in current buffer
|
||||||
|
function M.collapse_all_attr_blocks(patterns)
|
||||||
|
patterns = patterns or {
|
||||||
|
"^%s*EXTERNAL", -- Match EXTERNALACCESSIBLE, EXTERNALVISIBLE, etc. (with optional leading space)
|
||||||
|
"^%s*S7_", -- Match S7_Optimized_Access, etc. (with optional leading space)
|
||||||
|
}
|
||||||
|
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
-- Initialize buffer state
|
||||||
|
if not buffer_attr_state[bufnr] then
|
||||||
|
buffer_attr_state[bufnr] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
for line_idx, line in ipairs(lines) do
|
||||||
|
local pos = 1
|
||||||
|
while pos <= #line do
|
||||||
|
local start_pos, end_pos, capture = line:find("{(.-)}", pos)
|
||||||
|
if not start_pos then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
local content = line:sub(start_pos + 1, end_pos - 1)
|
||||||
|
|
||||||
|
-- Check if content matches any pattern
|
||||||
|
local should_collapse = false
|
||||||
|
for _, pattern in ipairs(patterns) do
|
||||||
|
if content:match(pattern) then
|
||||||
|
should_collapse = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Don't collapse if already collapsed
|
||||||
|
if should_collapse and content ~= "..." then
|
||||||
|
local state_key = line_idx .. "_" .. start_pos
|
||||||
|
buffer_attr_state[bufnr][state_key] = {
|
||||||
|
collapsed = "...",
|
||||||
|
expanded = content,
|
||||||
|
is_collapsed = true,
|
||||||
|
line_num = line_idx,
|
||||||
|
start_col = start_pos,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Replace in line
|
||||||
|
line = line:sub(1, start_pos) .. "..." .. line:sub(end_pos)
|
||||||
|
count = count + 1
|
||||||
|
|
||||||
|
-- Update position for next search
|
||||||
|
pos = start_pos + 4
|
||||||
|
else
|
||||||
|
pos = end_pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lines[line_idx] = line
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
vim.notify("Collapsed " .. count .. " attribute block(s)", vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear state when buffer is unloaded
|
||||||
|
function M.clear_buffer_state(bufnr)
|
||||||
|
buffer_attr_state[bufnr] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Setup autocmd to clear state on buffer unload
|
||||||
|
function M.setup()
|
||||||
|
local augroup = vim.api.nvim_create_augroup("SCLAttrToggle", { clear = true })
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("BufUnload", {
|
||||||
|
group = augroup,
|
||||||
|
pattern = "*.scl",
|
||||||
|
callback = function(args)
|
||||||
|
M.clear_buffer_state(args.buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Create commands
|
||||||
|
vim.api.nvim_create_user_command("SCLToggleAttrBlock", function()
|
||||||
|
M.toggle_attr_block()
|
||||||
|
end, {})
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("SCLExpandAllAttrBlocks", function()
|
||||||
|
M.expand_all_attr_blocks()
|
||||||
|
end, {})
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("SCLCollapseAllAttrBlocks", function()
|
||||||
|
M.collapse_all_attr_blocks()
|
||||||
|
end, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -88,6 +88,9 @@ function M.setup(opts)
|
|||||||
|
|
||||||
-- Multiline parameters feature
|
-- Multiline parameters feature
|
||||||
M.setup_multiline_params()
|
M.setup_multiline_params()
|
||||||
|
|
||||||
|
-- Attribute block toggle feature
|
||||||
|
M.setup_attr_toggle()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.setup_multiline_params()
|
function M.setup_multiline_params()
|
||||||
@@ -171,6 +174,76 @@ function M.create_commands()
|
|||||||
vim.api.nvim_create_user_command("SCLShowVariables", function() M.show_variables() end, {})
|
vim.api.nvim_create_user_command("SCLShowVariables", function() M.show_variables() end, {})
|
||||||
vim.api.nvim_create_user_command("SCLShowWorkspaceTypes", function() M.show_workspace_types() end, {})
|
vim.api.nvim_create_user_command("SCLShowWorkspaceTypes", function() M.show_workspace_types() end, {})
|
||||||
vim.api.nvim_create_user_command("SCLRescanWorkspaceTypes", function() M.rescan_workspace_types() end, {})
|
vim.api.nvim_create_user_command("SCLRescanWorkspaceTypes", function() M.rescan_workspace_types() end, {})
|
||||||
|
|
||||||
|
-- Attribute toggle commands
|
||||||
|
vim.api.nvim_create_user_command("SCLToggleAttrBlock", function()
|
||||||
|
local ok, at = pcall(require, "scl.attr_toggle")
|
||||||
|
if ok then
|
||||||
|
at.toggle_attr_block()
|
||||||
|
else
|
||||||
|
vim.notify("SCL: Failed to load attr_toggle module", vim.log.levels.ERROR)
|
||||||
|
end
|
||||||
|
end, {})
|
||||||
|
vim.api.nvim_create_user_command("SCLExpandAllAttrBlocks", function()
|
||||||
|
local ok, at = pcall(require, "scl.attr_toggle")
|
||||||
|
if ok then
|
||||||
|
at.expand_all_attr_blocks()
|
||||||
|
else
|
||||||
|
vim.notify("SCL: Failed to load attr_toggle module", vim.log.levels.ERROR)
|
||||||
|
end
|
||||||
|
end, {})
|
||||||
|
vim.api.nvim_create_user_command("SCLCollapseAllAttrBlocks", function()
|
||||||
|
local ok, at = pcall(require, "scl.attr_toggle")
|
||||||
|
if ok then
|
||||||
|
at.collapse_all_attr_blocks()
|
||||||
|
else
|
||||||
|
vim.notify("SCL: Failed to load attr_toggle module", vim.log.levels.ERROR)
|
||||||
|
end
|
||||||
|
end, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.setup_attr_toggle()
|
||||||
|
local at = require("scl.attr_toggle")
|
||||||
|
at.setup()
|
||||||
|
|
||||||
|
local function setup_buffer_keymaps(bufnr)
|
||||||
|
local opts = { buffer = bufnr, silent = true }
|
||||||
|
local ft = vim.bo[bufnr].filetype
|
||||||
|
|
||||||
|
if ft ~= "scl" and ft ~= "udt" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Toggle individual attribute block under cursor
|
||||||
|
vim.keymap.set({ "n", "i" }, "<Leader>xa", function()
|
||||||
|
at.toggle_attr_block()
|
||||||
|
end, vim.tbl_extend("force", opts, { desc = "SCL: Toggle attribute block" }))
|
||||||
|
|
||||||
|
-- Expand all collapsed blocks
|
||||||
|
vim.keymap.set("n", "<Leader>xae", function()
|
||||||
|
at.expand_all_attr_blocks()
|
||||||
|
end, vim.tbl_extend("force", opts, { desc = "SCL: Expand all attribute blocks" }))
|
||||||
|
|
||||||
|
-- Collapse all attribute blocks
|
||||||
|
vim.keymap.set("n", "<Leader>xac", function()
|
||||||
|
at.collapse_all_attr_blocks()
|
||||||
|
end, vim.tbl_extend("force", opts, { desc = "SCL: Collapse all attribute blocks" }))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set up keybindings for SCL files only
|
||||||
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
|
pattern = { "scl", "udt" },
|
||||||
|
callback = function(args)
|
||||||
|
setup_buffer_keymaps(args.buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Also check if there are already SCL buffers open
|
||||||
|
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
|
||||||
|
if vim.api.nvim_buf_is_loaded(bufnr) then
|
||||||
|
setup_buffer_keymaps(bufnr)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ function M.setup(opts)
|
|||||||
|
|
||||||
M.setup_syntax(opts)
|
M.setup_syntax(opts)
|
||||||
M.setup_multiline_params()
|
M.setup_multiline_params()
|
||||||
|
M.setup_attr_toggle()
|
||||||
M.create_commands()
|
M.create_commands()
|
||||||
|
|
||||||
if opts.auto_prefix ~= false then
|
if opts.auto_prefix ~= false then
|
||||||
@@ -237,6 +238,83 @@ function M.create_commands()
|
|||||||
vim.api.nvim_create_user_command("SCLShowWorkspaceTypes", function() M.show_workspace_types() end, {})
|
vim.api.nvim_create_user_command("SCLShowWorkspaceTypes", function() M.show_workspace_types() end, {})
|
||||||
vim.api.nvim_create_user_command("SCLRescanWorkspaceTypes", function() M.rescan_workspace_types() end, {})
|
vim.api.nvim_create_user_command("SCLRescanWorkspaceTypes", function() M.rescan_workspace_types() end, {})
|
||||||
vim.api.nvim_create_user_command("SCLPrefixWord", function() M.prefix_current_word() end, {})
|
vim.api.nvim_create_user_command("SCLPrefixWord", function() M.prefix_current_word() end, {})
|
||||||
|
|
||||||
|
-- Attribute toggle commands
|
||||||
|
vim.api.nvim_create_user_command("SCLToggleAttrBlock", function()
|
||||||
|
local ok, at = pcall(require, "scl.attr_toggle")
|
||||||
|
if ok then
|
||||||
|
at.toggle_attr_block()
|
||||||
|
else
|
||||||
|
vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR)
|
||||||
|
end
|
||||||
|
end, {})
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("SCLExpandAllAttrBlocks", function()
|
||||||
|
local ok, at = pcall(require, "scl.attr_toggle")
|
||||||
|
if ok then
|
||||||
|
at.expand_all_attr_blocks()
|
||||||
|
else
|
||||||
|
vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR)
|
||||||
|
end
|
||||||
|
end, {})
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("SCLCollapseAllAttrBlocks", function()
|
||||||
|
local ok, at = pcall(require, "scl.attr_toggle")
|
||||||
|
if ok then
|
||||||
|
at.collapse_all_attr_blocks()
|
||||||
|
else
|
||||||
|
vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR)
|
||||||
|
end
|
||||||
|
end, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.setup_attr_toggle()
|
||||||
|
local ok, at = pcall(require, "scl.attr_toggle")
|
||||||
|
if not ok then
|
||||||
|
vim.notify("SCL: attr_toggle module not found", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
at.setup()
|
||||||
|
|
||||||
|
local function setup_buffer_keymaps(bufnr)
|
||||||
|
local opts = { buffer = bufnr, silent = true }
|
||||||
|
local ft = vim.bo[bufnr].filetype
|
||||||
|
|
||||||
|
if ft ~= "scl" and ft ~= "udt" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Toggle individual attribute block under cursor
|
||||||
|
vim.keymap.set({ "n", "i" }, "<Leader>xa", function()
|
||||||
|
at.toggle_attr_block()
|
||||||
|
end, vim.tbl_extend("force", opts, { desc = "SCL: Toggle attribute block" }))
|
||||||
|
|
||||||
|
-- Expand all collapsed blocks
|
||||||
|
vim.keymap.set("n", "<Leader>xae", function()
|
||||||
|
at.expand_all_attr_blocks()
|
||||||
|
end, vim.tbl_extend("force", opts, { desc = "SCL: Expand all attribute blocks" }))
|
||||||
|
|
||||||
|
-- Collapse all attribute blocks
|
||||||
|
vim.keymap.set("n", "<Leader>xac", function()
|
||||||
|
at.collapse_all_attr_blocks()
|
||||||
|
end, vim.tbl_extend("force", opts, { desc = "SCL: Collapse all attribute blocks" }))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set up keybindings for SCL files only
|
||||||
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
|
pattern = { "scl", "udt" },
|
||||||
|
callback = function(args)
|
||||||
|
setup_buffer_keymaps(args.buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Also check if there are already SCL buffers open
|
||||||
|
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
|
||||||
|
if vim.api.nvim_buf_is_loaded(bufnr) then
|
||||||
|
setup_buffer_keymaps(bufnr)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
Reference in New Issue
Block a user