Merge branch 'feat/scl_formatter' into develop
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# AGENTS.md - SCL Language Server
|
# 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 Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
|
||||||
|
|
||||||
## Build/Lint/Test Commands
|
## Build/Lint/Test Commands
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ lua src/main.lua --test # Direct test execution
|
|||||||
|
|
||||||
# Tree-sitter Parser
|
# Tree-sitter Parser
|
||||||
tree-sitter generate # Generate parser from grammar.js
|
tree-sitter generate # Generate parser from grammar.js
|
||||||
tree-sitter test # Run parser tests
|
tree-sitter test # Run all parser tests
|
||||||
tree-sitter parse <file.scl> # Parse single SCL file
|
tree-sitter parse <file.scl> # Parse single SCL file
|
||||||
|
|
||||||
# Lua Linting
|
# Lua Linting
|
||||||
@@ -31,11 +31,10 @@ scl_lsp/
|
|||||||
│ ├── formatter.lua # Document formatter
|
│ ├── formatter.lua # Document formatter
|
||||||
│ ├── treesitter.lua # Tree-sitter integration
|
│ ├── treesitter.lua # Tree-sitter integration
|
||||||
│ ├── plc_json.lua # External UDT/DB loading
|
│ ├── plc_json.lua # External UDT/DB loading
|
||||||
│ ├── builtin_instructions.lua # TIA Portal built-in types/FBs/functions
|
│ ├── builtin_instructions.lua # TIA Portal built-in types
|
||||||
│ └── json.lua # JSON encoder/decoder
|
│ └── json.lua # JSON encoder/decoder
|
||||||
├── lua/scl/ # Neovim plugin (uses require)
|
├── lua/scl/ # Neovim plugin (uses require)
|
||||||
│ ├── init.lua # Main plugin setup
|
│ ├── init.lua # Main plugin setup
|
||||||
│ ├── blink_cmp_source.lua
|
|
||||||
│ ├── udt_parser.lua # UDT parser
|
│ ├── udt_parser.lua # UDT parser
|
||||||
│ ├── db_parser.lua # Global DB parser
|
│ ├── db_parser.lua # Global DB parser
|
||||||
│ ├── fb_parser.lua # Function Block parser
|
│ ├── fb_parser.lua # Function Block parser
|
||||||
@@ -43,34 +42,26 @@ scl_lsp/
|
|||||||
│ ├── workspace_types.lua # Workspace scanning
|
│ ├── workspace_types.lua # Workspace scanning
|
||||||
│ ├── multiline_params.lua
|
│ ├── multiline_params.lua
|
||||||
│ ├── auto_prefix.lua
|
│ ├── auto_prefix.lua
|
||||||
│ └── attr_toggle.lua # Interactive attribute block toggle
|
│ └── attr_toggle.lua
|
||||||
├── queries/ # Tree-sitter queries
|
├── queries/ # Tree-sitter queries
|
||||||
│ ├── highlights.scm
|
|
||||||
│ ├── indents.scm
|
|
||||||
│ └── folds.scm
|
|
||||||
└── grammar.js # Tree-sitter grammar
|
└── grammar.js # Tree-sitter grammar
|
||||||
```
|
```
|
||||||
|
|
||||||
## Filetype Detection
|
## Filetype Detection
|
||||||
|
|
||||||
The LSP requires the `scl` filetype to be set. Add to your Neovim config (e.g., `ftdetect/scl.vim`):
|
Required in Neovim config (ftdetect/scl.vim):
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
au BufRead,BufNewFile *.scl set filetype=scl
|
au BufRead,BufNewFile *.scl set filetype=scl
|
||||||
au BufRead,BufNewFile *.udt set filetype=scl
|
au BufRead,BufNewFile *.udt set filetype=scl
|
||||||
au BufRead,BufNewFile *.db set filetype=scl
|
au BufRead,BufNewFile *.db set filetype=scl
|
||||||
```
|
```
|
||||||
|
|
||||||
This enables LSP features (completion, goto definition, hover, etc.) for all SCL-related file types.
|
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### Module Pattern
|
### Module Pattern
|
||||||
```lua
|
```lua
|
||||||
-- File header comment
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Private cache
|
|
||||||
local cache = {}
|
local cache = {}
|
||||||
|
|
||||||
function M.public_function()
|
function M.public_function()
|
||||||
@@ -89,8 +80,8 @@ return M
|
|||||||
- Booleans: 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: `require("scl.module_name")`
|
||||||
- `src/` modules: use `dofile(script_path .. "/module.lua")`
|
- `src/` modules: `dofile(script_path .. "/module.lua")`
|
||||||
- External dependencies: wrap in `pcall()` for safety
|
- External dependencies: wrap in `pcall()` for safety
|
||||||
|
|
||||||
### Code Formatting
|
### Code Formatting
|
||||||
@@ -99,6 +90,9 @@ return M
|
|||||||
- Max line length: 120 characters
|
- Max line length: 120 characters
|
||||||
- No trailing whitespace
|
- No trailing whitespace
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
**DO NOT ADD COMMENTS** in code unless explicitly requested by the user.
|
||||||
|
|
||||||
### Parser Module API
|
### Parser Module API
|
||||||
All parser modules must implement:
|
All parser modules must implement:
|
||||||
- `parse_*_file(filepath)` - Parse and cache
|
- `parse_*_file(filepath)` - Parse and cache
|
||||||
@@ -121,291 +115,54 @@ function M.clear_cache()
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lua Reserved Keywords
|
### Error Handling
|
||||||
|
|
||||||
Cannot use reserved keywords as table keys directly:
|
|
||||||
```lua
|
|
||||||
-- WRONG: syntax error
|
|
||||||
local range = { start = pos1, end = pos2 }
|
|
||||||
|
|
||||||
-- CORRECT: bracket notation
|
|
||||||
local range = { start = pos1, ["end"] = pos2 }
|
|
||||||
```
|
|
||||||
|
|
||||||
Critical for LSP range objects with `end` field.
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Return Pattern
|
|
||||||
```lua
|
```lua
|
||||||
function M.parse_file(filepath)
|
function M.parse_file(filepath)
|
||||||
local file = io.open(filepath, "r")
|
local file = io.open(filepath, "r")
|
||||||
if not file then
|
if not file then
|
||||||
return nil, "Cannot open file: " .. filepath
|
return nil, "Cannot open file: " .. filepath
|
||||||
end
|
end
|
||||||
-- ... parse logic
|
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
```
|
|
||||||
|
|
||||||
### External Dependencies
|
-- External dependencies
|
||||||
```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
|
||||||
module.do_something()
|
module.do_something()
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
## Diagnostics
|
## Lua Reserved Keywords
|
||||||
|
|
||||||
The LSP validates data types and provides warnings for unknown types. It recognizes:
|
|
||||||
- **Elementary data types** - BOOL, BYTE, WORD, DWORD, LWORD, CHAR, WCHAR, STRING, WSTRING, INT, DINT, LINT, USINT, SINT, UINT, UDINT, ULINT, REAL, LREAL, TIME, DATE, TOD, TIME_OF_DAY, DATE_AND_TIME, DT, S5TIME, LTIME, DTL
|
|
||||||
- **Timer/counter types** - IEC_TIMER, TON_TIME, TOF_TIME, TONR_TIME, TP_TIME, CTU, CTD, CTUD
|
|
||||||
- **TIA Portal built-in instructions** - Functions and function blocks
|
|
||||||
- **Workspace types** - UDTs from `.udt` files and data blocks from `.db` files
|
|
||||||
|
|
||||||
## Completion Triggers
|
|
||||||
- `#` - Local variables (after BEGIN)
|
|
||||||
- `.` - Member access (UDT/DB)
|
|
||||||
- `"` - Global DB names
|
|
||||||
- `(` - FB/Function parameters
|
|
||||||
- ` ` (space) - General completion
|
|
||||||
- `:` - Type declaration (in VAR section)
|
|
||||||
|
|
||||||
### Completion Types
|
|
||||||
The LSP provides auto-completion for:
|
|
||||||
- **Local variables** - Variables declared in VAR sections
|
|
||||||
- **UDT types** - User-defined types from `.udt` files
|
|
||||||
- **Global DBs** - Data blocks from `.db` files
|
|
||||||
- **Elementary types** - BOOL, INT, DINT, REAL, TIME, etc.
|
|
||||||
- **TIA Portal built-in FBs** - TON, TOF, TP, CTU, CTD, CTUD, R_TRIG, etc.
|
|
||||||
- **TIA Portal built-in functions** - ADD, SUB, MUL, DIV, SIN, COS, SQRT, etc.
|
|
||||||
|
|
||||||
## Formatter Options
|
|
||||||
|
|
||||||
The formatter supports collapsing verbose attribute blocks:
|
|
||||||
|
|
||||||
|
Cannot use reserved keywords as table keys directly:
|
||||||
```lua
|
```lua
|
||||||
-- Before formatting:
|
-- WRONG
|
||||||
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
|
local range = { start = pos1, end = pos2 }
|
||||||
|
|
||||||
-- After formatting:
|
-- CORRECT
|
||||||
statCntrPartsIn{...} : Int;
|
local range = { start = pos1, ["end"] = pos2 }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Formatter Configuration
|
Critical for LSP range objects with `end` field.
|
||||||
```lua
|
|
||||||
local options = {
|
|
||||||
insertSpaces = false,
|
|
||||||
tabSize = 1,
|
|
||||||
collapseAttributes = false, -- Disabled by default (see note below)
|
|
||||||
collapsePatterns = { -- Override default patterns
|
|
||||||
"^%s*{%s*EXTERNAL",
|
|
||||||
"^%s*{ S7_",
|
|
||||||
},
|
|
||||||
extendCollapsePatterns = { -- Add custom patterns
|
|
||||||
"^%s*{%s*CUSTOM",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** `collapseAttributes` is disabled by default to preserve compatibility with the interactive toggle feature (`:SCLToggleAttrBlock`). When the formatter automatically collapses blocks, the original content is lost. To use both formatting and toggle:
|
## Type System
|
||||||
1. Format with `:LspSCLFormat` (preserves original content)
|
|
||||||
2. Manually collapse with `:SCLCollapseAllAttrBlocks` (stores original content)
|
|
||||||
3. Now toggle/expand works correctly
|
|
||||||
|
|
||||||
### Default Collapse Patterns
|
The LSP validates data types and provides warnings for unknown types:
|
||||||
The `:SCLCollapseAllAttrBlocks` command uses case-insensitive patterns:
|
- **Elementary**: BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, TIME, STRING, etc.
|
||||||
- `EXTERNAL` - Variable attributes: `{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'}`
|
- **Timer/counter**: IEC_TIMER, TON_TIME, TOF_TIME, CTU, CTD, CTUD
|
||||||
- `S7_` - Block attributes: `{S7_Optimized_Access := 'TRUE'}`
|
- **Built-in FBs**: TON, TOF, TP, R_TRIG, F_TRIG, etc.
|
||||||
- `NonRetain` - NonRetain attribute: `{NonRetain}`
|
- **Built-in functions**: ADD, SUB, MUL, DIV, SIN, COS, SQRT, etc.
|
||||||
- `Retain` - Retain attribute: `{Retain}`
|
- **Workspace types**: UDTs from `.udt` files, DBs from `.db` files
|
||||||
- `%w+ :=` - Generic attributes with assignments: `{key := 'value'}`
|
|
||||||
- `%w+` - Single word attributes: `{SomeAttribute}`
|
|
||||||
|
|
||||||
Works in `.scl`, `.udt`, and `.db` files.
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `:SCLShowVariables` | Show local variables |
|
|
||||||
| `:SCLShowWorkspaceTypes` | Show workspace types count |
|
|
||||||
| `:SCLRescanWorkspaceTypes` | Rescan workspace |
|
|
||||||
| `:SCLPrefixWord` | Prefix word with `#` |
|
|
||||||
| `:SCLMultilineParams` | Fill FB parameters |
|
|
||||||
| `:LspSCLFormat` | Format SCL file |
|
|
||||||
| `:SCLToggleAttrBlock` | Toggle attribute block under cursor |
|
|
||||||
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
|
||||||
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
|
||||||
|
|
||||||
## LSP Features
|
|
||||||
|
|
||||||
### Goto Declaration (`gD`)
|
|
||||||
The LSP supports goto declaration functionality for:
|
|
||||||
- **Functions** - Jump to function definition in `.scl` files
|
|
||||||
- **Function Blocks (FB)** - Jump to FB definition in `.scl` files
|
|
||||||
- **Data Blocks (DB)** - Jump to DB definition in `.scl` and `.db` files
|
|
||||||
- **User-Defined Types (UDT)** - Jump to type definition in `.scl` and `.udt` files
|
|
||||||
|
|
||||||
Usage: Place cursor on a variable or type name and press `gD` (or use LSP client command).
|
|
||||||
|
|
||||||
## Auto-Editing Features
|
|
||||||
|
|
||||||
### Auto-Uppercase Keywords
|
|
||||||
When typing SCL keywords followed by a space, they are automatically converted to uppercase:
|
|
||||||
- `if ` → `IF `
|
|
||||||
- `then ` → `THEN `
|
|
||||||
- `end_if` → `END_IF`
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
Works in `.scl`, `.udt`, and `.db` files.
|
|
||||||
|
|
||||||
### Auto-Semicolon for Control Structures
|
|
||||||
When pressing Enter after an `END_*` control structure keyword, a semicolon is automatically added:
|
|
||||||
- `END_IF` → `END_IF;`
|
|
||||||
- `END_FOR` → `END_FOR;`
|
|
||||||
- `END_WHILE` → `END_WHILE;`
|
|
||||||
- `END_REPEAT` → `END_REPEAT;`
|
|
||||||
- `END_CASE` → `END_CASE;`
|
|
||||||
|
|
||||||
Block definitions (END_VAR, END_FUNCTION, etc.) are NOT affected.
|
|
||||||
|
|
||||||
### Auto-Prefix Local Variables
|
|
||||||
Local variables are automatically prefixed with `#` when typing space or semicolon after BEGIN:
|
|
||||||
- `myVar := ` → `#myVar := `
|
|
||||||
- `counter.in` → `#counter.in` (on dot)
|
|
||||||
|
|
||||||
Can be toggled with `:lua require("scl.auto_prefix").toggle()`.
|
|
||||||
|
|
||||||
## 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 |
|
|
||||||
|-----|------|-------------|
|
|
||||||
| `<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.
|
|
||||||
|
|
||||||
### Attribute Toggle After Formatting
|
|
||||||
|
|
||||||
**Note:** After using `:LspSCLFormat`, all collapsed attribute blocks will be expanded and the toggle state is reset. This is because formatting changes line numbers and positions. You can collapse blocks again after formatting.
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Manual testing using: `~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project`
|
Use the reference project for testing:
|
||||||
|
```
|
||||||
Workflow:
|
~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/
|
||||||
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
|
|
||||||
```lua
|
|
||||||
local method_name = message.method:gsub("/", "_")
|
|
||||||
local handler = handlers[method_name]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Request vs Notification
|
Contains `.scl`, `.db`, `.udt`, and `.xml` files for comprehensive testing of:
|
||||||
```lua
|
- LSP features (hover, completion, go-to-definition)
|
||||||
if message.id then
|
- Formatter (`:SCLFormat`)
|
||||||
-- Request: send response
|
- Attribute block toggle
|
||||||
return { id = message.id, result = result }
|
- Workspace type scanning
|
||||||
end
|
|
||||||
-- Notification: no response needed
|
|
||||||
```
|
|
||||||
|
|
||||||
### Script Path Pattern
|
|
||||||
```lua
|
|
||||||
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"
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP)
|
|||||||
### Formatter
|
### Formatter
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| **Document Formatting** | Format entire SCL files |
|
| **Document Formatting** | Format entire SCL files with `:SCLFormat` |
|
||||||
|
| **Multi-line Assignments** | Preserves and formats multi-line expressions with proper alignment |
|
||||||
|
| **FB Call Formatting** | Formats function block calls in multiline style |
|
||||||
| **Indentation** | Proper block indentation |
|
| **Indentation** | Proper block indentation |
|
||||||
| **Keyword Casing** | Maintains SCL keyword casing |
|
| **Keyword Casing** | Maintains SCL keyword casing |
|
||||||
|
|
||||||
@@ -119,8 +121,7 @@ require("scl_lsp").setup({
|
|||||||
| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs |
|
| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs |
|
||||||
| `:SCLPrefixWord` | Manually prefix current word with `#` |
|
| `:SCLPrefixWord` | Manually prefix current word with `#` |
|
||||||
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
|
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
|
||||||
| `:LspSCLFormat` | Format current SCL file |
|
| `:SCLFormat` | Format current SCL file |
|
||||||
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ |
|
|
||||||
| `:SCLToggleAttrBlock` | Toggle attribute block under cursor |
|
| `:SCLToggleAttrBlock` | Toggle attribute block under cursor |
|
||||||
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
||||||
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
||||||
@@ -159,7 +160,7 @@ statCounter{...} : Int;
|
|||||||
- Use `<Leader>xae` to expand all blocks in buffer
|
- Use `<Leader>xae` to expand all blocks in buffer
|
||||||
- Use `<Leader>xac` to collapse all blocks
|
- Use `<Leader>xac` to collapse all blocks
|
||||||
|
|
||||||
**Note:** The formatter has automatic collapsing disabled by default to preserve compatibility with the toggle feature. Format with `:LspSCLFormat` first, then use `:SCLCollapseAllAttrBlocks` to collapse while preserving the ability to expand later.
|
**Note:** Collapsed blocks are automatically expanded before saving to preserve full content on disk. Use `:SCLFormat` to format, then `:SCLCollapseAllAttrBlocks` to collapse while preserving the ability to expand later.
|
||||||
|
|
||||||
## External UDT Support
|
## External UDT Support
|
||||||
|
|
||||||
|
|||||||
+194
-198
@@ -5,11 +5,8 @@ local M = {}
|
|||||||
-- Default patterns for attribute blocks to collapse
|
-- Default patterns for attribute blocks to collapse
|
||||||
-- Users can extend these via options.collapse_patterns
|
-- Users can extend these via options.collapse_patterns
|
||||||
local DEFAULT_COLLAPSE_PATTERNS = {
|
local DEFAULT_COLLAPSE_PATTERNS = {
|
||||||
-- Match S7 variable attributes: {EXTERNALACCESSIBLE := 'false'; ...}
|
|
||||||
"^%s*{%s*EXTERNAL",
|
"^%s*{%s*EXTERNAL",
|
||||||
-- Match S7 block attributes: { S7_Optimized_Access := 'TRUE' } or {S7_...}
|
|
||||||
"^%s*{%s*S7_",
|
"^%s*{%s*S7_",
|
||||||
-- Match generic attribute blocks with assignments
|
|
||||||
"^%s*{%s*%w+%s*:=",
|
"^%s*{%s*%w+%s*:=",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,20 +23,17 @@ end
|
|||||||
|
|
||||||
-- Block types with their corresponding enders
|
-- Block types with their corresponding enders
|
||||||
local BLOCKS = {
|
local BLOCKS = {
|
||||||
-- Control structures
|
|
||||||
["IF"] = "END_IF",
|
["IF"] = "END_IF",
|
||||||
["FOR"] = "END_FOR",
|
["FOR"] = "END_FOR",
|
||||||
["WHILE"] = "END_WHILE",
|
["WHILE"] = "END_WHILE",
|
||||||
["REPEAT"] = "END_REPEAT",
|
["REPEAT"] = "END_REPEAT",
|
||||||
["CASE"] = "END_CASE",
|
["CASE"] = "END_CASE",
|
||||||
["REGION"] = "END_REGION",
|
["REGION"] = "END_REGION",
|
||||||
-- Declarations
|
|
||||||
["FUNCTION"] = "END_FUNCTION",
|
["FUNCTION"] = "END_FUNCTION",
|
||||||
["FUNCTION_BLOCK"] = "END_FUNCTION_BLOCK",
|
["FUNCTION_BLOCK"] = "END_FUNCTION_BLOCK",
|
||||||
["ORGANIZATION_BLOCK"] = "END_ORGANIZATION_BLOCK",
|
["ORGANIZATION_BLOCK"] = "END_ORGANIZATION_BLOCK",
|
||||||
["TYPE"] = "END_TYPE",
|
["TYPE"] = "END_TYPE",
|
||||||
["STRUCT"] = "END_STRUCT",
|
["STRUCT"] = "END_STRUCT",
|
||||||
-- VAR sections (special handling)
|
|
||||||
["VAR"] = "END_VAR",
|
["VAR"] = "END_VAR",
|
||||||
["VAR_INPUT"] = "END_VAR",
|
["VAR_INPUT"] = "END_VAR",
|
||||||
["VAR_OUTPUT"] = "END_VAR",
|
["VAR_OUTPUT"] = "END_VAR",
|
||||||
@@ -50,7 +44,6 @@ local BLOCKS = {
|
|||||||
["VAR_NON_RETAIN"] = "END_VAR",
|
["VAR_NON_RETAIN"] = "END_VAR",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Keywords that don't change indent level themselves (handled specially)
|
|
||||||
local SAME_LEVEL = {
|
local SAME_LEVEL = {
|
||||||
["THEN"] = true,
|
["THEN"] = true,
|
||||||
["ELSE"] = true,
|
["ELSE"] = true,
|
||||||
@@ -60,7 +53,6 @@ local SAME_LEVEL = {
|
|||||||
["BEGIN"] = true,
|
["BEGIN"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Control structure enders that need semicolons
|
|
||||||
local NEEDS_SEMICOLON = {
|
local NEEDS_SEMICOLON = {
|
||||||
["END_IF"] = true,
|
["END_IF"] = true,
|
||||||
["END_FOR"] = true,
|
["END_FOR"] = true,
|
||||||
@@ -70,7 +62,6 @@ local NEEDS_SEMICOLON = {
|
|||||||
["END_REGION"] = true,
|
["END_REGION"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- VAR keywords for special handling
|
|
||||||
local VAR_KEYWORDS = {
|
local VAR_KEYWORDS = {
|
||||||
["VAR"] = true,
|
["VAR"] = true,
|
||||||
["VAR_INPUT"] = true,
|
["VAR_INPUT"] = true,
|
||||||
@@ -82,7 +73,6 @@ local VAR_KEYWORDS = {
|
|||||||
["VAR_NON_RETAIN"] = true,
|
["VAR_NON_RETAIN"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Declaration keywords (reset indent)
|
|
||||||
local DECLARATIONS = {
|
local DECLARATIONS = {
|
||||||
["FUNCTION"] = true,
|
["FUNCTION"] = true,
|
||||||
["FUNCTION_BLOCK"] = true,
|
["FUNCTION_BLOCK"] = true,
|
||||||
@@ -91,31 +81,32 @@ local DECLARATIONS = {
|
|||||||
["STRUCT"] = true,
|
["STRUCT"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local CONTINUATION_OPERATORS = {
|
||||||
|
["AND"] = true,
|
||||||
|
["OR"] = true,
|
||||||
|
["XOR"] = true,
|
||||||
|
["NOT"] = true,
|
||||||
|
["MOD"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
function M.format_document(content, options)
|
function M.format_document(content, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local use_spaces = options.insertSpaces or false
|
local use_spaces = options.insertSpaces or false
|
||||||
local indent_size = options.tabSize or 1
|
local indent_size = options.tabSize or 1
|
||||||
local indent_char = use_spaces and string.rep(" ", indent_size) or "\t"
|
local indent_char = use_spaces and string.rep(" ", indent_size) or "\t"
|
||||||
|
|
||||||
-- Attribute block collapsing options
|
|
||||||
-- NOTE: Disabled by default to preserve compatibility with attr_toggle.
|
|
||||||
-- When formatter collapses blocks, the original content is lost.
|
|
||||||
-- Use :SCLCollapseAllAttrBlocks after formatting instead.
|
|
||||||
local collapse_attributes = options.collapseAttributes
|
local collapse_attributes = options.collapseAttributes
|
||||||
if collapse_attributes == nil then
|
if collapse_attributes == nil then
|
||||||
collapse_attributes = false -- Default: disabled to preserve toggle functionality
|
collapse_attributes = false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- User can provide custom patterns or extend defaults
|
|
||||||
local collapse_patterns = options.collapsePatterns or DEFAULT_COLLAPSE_PATTERNS
|
local collapse_patterns = options.collapsePatterns or DEFAULT_COLLAPSE_PATTERNS
|
||||||
if options.extendCollapsePatterns then
|
if options.extendCollapsePatterns then
|
||||||
-- Extend defaults with user patterns
|
|
||||||
for _, pattern in ipairs(options.extendCollapsePatterns) do
|
for _, pattern in ipairs(options.extendCollapsePatterns) do
|
||||||
table.insert(collapse_patterns, pattern)
|
table.insert(collapse_patterns, pattern)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse content into lines
|
|
||||||
local lines = {}
|
local lines = {}
|
||||||
for line in content:gmatch("([^\n]*)\n") do
|
for line in content:gmatch("([^\n]*)\n") do
|
||||||
table.insert(lines, line)
|
table.insert(lines, line)
|
||||||
@@ -128,18 +119,9 @@ function M.format_document(content, options)
|
|||||||
return { { range = range(0, 0, 0, 0), newText = "" } }
|
return { { range = range(0, 0, 0, 0), newText = "" } }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Stack of active blocks
|
|
||||||
-- Each entry: { type = "IF", indent = 1, is_case = false }
|
|
||||||
local stack = {}
|
local stack = {}
|
||||||
local formatted_lines = {}
|
local formatted_lines = {}
|
||||||
|
|
||||||
-- FB call state
|
|
||||||
local in_fb_call = false
|
|
||||||
local fb_name_length = 0
|
|
||||||
local fb_call_first_line = false
|
|
||||||
local fb_paren_offset = 0 -- Offset to align parameters with opening paren
|
|
||||||
|
|
||||||
-- Helper: get current indent string
|
|
||||||
local function get_indent()
|
local function get_indent()
|
||||||
local level = 0
|
local level = 0
|
||||||
for _, block in ipairs(stack) do
|
for _, block in ipairs(stack) do
|
||||||
@@ -150,100 +132,26 @@ function M.format_document(content, options)
|
|||||||
return string.rep(indent_char, level)
|
return string.rep(indent_char, level)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper: get continuation indent for FB calls
|
local function is_assignment_continuation(trimmed)
|
||||||
local function get_continuation_indent()
|
if not trimmed then return false end
|
||||||
local base = get_indent()
|
local first_word = trimmed:match("^(%w+)")
|
||||||
return base .. string.rep(" ", fb_paren_offset)
|
if first_word and CONTINUATION_OPERATORS[first_word:upper()] then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper: check if line is a case label
|
local function is_fb_call_start(trimmed)
|
||||||
local function is_case_label(trimmed)
|
if not (trimmed:match("^#?%w+") or trimmed:match('^"')) then
|
||||||
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Helper: extract variable name from a line (before any attribute block)
|
|
||||||
local function extract_variable_name(line)
|
|
||||||
-- Match pattern: variableName{...} or variableName : type
|
|
||||||
-- Variable names can contain letters, numbers, underscores
|
|
||||||
local var_name = line:match("^%s*([%w_]+)%s*[%({:]")
|
|
||||||
return var_name
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Helper: collapse attribute blocks like {EXTERNALACCESSIBLE := 'false'; ...} to {...}
|
|
||||||
local function collapse_attribute_block(line)
|
|
||||||
if not collapse_attributes then
|
|
||||||
return line
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if line contains an attribute block
|
|
||||||
local attr_start, attr_end = line:find("{.-}")
|
|
||||||
if not attr_start then
|
|
||||||
return line
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Extract the content before the attribute block
|
|
||||||
local before_attr = line:sub(1, attr_start - 1)
|
|
||||||
local attr_content = line:sub(attr_start, attr_end)
|
|
||||||
local after_attr = line:sub(attr_end + 1)
|
|
||||||
|
|
||||||
-- Extract variable name for per-variable rules
|
|
||||||
local var_name = extract_variable_name(line)
|
|
||||||
|
|
||||||
-- Check per-variable collapse rules first (highest priority)
|
|
||||||
if options.collapseVariableRules and var_name then
|
|
||||||
for _, rule in ipairs(options.collapseVariableRules) do
|
|
||||||
local var_pattern = rule.variablePattern
|
|
||||||
local attr_pattern = rule.attributePattern
|
|
||||||
local should_collapse = rule.collapse
|
|
||||||
|
|
||||||
-- Check if variable name matches
|
|
||||||
local var_matches = not var_pattern or var_name:match(var_pattern)
|
|
||||||
|
|
||||||
-- Check if attribute content matches (if specified)
|
|
||||||
local attr_matches = not attr_pattern or attr_content:match(attr_pattern)
|
|
||||||
|
|
||||||
if var_matches and attr_matches then
|
|
||||||
if should_collapse then
|
|
||||||
return before_attr .. "{...}" .. after_attr
|
|
||||||
else
|
|
||||||
-- Explicitly don't collapse this match
|
|
||||||
return line
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if this attribute block matches any global collapse pattern
|
|
||||||
for _, pattern in ipairs(collapse_patterns) do
|
|
||||||
if attr_content:match(pattern) then
|
|
||||||
-- Collapse to {...}
|
|
||||||
return before_attr .. "{...}" .. after_attr
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return line
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Helper: check if line starts FB call
|
|
||||||
local function is_fb_call_start(line)
|
|
||||||
local trimmed = line:gsub("^%s+", "")
|
|
||||||
if not (trimmed:match("^#%w+") or trimmed:match('^"')) then
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local paren_pos = trimmed:find("%(")
|
local paren_pos = trimmed:find("%(")
|
||||||
if not paren_pos then return false end
|
if not paren_pos then return false end
|
||||||
local assign_pos = trimmed:find(":=")
|
local assign_pos = trimmed:find(":=")
|
||||||
if assign_pos and assign_pos < paren_pos then return false end
|
if assign_pos and assign_pos < paren_pos then return false end
|
||||||
local open_count, close_count = 0, 0
|
return true
|
||||||
for i = 1, #trimmed do
|
|
||||||
local c = trimmed:sub(i, i)
|
|
||||||
if c == "(" then open_count = open_count + 1
|
|
||||||
elseif c == ")" then close_count = close_count + 1 end
|
|
||||||
end
|
|
||||||
return open_count > 0 and close_count < open_count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper: check if FB call ends on this line
|
|
||||||
local function fb_call_has_closing(line)
|
local function fb_call_has_closing(line)
|
||||||
local open_count, close_count = 0, 0
|
local open_count, close_count = 0, 0
|
||||||
for i = 1, #line do
|
for i = 1, #line do
|
||||||
@@ -251,23 +159,129 @@ function M.format_document(content, options)
|
|||||||
if c == "(" then open_count = open_count + 1
|
if c == "(" then open_count = open_count + 1
|
||||||
elseif c == ")" then close_count = close_count + 1 end
|
elseif c == ")" then close_count = close_count + 1 end
|
||||||
end
|
end
|
||||||
-- FB call ends when we have closes > opens, not when they're equal
|
|
||||||
-- (equal means no parens on this line, which shouldn't end the call)
|
|
||||||
return close_count > 0 and close_count >= open_count
|
return close_count > 0 and close_count >= open_count
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper: check if line ends with comma (FB call continuation)
|
local function format_fb_call_multiline(fb_lines, base_indent, param_indent)
|
||||||
local function is_continuation(line)
|
local result = {}
|
||||||
return line:gsub("^%s+", ""):gsub("%s+$", ""):match(".*,$") ~= nil
|
local all_text = table.concat(fb_lines, " ")
|
||||||
|
|
||||||
|
local fb_name = all_text:match("^(#?[%w_]+)%s*%(")
|
||||||
|
if not fb_name then
|
||||||
|
for _, l in ipairs(fb_lines) do
|
||||||
|
table.insert(result, base_indent .. l)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local paren_start = all_text:find("%(")
|
||||||
|
local paren_end = all_text:find("%);%s*$") or all_text:find("%)%s*$")
|
||||||
|
if not paren_start or not paren_end then
|
||||||
|
for _, l in ipairs(fb_lines) do
|
||||||
|
table.insert(result, base_indent .. l)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local params_text = all_text:sub(paren_start + 1, paren_end - 1)
|
||||||
|
|
||||||
|
local params = {}
|
||||||
|
local current_param = ""
|
||||||
|
local depth = 0
|
||||||
|
for i = 1, #params_text do
|
||||||
|
local c = params_text:sub(i, i)
|
||||||
|
if c == "(" then
|
||||||
|
depth = depth + 1
|
||||||
|
current_param = current_param .. c
|
||||||
|
elseif c == ")" then
|
||||||
|
depth = depth - 1
|
||||||
|
current_param = current_param .. c
|
||||||
|
elseif c == "," and depth == 0 then
|
||||||
|
local trimmed_param = current_param:match("^%s*(.-)%s*$")
|
||||||
|
if trimmed_param ~= "" then
|
||||||
|
table.insert(params, trimmed_param)
|
||||||
|
end
|
||||||
|
current_param = ""
|
||||||
|
else
|
||||||
|
current_param = current_param .. c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local last_param = current_param:match("^%s*(.-)%s*$")
|
||||||
|
if last_param and last_param ~= "" then
|
||||||
|
table.insert(params, last_param)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #params == 0 then
|
||||||
|
return { base_indent .. fb_name .. "();" }
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(result, base_indent .. fb_name .. "(" .. params[1] .. ",")
|
||||||
|
|
||||||
|
for i = 2, #params do
|
||||||
|
local closing = (i == #params) and ");" or ","
|
||||||
|
table.insert(result, param_indent .. params[i] .. closing)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_case_label(trimmed)
|
||||||
|
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function extract_variable_name(line)
|
||||||
|
local var_name = line:match("^%s*([%w_]+)%s*[%({:]")
|
||||||
|
return var_name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function collapse_attribute_block(line)
|
||||||
|
if not collapse_attributes then
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
|
||||||
|
local attr_start, attr_end = line:find("{.-}")
|
||||||
|
if not attr_start then
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
|
||||||
|
local before_attr = line:sub(1, attr_start - 1)
|
||||||
|
local attr_content = line:sub(attr_start, attr_end)
|
||||||
|
local after_attr = line:sub(attr_end + 1)
|
||||||
|
|
||||||
|
local var_name = extract_variable_name(line)
|
||||||
|
|
||||||
|
if options.collapseVariableRules and var_name then
|
||||||
|
for _, rule in ipairs(options.collapseVariableRules) do
|
||||||
|
local var_pattern = rule.variablePattern
|
||||||
|
local attr_pattern = rule.attributePattern
|
||||||
|
local should_collapse = rule.collapse
|
||||||
|
|
||||||
|
local var_matches = not var_pattern or var_name:match(var_pattern)
|
||||||
|
local attr_matches = not attr_pattern or attr_content:match(attr_pattern)
|
||||||
|
|
||||||
|
if var_matches and attr_matches then
|
||||||
|
if should_collapse then
|
||||||
|
return before_attr .. "{...}" .. after_attr
|
||||||
|
else
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, pattern in ipairs(collapse_patterns) do
|
||||||
|
if attr_content:match(pattern) then
|
||||||
|
return before_attr .. "{...}" .. after_attr
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return line
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper: extract keyword from line
|
|
||||||
local function get_keyword(trimmed)
|
local function get_keyword(trimmed)
|
||||||
-- Match VAR_IN_OUT, VAR_CONSTANT, etc. (words with multiple underscores)
|
|
||||||
return trimmed:match("^([%w_]+)") or trimmed:match("^(%w+)")
|
return trimmed:match("^([%w_]+)") or trimmed:match("^(%w+)")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper: find case block in stack
|
|
||||||
local function find_case_block()
|
local function find_case_block()
|
||||||
for i = #stack, 1, -1 do
|
for i = #stack, 1, -1 do
|
||||||
if stack[i].type == "CASE" then
|
if stack[i].type == "CASE" then
|
||||||
@@ -277,7 +291,6 @@ function M.format_document(content, options)
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper: pop blocks until matching ender found
|
|
||||||
local function pop_to_ender(ender_type)
|
local function pop_to_ender(ender_type)
|
||||||
while #stack > 0 do
|
while #stack > 0 do
|
||||||
local top = stack[#stack]
|
local top = stack[#stack]
|
||||||
@@ -290,77 +303,83 @@ function M.format_document(content, options)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Process each line
|
-- State tracking
|
||||||
|
local in_fb_call = false
|
||||||
|
local fb_call_lines = {}
|
||||||
|
local fb_base_indent = ""
|
||||||
|
local fb_param_indent = ""
|
||||||
|
|
||||||
|
local in_assignment = false
|
||||||
|
local assignment_continuation_indent = ""
|
||||||
|
|
||||||
for line_num, line in ipairs(lines) do
|
for line_num, line in ipairs(lines) do
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
local kw = get_keyword(trimmed)
|
local kw = get_keyword(trimmed)
|
||||||
|
|
||||||
-- Handle empty lines (preserve as-is)
|
|
||||||
if trimmed == "" then
|
if trimmed == "" then
|
||||||
table.insert(formatted_lines, "")
|
table.insert(formatted_lines, "")
|
||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle comments (re-indent with current level)
|
|
||||||
if trimmed:match("^//") or trimmed:match("^%(") then
|
if trimmed:match("^//") or trimmed:match("^%(") then
|
||||||
-- Apply attribute block collapsing to comments too (for consistency)
|
|
||||||
local collapsed = collapse_attribute_block(trimmed)
|
local collapsed = collapse_attribute_block(trimmed)
|
||||||
table.insert(formatted_lines, get_indent() .. collapsed)
|
table.insert(formatted_lines, get_indent() .. collapsed)
|
||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Apply attribute block collapsing
|
|
||||||
trimmed = collapse_attribute_block(trimmed)
|
trimmed = collapse_attribute_block(trimmed)
|
||||||
|
|
||||||
-- Check for FB call start
|
-- Check if continuing assignment
|
||||||
|
if in_assignment and is_assignment_continuation(trimmed) then
|
||||||
|
table.insert(formatted_lines, assignment_continuation_indent .. trimmed)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
in_assignment = false
|
||||||
|
|
||||||
|
-- Check if starting FB call
|
||||||
if not in_fb_call and is_fb_call_start(trimmed) then
|
if not in_fb_call and is_fb_call_start(trimmed) then
|
||||||
local fb_name = trimmed:match("^#?([%w_]+)") or trimmed:match('^"?([%w_]+)"?')
|
|
||||||
if fb_name then
|
|
||||||
fb_name_length = #fb_name
|
|
||||||
in_fb_call = true
|
in_fb_call = true
|
||||||
fb_call_first_line = true
|
fb_call_lines = { trimmed }
|
||||||
-- Calculate paren offset: find position of opening paren + 1
|
fb_base_indent = get_indent()
|
||||||
-- This aligns continuation lines with the content inside the parens
|
local fb_name = trimmed:match("^(#?[%w_]+)")
|
||||||
local paren_pos = trimmed:find("%(")
|
if fb_name then
|
||||||
if paren_pos then
|
fb_param_indent = fb_base_indent .. string.rep(" ", #fb_name + 1)
|
||||||
fb_paren_offset = paren_pos + 1 -- +1 to align with content after paren
|
|
||||||
else
|
else
|
||||||
fb_paren_offset = fb_name_length + 2
|
fb_param_indent = fb_base_indent .. string.rep(" ", 4)
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle FB call lines
|
if fb_call_has_closing(trimmed) then
|
||||||
if in_fb_call then
|
local formatted_fb = format_fb_call_multiline(fb_call_lines, fb_base_indent, fb_param_indent)
|
||||||
local is_first = fb_call_first_line
|
for _, fline in ipairs(formatted_fb) do
|
||||||
local has_closing = fb_call_has_closing(trimmed)
|
table.insert(formatted_lines, fline)
|
||||||
local is_cont = is_continuation(trimmed)
|
end
|
||||||
|
|
||||||
local formatted
|
|
||||||
if has_closing then
|
|
||||||
formatted = (is_first and get_indent() or get_continuation_indent()) .. trimmed
|
|
||||||
in_fb_call = false
|
in_fb_call = false
|
||||||
fb_call_first_line = false
|
fb_call_lines = {}
|
||||||
elseif is_cont then
|
end
|
||||||
formatted = is_first and (get_indent() .. trimmed) or (get_continuation_indent() .. trimmed)
|
goto continue
|
||||||
fb_call_first_line = false
|
end
|
||||||
else
|
|
||||||
formatted = get_continuation_indent() .. trimmed
|
-- Continue collecting FB call lines
|
||||||
|
if in_fb_call then
|
||||||
|
table.insert(fb_call_lines, trimmed)
|
||||||
|
if fb_call_has_closing(trimmed) then
|
||||||
|
local formatted_fb = format_fb_call_multiline(fb_call_lines, fb_base_indent, fb_param_indent)
|
||||||
|
for _, fline in ipairs(formatted_fb) do
|
||||||
|
table.insert(formatted_lines, fline)
|
||||||
|
end
|
||||||
|
in_fb_call = false
|
||||||
|
fb_call_lines = {}
|
||||||
end
|
end
|
||||||
table.insert(formatted_lines, formatted)
|
|
||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle block declarations (FUNCTION_BLOCK, etc.)
|
|
||||||
if DECLARATIONS[kw] then
|
if DECLARATIONS[kw] then
|
||||||
-- Clear stack for new block
|
|
||||||
stack = {}
|
stack = {}
|
||||||
table.insert(formatted_lines, trimmed)
|
table.insert(formatted_lines, trimmed)
|
||||||
table.insert(stack, { type = kw, indent = 0, is_declaration = true })
|
table.insert(stack, { type = kw, indent = 0, is_declaration = true })
|
||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle declaration enders
|
|
||||||
if kw and not BLOCKS[kw] then
|
if kw and not BLOCKS[kw] then
|
||||||
for starter, ender in pairs(BLOCKS) do
|
for starter, ender in pairs(BLOCKS) do
|
||||||
if DECLARATIONS[starter] and kw == ender then
|
if DECLARATIONS[starter] and kw == ender then
|
||||||
@@ -371,20 +390,15 @@ function M.format_document(content, options)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle BEGIN (transition to code section)
|
|
||||||
if kw == "BEGIN" then
|
if kw == "BEGIN" then
|
||||||
-- Pop any VAR blocks
|
|
||||||
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
||||||
table.remove(stack)
|
table.remove(stack)
|
||||||
end
|
end
|
||||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
-- BEGIN itself doesn't add indent, but code inside does
|
|
||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle VAR section start
|
|
||||||
if VAR_KEYWORDS[kw] then
|
if VAR_KEYWORDS[kw] then
|
||||||
-- Pop to declaration level
|
|
||||||
while #stack > 0 and not stack[#stack].is_declaration do
|
while #stack > 0 and not stack[#stack].is_declaration do
|
||||||
table.remove(stack)
|
table.remove(stack)
|
||||||
end
|
end
|
||||||
@@ -393,9 +407,7 @@ function M.format_document(content, options)
|
|||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle END_VAR
|
|
||||||
if kw == "END_VAR" then
|
if kw == "END_VAR" then
|
||||||
-- Pop VAR blocks
|
|
||||||
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
||||||
table.remove(stack)
|
table.remove(stack)
|
||||||
end
|
end
|
||||||
@@ -403,14 +415,10 @@ function M.format_document(content, options)
|
|||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle block enders (END_IF, END_FOR, etc.)
|
|
||||||
if kw and BLOCKS[kw] == nil then
|
if kw and BLOCKS[kw] == nil then
|
||||||
-- Check if it's an ender
|
|
||||||
for starter, ender in pairs(BLOCKS) do
|
for starter, ender in pairs(BLOCKS) do
|
||||||
if kw == ender and not DECLARATIONS[starter] then
|
if kw == ender and not DECLARATIONS[starter] then
|
||||||
-- Pop matching block
|
|
||||||
pop_to_ender(kw)
|
pop_to_ender(kw)
|
||||||
-- Add semicolon if needed
|
|
||||||
local formatted = get_indent() .. trimmed
|
local formatted = get_indent() .. trimmed
|
||||||
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
||||||
formatted = formatted .. ";"
|
formatted = formatted .. ";"
|
||||||
@@ -421,11 +429,9 @@ function M.format_document(content, options)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle same-level keywords (THEN, ELSE, ELSIF)
|
|
||||||
if SAME_LEVEL[kw] then
|
if SAME_LEVEL[kw] then
|
||||||
local formatted
|
local formatted
|
||||||
if kw == "THEN" then
|
if kw == "THEN" then
|
||||||
-- Pop IF block temporarily to get correct indent
|
|
||||||
local if_block = nil
|
local if_block = nil
|
||||||
for i = #stack, 1, -1 do
|
for i = #stack, 1, -1 do
|
||||||
if stack[i].type == "IF" then
|
if stack[i].type == "IF" then
|
||||||
@@ -439,14 +445,11 @@ function M.format_document(content, options)
|
|||||||
formatted = get_indent() .. trimmed
|
formatted = get_indent() .. trimmed
|
||||||
end
|
end
|
||||||
table.insert(formatted_lines, formatted)
|
table.insert(formatted_lines, formatted)
|
||||||
-- Push content block after THEN
|
|
||||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||||
elseif kw == "ELSE" or kw == "ELSIF" then
|
elseif kw == "ELSE" or kw == "ELSIF" then
|
||||||
-- Pop THEN_CONTENT if present
|
|
||||||
if #stack > 0 and stack[#stack].type == "THEN_CONTENT" then
|
if #stack > 0 and stack[#stack].type == "THEN_CONTENT" then
|
||||||
table.remove(stack)
|
table.remove(stack)
|
||||||
end
|
end
|
||||||
-- Find IF block for correct indent
|
|
||||||
local if_block = nil
|
local if_block = nil
|
||||||
for i = #stack, 1, -1 do
|
for i = #stack, 1, -1 do
|
||||||
if stack[i].type == "IF" then
|
if stack[i].type == "IF" then
|
||||||
@@ -460,7 +463,6 @@ function M.format_document(content, options)
|
|||||||
formatted = get_indent() .. trimmed
|
formatted = get_indent() .. trimmed
|
||||||
end
|
end
|
||||||
table.insert(formatted_lines, formatted)
|
table.insert(formatted_lines, formatted)
|
||||||
-- Push new content block
|
|
||||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||||
else
|
else
|
||||||
formatted = get_indent() .. trimmed
|
formatted = get_indent() .. trimmed
|
||||||
@@ -469,18 +471,14 @@ function M.format_document(content, options)
|
|||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle case labels
|
|
||||||
if is_case_label(trimmed) then
|
if is_case_label(trimmed) then
|
||||||
local case_block, case_idx = find_case_block()
|
local case_block, case_idx = find_case_block()
|
||||||
if case_block then
|
if case_block then
|
||||||
-- Pop to just after CASE
|
|
||||||
while #stack > case_idx do
|
while #stack > case_idx do
|
||||||
table.remove(stack)
|
table.remove(stack)
|
||||||
end
|
end
|
||||||
-- Case label at CASE level + 1
|
|
||||||
local label_indent = string.rep(indent_char, case_block.indent + 1)
|
local label_indent = string.rep(indent_char, case_block.indent + 1)
|
||||||
table.insert(formatted_lines, label_indent .. trimmed)
|
table.insert(formatted_lines, label_indent .. trimmed)
|
||||||
-- Push case content level
|
|
||||||
table.insert(stack, { type = "CASE_LABEL", indent = case_block.indent + 1, no_indent = true })
|
table.insert(stack, { type = "CASE_LABEL", indent = case_block.indent + 1, no_indent = true })
|
||||||
else
|
else
|
||||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
@@ -488,7 +486,6 @@ function M.format_document(content, options)
|
|||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle block starters (IF, FOR, CASE, REGION, etc.)
|
|
||||||
if kw and BLOCKS[kw] and not VAR_KEYWORDS[kw] and not DECLARATIONS[kw] then
|
if kw and BLOCKS[kw] and not VAR_KEYWORDS[kw] and not DECLARATIONS[kw] then
|
||||||
local current_level = 0
|
local current_level = 0
|
||||||
for _, block in ipairs(stack) do
|
for _, block in ipairs(stack) do
|
||||||
@@ -502,9 +499,24 @@ function M.format_document(content, options)
|
|||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Regular line (including variable declarations inside VAR sections)
|
-- Regular line - check for assignment start
|
||||||
|
if trimmed:match(":=") then
|
||||||
|
local base_indent = get_indent()
|
||||||
|
local assign_pos = trimmed:find(":=")
|
||||||
|
if assign_pos then
|
||||||
|
local var_part = trimmed:sub(1, assign_pos - 1)
|
||||||
|
local var_trimmed = var_part:match("^(.-)%s*$")
|
||||||
|
assignment_continuation_indent = base_indent .. string.rep(" ", #var_trimmed + 4)
|
||||||
|
|
||||||
|
if not trimmed:match(";%s*$") then
|
||||||
|
in_assignment = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
in_assignment = false
|
||||||
|
end
|
||||||
|
|
||||||
local formatted = get_indent() .. trimmed
|
local formatted = get_indent() .. trimmed
|
||||||
-- Add semicolon if it looks like it needs one
|
|
||||||
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
||||||
formatted = formatted .. ";"
|
formatted = formatted .. ";"
|
||||||
end
|
end
|
||||||
@@ -513,7 +525,6 @@ function M.format_document(content, options)
|
|||||||
::continue::
|
::continue::
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Join lines
|
|
||||||
local result = table.concat(formatted_lines, "\n")
|
local result = table.concat(formatted_lines, "\n")
|
||||||
if content:sub(-1) == "\n" then
|
if content:sub(-1) == "\n" then
|
||||||
result = result .. "\n"
|
result = result .. "\n"
|
||||||
@@ -534,22 +545,7 @@ function M.get_formatting_options()
|
|||||||
trimTrailingWhitespace = true,
|
trimTrailingWhitespace = true,
|
||||||
insertFinalNewline = true,
|
insertFinalNewline = true,
|
||||||
trimFinalNewlines = true,
|
trimFinalNewlines = true,
|
||||||
collapseAttributes = false, -- Disabled by default to preserve toggle functionality
|
collapseAttributes = false,
|
||||||
-- Set to true to enable automatic collapsing during formatting:
|
|
||||||
-- collapseAttributes = true,
|
|
||||||
-- collapsePatterns = { ... }, -- Override default collapse patterns
|
|
||||||
-- extendCollapsePatterns = { ... }, -- Extend default patterns with custom ones
|
|
||||||
-- collapseVariableRules = { -- Per-variable collapse rules (highest priority)
|
|
||||||
-- {
|
|
||||||
-- variablePattern = "^stat", -- Match variable names starting with "stat"
|
|
||||||
-- attributePattern = "EXTERNAL", -- Optional: also match attribute content
|
|
||||||
-- collapse = true, -- true to collapse, false to expand
|
|
||||||
-- },
|
|
||||||
-- {
|
|
||||||
-- variablePattern = "^temp", -- Match variables starting with "temp"
|
|
||||||
-- collapse = false, -- Never collapse these
|
|
||||||
-- },
|
|
||||||
-- },
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user