362 lines
10 KiB
Markdown
362 lines
10 KiB
Markdown
# AGENTS.md - SCL Language Server
|
|
|
|
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
|
|
|
|
## Build/Lint/Test Commands
|
|
|
|
```bash
|
|
# LSP Server
|
|
make start # Start LSP server
|
|
make test # Run LSP in test mode
|
|
lua src/main.lua --test # Direct test execution
|
|
|
|
# Tree-sitter Parser
|
|
tree-sitter generate # Generate parser from grammar.js
|
|
tree-sitter test # Run parser tests
|
|
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
|
|
|
|
```
|
|
scl_lsp/
|
|
├── src/ # LSP server (uses dofile)
|
|
│ ├── main.lua # Entry point, JSON-RPC protocol
|
|
│ ├── parser.lua # SCL parser
|
|
│ ├── diagnostics.lua # Linter
|
|
│ ├── formatter.lua # Document formatter
|
|
│ ├── treesitter.lua # Tree-sitter integration
|
|
│ ├── plc_json.lua # External UDT loading
|
|
│ └── json.lua # JSON encoder/decoder
|
|
├── 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
|
|
│ ├── variables.lua # Variable extraction
|
|
│ ├── workspace_types.lua # Workspace scanning
|
|
│ ├── multiline_params.lua
|
|
│ ├── 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
|
|
```
|
|
|
|
## Filetype Detection
|
|
|
|
The LSP requires the `scl` filetype to be set. Add to your Neovim config (e.g., `ftdetect/scl.vim`):
|
|
|
|
```vim
|
|
au BufRead,BufNewFile *.scl set filetype=scl
|
|
au BufRead,BufNewFile *.udt 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
|
|
|
|
### Module Pattern
|
|
```lua
|
|
-- File header comment
|
|
local M = {}
|
|
|
|
-- Private cache
|
|
local cache = {}
|
|
|
|
function M.public_function()
|
|
end
|
|
|
|
local function private_function()
|
|
end
|
|
|
|
return M
|
|
```
|
|
|
|
### Naming Conventions
|
|
- Functions/variables: `snake_case` (e.g., `get_udt_members`)
|
|
- Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`)
|
|
- Private functions: declare as `local function` before public functions
|
|
- Booleans: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`)
|
|
|
|
### Imports
|
|
- `lua/scl/` modules: use `require("scl.module_name")`
|
|
- `src/` modules: use `dofile(script_path .. "/module.lua")`
|
|
- External dependencies: wrap in `pcall()` for safety
|
|
|
|
### Code Formatting
|
|
- `lua/scl/`: 2 spaces indentation
|
|
- `src/`: tab-based indentation
|
|
- Max line length: 120 characters
|
|
- No trailing whitespace
|
|
|
|
### Parser Module API
|
|
All parser modules must implement:
|
|
- `parse_*_file(filepath)` - Parse and cache
|
|
- `parse_*_content(content, filename)` - Parse string
|
|
- `get_*(name)` - Get cached item
|
|
- `get_all_*_names()` - List all cached names
|
|
- `get_*_members(name)` - Get members/fields
|
|
- `is_*_type(name)` - Check if known type
|
|
- `clear_cache()` - Clear cached data
|
|
- `get_cache_count()` - Return cache size
|
|
|
|
### Cache Management
|
|
```lua
|
|
local cache = {}
|
|
|
|
function M.clear_cache()
|
|
for k in pairs(cache) do
|
|
cache[k] = nil
|
|
end
|
|
end
|
|
```
|
|
|
|
## Lua Reserved Keywords
|
|
|
|
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
|
|
function M.parse_file(filepath)
|
|
local file = io.open(filepath, "r")
|
|
if not file then
|
|
return nil, "Cannot open file: " .. filepath
|
|
end
|
|
-- ... parse logic
|
|
return result
|
|
end
|
|
```
|
|
|
|
### External Dependencies
|
|
```lua
|
|
local ok, module = pcall(require, "some_module")
|
|
if ok and module then
|
|
module.do_something()
|
|
end
|
|
```
|
|
|
|
## Completion Triggers
|
|
- `#` - Local variables (after BEGIN)
|
|
- `.` - Member access (UDT/DB)
|
|
- `"` - Global DB names
|
|
- `(` - FB/Function parameters
|
|
- ` ` (space) - General completion
|
|
|
|
## Formatter Options
|
|
|
|
The formatter supports collapsing verbose attribute blocks:
|
|
|
|
```lua
|
|
-- Before formatting:
|
|
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
|
|
|
|
-- After formatting:
|
|
statCntrPartsIn{...} : Int;
|
|
```
|
|
|
|
### Formatter Configuration
|
|
```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:
|
|
1. Format with `:LspSCLFormat` (preserves original content)
|
|
2. Manually collapse with `:SCLCollapseAllAttrBlocks` (stores original content)
|
|
3. Now toggle/expand works correctly
|
|
|
|
### 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
|
|
|
|
| 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).
|
|
|
|
## 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.
|
|
|
|
### 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
|
|
|
|
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
|
|
```lua
|
|
local method_name = message.method:gsub("/", "_")
|
|
local handler = handlers[method_name]
|
|
```
|
|
|
|
### Request vs Notification
|
|
```lua
|
|
if message.id then
|
|
-- Request: send response
|
|
return { id = message.id, result = result }
|
|
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"
|
|
```
|