Files
tia-lsp/AGENTS.md
T
lazar 978e46ba45 feat: add interactive attribute block toggle with keybindings and commands
Add ability to expand/collapse SCL variable attribute blocks like {EXTERNALACCESSIBLE := 'false'} to {...}

Features:
- Per-variable collapse rules via collapseVariableRules option
- Interactive toggle with <Leader>xa keybinding
- Commands: SCLToggleAttrBlock, SCLExpandAllAttrBlocks, SCLCollapseAllAttrBlocks
- Keybindings: <Leader>xa (toggle), <Leader>xae (expand all), <Leader>xac (collapse all)
- Supports both formatter-collapsed and manually collapsed blocks

Also update AGENTS.md with new documentation and troubleshooting guide
2026-02-19 10:12:58 +01:00

8.8 KiB

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

# 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

-- 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

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:

-- 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

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

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:

-- Before formatting:
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;

-- After formatting:
statCntrPartsIn{...} : Int;

Formatter Configuration

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):

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 print(vim.inspect(require("scl_lsp")))
    
  2. Verify commands exist:

    :command SCLToggleAttrBlock
    

    Should show the command definition.

  3. Check for errors during setup:

    :lua require("scl_lsp").setup({})
    
  4. Verify leader key:

    :echo mapleader
    

    If empty, your leader is \ (backslash).

  5. Manual test keybinding:

    :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

local method_name = message.method:gsub("/", "_")
local handler = handlers[method_name]

Request vs Notification

if message.id then
  -- Request: send response
  return { id = message.id, result = result }
end
-- Notification: no response needed

Script Path Pattern

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"