Merge branch 'feat/scl_formatter' into develop

This commit is contained in:
2026-02-19 10:24:38 +01:00
5 changed files with 687 additions and 124 deletions
+241 -80
View File
@@ -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.
## 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
# LSP Server
make start # Start LSP server
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
```bash
# Tree-sitter Parser
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
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/ # 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
├── 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
All modules follow the standard Lua module pattern:
```lua
-- Module description comment at top
-- File header comment
local M = {}
-- Private module-level cache
-- Private cache
local cache = {}
function M.public_function()
@@ -66,37 +70,37 @@ return M
```
### 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`)
- 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
- `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
### Code Formatting
- `lua/scl/`: 2 spaces indentation
- `src/`: tab-based indentation
- Max line length: 120 characters
- No trailing whitespace
### 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
### 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
- Use module-level local tables for caching: `local cache = {}`
- Clear caches by iterating and setting to nil, not by reassigning:
```lua
local cache = {}
function M.clear_cache()
for k in pairs(cache) do
cache[k] = nil
@@ -104,10 +108,22 @@ function M.clear_cache()
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
Functions that can fail return `nil, "error message"`:
```lua
function M.parse_file(filepath)
local file = io.open(filepath, "r")
@@ -120,7 +136,6 @@ end
```
### External Dependencies
Always wrap external requires in `pcall()`:
```lua
local ok, module = pcall(require, "some_module")
if ok and module then
@@ -128,41 +143,187 @@ if ok and module then
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
## Completion Triggers
- `#` - Local variables (after BEGIN)
- `.` - Member access (UDT/DB members)
- `.` - Member access (UDT/DB)
- `"` - 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
## Formatter Options
The formatter supports collapsing verbose attribute blocks:
```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
| 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/ |
| `: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.
## 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"
```
+43 -2
View File
@@ -48,6 +48,7 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP)
| **Workspace UDT Scanner** | Scans project for user-defined types (.udt files) |
| **Workspace Global DB Scanner** | Scans project for global data blocks (.db files) |
| **blink.cmp Integration** | Smart completion source for blink.cmp |
| **Attribute Block Toggle** | Interactive expand/collapse of `{...}` blocks with `<Leader>xa` |
## Installation
@@ -116,6 +117,9 @@ require("scl_lsp").setup({
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
| `:LspSCLFormat` | Format current 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 |
## Keybindings
@@ -127,9 +131,32 @@ require("scl_lsp").setup({
| `<leader>w` | Workspace Symbols |
| `<Space>mp` | Fill multiline FB/Function parameters (insert mode) |
| `<Tab>` | Jump to next parameter (insert mode) |
| `<Leader>xa` | Toggle attribute block under cursor |
| `<Leader>xae` | Expand all attribute blocks |
| `<Leader>xac` | Collapse all attribute blocks |
| `<leader>fw` | Workspace Symbols (Telescope) |
| `<leader>ca` | Code Actions |
## Attribute Block Toggle
Interactive expand/collapse of SCL variable attribute blocks:
```scl
-- Before: Verbose attributes
statCounter{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
-- After: Collapsed
statCounter{...} : Int;
```
**Usage:**
- Place cursor inside any `{...}` block
- Press `<Leader>xa` to toggle expand/collapse
- Use `<Leader>xae` to expand all blocks in buffer
- Use `<Leader>xac` to collapse all blocks
The formatter also supports automatic collapsing via `collapseAttributes` option.
## External UDT Support
### Option 1: Create `.plc.json`
@@ -253,10 +280,24 @@ myVar.temperature := 25.5;
END_FUNCTION_BLOCK
```
## Architecture
The plugin uses a standalone LSP server (`src/main.lua`) that communicates via JSON-RPC over stdio. The server handles:
- Text document synchronization
- Diagnostics (linting)
- Formatting
- Completion, hover, go-to-definition
- Semantic tokens
The Neovim plugin (`lua/scl_lsp/init.lua`) manages:
- LSP client lifecycle via `vim.lsp.start()`
- FileType autocmd for lazy loading
- blink.cmp integration
- Workspace scanning for UDTs/DBs
## Requirements
- **Neovim 0.9+**
- **nvim-lspconfig** - LSP client
- **Neovim 0.11+** (uses `vim.lsp.start()`)
- **nvim-treesitter** - Syntax highlighting
- **blink.cmp** - Optional, for completion
- **Lua 5.1+** - LSP server runtime
+207
View File
@@ -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
+73
View File
@@ -88,6 +88,9 @@ function M.setup(opts)
-- Multiline parameters feature
M.setup_multiline_params()
-- Attribute block toggle feature
M.setup_attr_toggle()
end
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("SCLShowWorkspaceTypes", function() M.show_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
return M
+119 -38
View File
@@ -13,54 +13,58 @@ function M.setup(opts)
setup_package_path()
opts = opts or {}
local lspconfig_avail, lspconfig = pcall(require, "lspconfig")
if not lspconfig_avail then
vim.notify("lspconfig not found. Please install nvim-lspconfig.", vim.log.levels.ERROR)
return
end
local server_path = opts.server_path or vim.fn.expand("~/dev/scl_lsp/src/main.lua")
local root_patterns = { ".git", "data_types", "plc.data.json" }
local function get_root_dir(fname)
local root = vim.fs.root(fname, root_patterns)
return root or vim.fn.fnamemodify(fname, ":h")
end
local function on_attach(client, bufnr)
if opts.lsp and opts.lsp.on_attach then
opts.lsp.on_attach(client, bufnr)
end
local function start_lsp(bufnr)
local fname = vim.api.nvim_buf_get_name(bufnr)
if fname == "" then return end
if opts.lsp.formatting then
vim.api.nvim_buf_create_user_command(bufnr, "LspSCLFormat", function()
vim.lsp.buf.format({ name = "scl-language-server" })
end, {})
end
end
local root_dir = vim.fs.root(fname, root_patterns)
or vim.fn.fnamemodify(fname, ":p:h")
lspconfig.scl_lsp = {
default_config = {
name = "scl-language-server",
vim.lsp.start({
name = "scl_lsp",
cmd = { "lua", server_path },
filetypes = { "scl" },
root_dir = get_root_dir,
settings = {},
on_attach = on_attach,
},
docs = {
description = "Language server for Siemens SCL (LSP, Linter, Formatter)",
default_config = {
name = "scl-language-server",
cmd = { "lua", server_path },
filetypes = { "scl" },
root_dir = "root_pattern('.git', 'data_types', 'plc.data.json')",
},
},
}
root_dir = root_dir,
on_attach = function(client, b)
if opts.lsp and opts.lsp.on_attach then
opts.lsp.on_attach(client, b)
end
if opts.lsp and opts.lsp.formatting then
vim.api.nvim_buf_create_user_command(b, "LspSCLFormat", function()
vim.lsp.buf.format({ name = "scl_lsp" })
end, {})
end
end,
})
end
-- Create FileType autocmd for future buffers
vim.api.nvim_create_autocmd("FileType", {
pattern = "scl",
callback = function(args)
start_lsp(args.buf)
end,
})
-- Create global format command (works on current buffer)
vim.api.nvim_create_user_command("LspSCLFormat", function()
vim.lsp.buf.format({ name = "scl_lsp" })
end, {})
-- Start LSP for any existing scl buffers (handles lazy loading case)
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[bufnr].filetype == "scl" and vim.api.nvim_buf_is_loaded(bufnr) then
start_lsp(bufnr)
end
end
M.setup_syntax(opts)
M.setup_multiline_params()
M.setup_attr_toggle()
M.create_commands()
if opts.auto_prefix ~= false then
@@ -234,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("SCLRescanWorkspaceTypes", function() M.rescan_workspace_types() 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
return M