Files
tia-lsp/AGENTS.md
T

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

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

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"