Files
tia-lsp.nvim/AGENTS.md
T

334 lines
9.1 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
```
## 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 = 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
| 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 |
## 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"
```