- Replace lspconfig.scl_lsp registration with vim.lsp.start() + FileType autocmd for Neovim 0.11+ compatibility - Fix Lua reserved keyword 'end' in tables (use bracket notation ['end']) - Fix JSON decoder pattern matching bugs (s:find -> s:sub:match) - Add LSP method name conversion (textDocument/didOpen -> textDocument_didOpen) - Handle request vs notification correctly (only respond to requests with id) - Strip CRLF line endings for Windows compatibility - Add semantic token handler aliases - Update documentation with implementation notes
213 lines
6.3 KiB
Markdown
213 lines
6.3 KiB
Markdown
# AGENTS.md - SCL Language Server (Unified)
|
|
|
|
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
|
|
|
|
### LSP Server
|
|
```bash
|
|
make start # Start LSP server
|
|
make test # Run LSP in test mode
|
|
lua src/main.lua --test # Direct test mode execution
|
|
```
|
|
|
|
### Tree-sitter Parser
|
|
```bash
|
|
tree-sitter generate # Generate parser from grammar.js
|
|
tree-sitter test # Run tree-sitter tests
|
|
tree-sitter parse <file.scl> # Parse a single SCL file
|
|
```
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
scl_lsp/
|
|
├── src/ # Standalone LSP server (uses dofile)
|
|
│ ├── main.lua # Entry point, JSON-RPC protocol
|
|
│ ├── parser.lua # Regex-based SCL parser
|
|
│ ├── diagnostics.lua # Linter diagnostics
|
|
│ └── formatter.lua # Document formatter
|
|
├── lua/scl/ # Neovim plugin modules (uses require)
|
|
│ ├── blink_cmp_source.lua # blink.cmp completion source
|
|
│ ├── udt_parser.lua # UDT (.udt) parser
|
|
│ ├── db_parser.lua # Global DB (.db) parser
|
|
│ ├── fb_parser.lua # Function Block parser
|
|
│ ├── variables.lua # Local variable extraction
|
|
│ ├── workspace_types.lua # Workspace scanning
|
|
│ └── multiline_params.lua # FB parameter filling
|
|
└── queries/ # Tree-sitter queries
|
|
```
|
|
|
|
## Code Style Guidelines
|
|
|
|
### Module Pattern
|
|
All modules follow the standard Lua module pattern:
|
|
```lua
|
|
-- Module description comment at top
|
|
local M = {}
|
|
|
|
-- Private module-level 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`, `var_types`)
|
|
- Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`)
|
|
- Private functions: declare as `local function` before public functions
|
|
- Boolean variables: 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
|
|
|
|
### Comments
|
|
- Every file must have a header comment explaining its purpose
|
|
- All functions should have a brief comment describing what they do
|
|
- Complex logic requires inline comments explaining the "why"
|
|
- Non-obvious regex patterns must have explanatory comments
|
|
|
|
### Parser Module API Convention
|
|
All parser modules (udt_parser, db_parser, fb_parser) must implement:
|
|
- `parse_*_file(filepath)` - Parse a file and cache result
|
|
- `parse_*_content(content, filename)` - Parse string content
|
|
- `get_*(name)` - Get single cached item by name
|
|
- `get_all_*_names()` - Return list of all cached names
|
|
- `get_*_members(name)` - Get members/fields for a type
|
|
- `is_*_type(name)` - Check if name is a known type
|
|
- `clear_cache()` - Clear all cached data
|
|
- `get_cache_count()` - Return number of cached items
|
|
|
|
### Cache Management
|
|
- Use module-level local tables for caching: `local cache = {}`
|
|
- Clear caches by iterating and setting to nil, not by reassigning:
|
|
```lua
|
|
function M.clear_cache()
|
|
for k in pairs(cache) do
|
|
cache[k] = nil
|
|
end
|
|
end
|
|
```
|
|
|
|
## Lua Reserved Keywords
|
|
|
|
Lua reserved keywords (like `end`, `for`, `in`, etc.) cannot be used as table keys directly. Use bracket notation:
|
|
```lua
|
|
-- WRONG: causes syntax error
|
|
local range = { start = pos1, end = pos2 }
|
|
|
|
-- CORRECT: use bracket notation
|
|
local range = { start = pos1, ["end"] = pos2 }
|
|
```
|
|
|
|
This is especially important for LSP range objects which require an `end` field per the LSP specification.
|
|
|
|
## Error Handling
|
|
|
|
### Return Pattern
|
|
Functions that can fail return `nil, "error message"`:
|
|
```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
|
|
Always wrap external requires in `pcall()`:
|
|
```lua
|
|
local ok, module = pcall(require, "some_module")
|
|
if ok and module then
|
|
module.do_something()
|
|
end
|
|
```
|
|
|
|
### Validation
|
|
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)
|
|
- `.` - Member access (UDT/DB members)
|
|
- `"` - Global DB names
|
|
- `(` - FB/Function parameters
|
|
- ` ` (space) - General completion
|
|
|
|
### Active Portion Detection
|
|
When matching patterns in completion, use the "active portion" of the line (after the last operator) to correctly handle expressions like:
|
|
```scl
|
|
"DB1".member := "DB2".member
|
|
```
|
|
Find the last `:=`, `=>`, `=`, `<>`, `>=`, `<=`, `>`, `<`, `AND`, `OR`, `NOT` and only match patterns after it.
|
|
|
|
## Commands
|
|
|
|
| Command | Description |
|
|
|---------|-------------|
|
|
| `:SCLShowVariables` | Show local variables in current file |
|
|
| `:SCLShowWorkspaceTypes` | Show workspace UDTs, FBs, and Global DBs count |
|
|
| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs |
|
|
| `:SCLPrefixWord` | Manually prefix current word with `#` |
|
|
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
|
|
| `:LspSCLFormat` | Format current SCL file |
|
|
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ |
|
|
|
|
## LSP Server Implementation Notes
|
|
|
|
### Method Name Conversion
|
|
LSP methods like `textDocument/didOpen` are converted to handler names like `textDocument_didOpen`:
|
|
```lua
|
|
local method_name = message.method:gsub("/", "_")
|
|
local handler = handlers[method_name]
|
|
```
|
|
|
|
### Request vs Notification
|
|
- Requests have `id` field and require a response
|
|
- Notifications have no `id` and should not receive a response
|
|
```lua
|
|
if message.id then
|
|
-- Only send response for requests
|
|
return { id = message.id, result = result }
|
|
end
|
|
```
|
|
|
|
### Line Ending Handling
|
|
The LSP server strips CRLF (`\r\n`) line endings for Windows compatibility:
|
|
```lua
|
|
line = line:gsub("\r$", "")
|
|
```
|
|
|
|
### JSON Parser
|
|
The standalone JSON parser in `src/json.lua` handles:
|
|
- Objects, arrays, strings, numbers, booleans, null
|
|
- Escape sequences in strings
|
|
- Whitespace skipping
|