Files
tia-lsp.nvim/AGENTS.md
T
lazar 35eae17f4f feat(lsp): add goto declaration for FB, DB, and UDT types
- Add declaration provider (gD) that finds definitions for:
  - Functions in .scl files
  - Function Blocks (FB) in .scl files
  - Data Blocks (DB) in .scl and .db files
  - User-Defined Types (UDT) in .scl and .udt files
- Implement project root detection for cross-directory searches
- Add duplicate location filtering to prevent multiple results
- Load UDT types from .udt files in plc_json
- Update documentation with goto declaration feature
2026-02-19 18:01:30 +01:00

9.9 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 = false,  -- Disabled by default (see note below)
  collapsePatterns = {        -- Override default patterns
    "^%s*{%s*EXTERNAL",
    "^%s*{ S7_",
  },
  extendCollapsePatterns = {  -- Add custom patterns
    "^%s*{%s*CUSTOM",
  },
}

Note: collapseAttributes is disabled by default to preserve compatibility with the interactive toggle feature (:SCLToggleAttrBlock). When the formatter automatically collapses blocks, the original content is lost. To use both formatting and toggle:

  1. Format with :LspSCLFormat (preserves original content)
  2. Manually collapse with :SCLCollapseAllAttrBlocks (stores original content)
  3. Now toggle/expand works correctly

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

LSP Features

Goto Declaration (gD)

The LSP supports goto declaration functionality for:

  • Functions - Jump to function definition in .scl files
  • Function Blocks (FB) - Jump to FB definition in .scl files
  • Data Blocks (DB) - Jump to DB definition in .scl and .db files
  • User-Defined Types (UDT) - Jump to type definition in .scl and .udt files

Usage: Place cursor on a variable or type name and press gD (or use LSP client command).

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"