diff --git a/AGENTS.md b/AGENTS.md index 81aa766..acf480c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,34 +1,19 @@ -# AGENTS.md - tia-lsp +# AGENTS.md - tia-lsp.nvim -A Neovim/LazyVim plugin and standalone LSP server for Siemens TIA Portal languages. Currently supports **SCL**; planned support for STL/AWL, LAD, FBD. The project is split into two repos: - -- **`tia-lsp`** (this repo) — LSP server (`src/`) + tree-sitter grammar (`grammar.js`). Editor-agnostic. -- **`tia-lsp.nvim`** — Neovim plugin (`lua/`) that launches the server and adds editor features. +Neovim plugin for Siemens TIA Portal languages. Currently supports **SCL**; planned support for STL/AWL, LAD, FBD. This repo is the plugin only (`lua/` + tree-sitter queries). The companion LSP server lives in `tia-lsp`. ## Build/Lint/Test Commands ```bash -# 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 all parser tests -tree-sitter parse # Parse single SCL file - # Lua Linting -luacheck src/ # Lint LSP server code -luacheck lua/tia/ # Lint plugin modules +luacheck lua/tia/ # Lint language modules +luacheck lua/tia_lsp/ # Lint plugin entry point # Unit & Integration Tests -lua test/run_tests.lua # Run all tests +lua test/run_tests.lua # Run all plugin tests lua test/test_udt.lua # Run UDT parser tests lua test/test_db.lua # Run DB parser tests lua test/test_fb.lua # Run FB parser tests -lua test/test_formatter.lua # Run formatter tests -lua test/test_plc_json.lua # Run plc_json tests lua test/test_integration.lua # Run integration tests # Reference Project @@ -39,17 +24,8 @@ lua test/test_integration.lua # Run integration tests ## Project Structure ``` -tia-lsp/ -├── src/ # LSP server (uses dofile) -│ ├── main.lua # Entry point, JSON-RPC protocol -│ ├── parser.lua # SCL parser (will become parser_scl.lua + dispatch) -│ ├── diagnostics.lua # Linter (source = "tia_lsp") -│ ├── formatter.lua # Document formatter -│ ├── treesitter.lua # Tree-sitter integration -│ ├── plc_json.lua # External UDT/DB loading (language-agnostic) -│ ├── builtin_instructions.lua # TIA Portal built-in types -│ └── json.lua # JSON encoder/decoder -├── lua/tia/ # Neovim plugin (uses require) +tia-lsp.nvim/ +├── lua/tia/ # Language modules (uses require) │ ├── udt_parser.lua # UDT parser │ ├── db_parser.lua # Global DB parser │ ├── fb_parser.lua # Function Block parser @@ -61,21 +37,20 @@ tia-lsp/ ├── lua/tia_lsp/ # Plugin entry point │ ├── init.lua # Main plugin setup (vim.lsp.start) │ └── plugins/scl.lua # LazyVim plugin spec -├── queries/ # Tree-sitter queries (per-language: queries/scl/) -└── grammar.js # Tree-sitter grammar (language name: 'scl') +├── queries/scl/ # Tree-sitter queries (per-language) +└── ftdetect/scl.vim # Filetype detection ``` ## Naming Convention -- **Project name** (repo, Lua module, LSP server, mason package): `tia-lsp` / `tia_lsp` — umbrella for all TIA languages. -- **Language name** (filetype, tree-sitter parser, grammar): `scl` — stays as-is. Future languages (`stl`, `lad`, `fbd`) get their own names. +- **Project name** (repo, Lua module): `tia-lsp.nvim` / `tia_lsp` — umbrella for all TIA languages. +- **Language name** (filetype, tree-sitter parser): `scl` — stays as-is. Future languages (`stl`, `lad`, `fbd`) get their own names. - **LSP client name**: `tia_lsp` (one client handles all TIA languages; dispatches by `filetype` / `languageId`). -- **Diagnostic source**: `tia_lsp`. - **Commands**: `:SCLFormat`, `:SCLShowVariables`, etc. — per-filetype prefix. Future: `:STLFormat`, etc. ## Filetype Detection -After the split, `ftdetect/scl.vim` will live in `tia-lsp.nvim`. Currently required in Neovim config: +`ftdetect/scl.vim` registers the following extensions as `scl` filetype: ```vim au BufRead,BufNewFile *.scl set filetype=scl au BufRead,BufNewFile *.udt set filetype=scl @@ -108,12 +83,10 @@ return M ### Imports - `lua/tia/` modules: `require("tia.module_name")` - `lua/tia_lsp/` entry: `require("tia_lsp")` -- `src/` modules: `dofile(script_path .. "/module.lua")` - External dependencies: wrap in `pcall()` for safety ### Code Formatting - `lua/tia/` and `lua/tia_lsp/`: 2 spaces indentation -- `src/`: tab-based indentation - Max line length: 120 characters - No trailing whitespace @@ -189,7 +162,5 @@ Use the reference project for testing: ``` Contains `.scl`, `.db`, `.udt`, and `.xml` files for comprehensive testing of: -- LSP features (hover, completion, go-to-definition) -- Formatter (`:SCLFormat`) -- Attribute block toggle +- Plugin features (auto-prefix, workspace scanning, attribute toggle) - Workspace type scanning diff --git a/README.md b/README.md index b7541f5..f3688ce 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,9 @@ -# tia-lsp - Language Server for Siemens TIA Portal +# tia-lsp.nvim - Neovim Plugin for Siemens TIA Portal -A Neovim/LazyVim plugin and standalone LSP server providing **Language Server Protocol (LSP)**, **Linting**, **Formatting**, and **Syntax Highlighting** for Siemens TIA Portal languages. Currently supports **SCL** (Structured Control Language), with planned support for **STL/AWL**, **LAD**, and **FBD**. - -The project is split into two repos: - -- **`tia-lsp`** (this repo) — the standalone LSP server (`src/`) plus the tree-sitter grammar (`grammar.js`, `src/parser.c`). Speaks JSON-RPC over stdio. Editor-agnostic: usable from Neovim, VS Code, or any LSP client. -- **`tia-lsp.nvim`** — the Neovim plugin (`lua/`) that launches the server via `vim.lsp.start()` and adds editor-specific features (auto-prefix, blink.cmp source, workspace UDT scanning, attribute toggle, multiline params). +A Neovim/LazyVim plugin that launches the [`tia-lsp`](https://gitea.l-tech.rs/lazar/tia-lsp) language server and adds editor-specific features for Siemens TIA Portal languages. Currently supports **SCL** (Structured Control Language), with planned support for **STL/AWL**, **LAD**, and **FBD**. ## Features -### LSP (Language Server Protocol) -| Feature | Description | -|---------|-------------| -| **Hover** | Press `K` to see variable type and struct fields | -| **Completion** | `#` for variables, `.` for struct fields, `(` for functions | -| **Go to Definition** | `gD` jump to function, FB, DB, or UDT definition | -| **Document Symbols** | Shows functions, variables, types | -| **Workspace Symbols** | Search across all `.scl` files with fuzzy matching | -| **Semantic Tokens** | Keywords, variables highlighted | -| **Inlay Hints** | Shows variable types after declaration | -| **External UDT** | Load types from `.plc.json` or `data_types/` folder | - -### Linter (Diagnostics) -| Feature | Description | -|---------|-------------| -| **Undefined Variables** | Detects variables used without declaration | -| **Invalid # Prefix** | Detects `#` prefix in VAR sections | -| **Unknown Types** | Warns about unknown data types | -| **Type Validation** | Basic type checking | - -### Formatter -| Feature | Description | -|---------|-------------| -| **Document Formatting** | Format entire SCL files with `:SCLFormat` | -| **Multi-line Assignments** | Preserves and formats multi-line expressions with proper alignment | -| **FB Call Formatting** | Formats function block calls in multiline style | -| **Indentation** | Proper block indentation | -| **Keyword Casing** | Maintains SCL keyword casing | - -### Syntax Highlighting -- Full SCL syntax support via tree-sitter -- Block definitions: ORGANIZATION_BLOCK, FUNCTION_BLOCK, FUNCTION -- Variable sections: VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_TEMP, VAR_CONSTANT -- Control structures: IF/THEN/ELSIF/ELSE/END_IF, CASE/OF/END_CASE, FOR/TO/BY/DO/END_FOR, WHILE/END_WHILE, REPEAT/UNTIL/END_REPEAT -- Built-in data types: BOOL, INT, DINT, REAL, STRING, TIME, etc. -- Function block instances highlighted in calls -- FB parameter names (IN, PT, CU, etc.) highlighted as properties -- `#` prefix for local variables highlighted -- Operators: AND, OR, NOT, XOR, MOD -- Comments: line (`//`), block (`(* *)`), C-style (`/* */`) - -### Additional Features | Feature | Description | |---------|-------------| | **Auto-Prefix** | Automatically adds `#` prefix to local variables (like TIA Portal) | @@ -60,65 +13,24 @@ The project is split into two repos: | **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 `xa` | +| **Tree-sitter Highlighting** | Syntax highlighting via tree-sitter queries | ## Installation -The plugin auto-detects the mason-installed `tia-lsp` executable via `vim.fn.exepath("tia-lsp")` and falls back to `lua ` for manual installs. See [Mason installation](#mason-installation) below for the recommended path. +The plugin auto-detects the mason-installed `tia-lsp` executable via `vim.fn.exepath("tia-lsp")` and falls back to `lua ` for manual installs. -### LazyVim -```lua --- ~/.config/nvim/lua/plugins/tia-lsp.lua -return { - "lazar/tia-lsp.nvim", - ft = "scl", - lazy = true, - dependencies = { - "neovim/nvim-lspconfig", - "nvim-treesitter/nvim-treesitter", - "saghen/blink.cmp", - }, - config = function() - require("tia_lsp").setup({ - -- LSP options - lsp = { - on_attach = function(client, bufnr) - -- custom keymaps - end, - }, - cmp = true, -- Enable blink.cmp integration - workspace_types = true, -- Enable workspace UDT scanning - auto_prefix = true, -- Enable auto-# prefixing - debug = false, - }) - end, -} -``` +### LazyVim + Mason (Recommended) -### Manual Setup -```lua --- In your init.lua or plugin file -require("tia_lsp").setup({ - server_path = "/path/to/tia-lsp/src/main.lua", -- only used if tia-lsp is not on PATH - ts_parser_url = "https://your-gitea/tia-lsp.git", -- tree-sitter parser source - cmp = true, - workspace_types = true, - auto_prefix = true, -}) -``` - -### Mason installation - -`tia-lsp` can be installed via [mason.nvim](https://github.com/mason-org/mason.nvim) using a custom registry. The setup below uses a `file:` registry sourced from a Gitea-hosted `tia-mason-registry` repo (cloned by lazy.nvim), and falls back to the official mason registry for any other packages. - -1. Clone the registry repo as a lazy.nvim plugin: +1. Add the mason registry as a lazy.nvim plugin: ```lua -- ~/.config/nvim/lua/plugins/tia-mason-registry.lua return { - url = "https://your-gitea/tia-mason-registry.git", + url = "https://gitea.l-tech.rs/lazar/tia-mason-registry.git", name = "tia-mason-registry", lazy = false, -- must be available before mason setup } ``` + 2. Register the registry with mason: ```lua -- ~/.config/nvim/lua/plugins/mason.lua @@ -132,17 +44,54 @@ require("tia_lsp").setup({ }, } ``` -3. Install `yq` (one-time prerequisite for mason's `file:` registry): - ``` - :MasonInstall yq - ``` -4. Install the server: - ``` - :MasonInstall tia-lsp - ``` -5. Verify: open a `.scl` file and run `:LspInfo` — `tia_lsp` should be attached, and `vim.fn.exepath("tia-lsp")` should resolve to `~/.local/share/nvim/mason/bin/tia-lsp`. -To upgrade: tag a new `vX.Y.Z` release on the `tia-lsp` Gitea repo, bump `source.id` in `tia-mason-registry/packages/tia-lsp/package.yaml`, push the registry repo, then `:MasonInstall tia-lsp` again. +3. Add the plugin: + ```lua + -- ~/.config/nvim/lua/plugins/tia-lsp.lua + return { + "lazar/tia-lsp.nvim", + ft = "scl", + lazy = true, + dependencies = { + "neovim/nvim-lspconfig", + "nvim-treesitter/nvim-treesitter", + "saghen/blink.cmp", + }, + config = function() + require("tia_lsp").setup({ + lsp = { + on_attach = function(client, bufnr) + -- custom keymaps + end, + }, + cmp = true, -- Enable blink.cmp integration + workspace_types = true, -- Enable workspace UDT scanning + auto_prefix = true, -- Enable auto-# prefixing + debug = false, + }) + end, + } + ``` + +4. Install: + ``` + :MasonInstall yq " one-time prerequisite for file: registry + :MasonInstall tia-lsp " installs the LSP server + ``` + +5. Verify: open a `.scl` file and run `:LspInfo` — `tia_lsp` should be attached. + +### Manual Setup (without Mason) + +```lua +require("tia_lsp").setup({ + server_path = "/path/to/tia-lsp/src/main.lua", -- only used if tia-lsp is not on PATH + ts_parser_url = "https://gitea.l-tech.rs/lazar/tia-lsp.git", + cmp = true, + workspace_types = true, + auto_prefix = true, +}) +``` ## Configuration Options @@ -207,152 +156,65 @@ statCounter{...} : Int; - Use `xae` to expand all blocks in buffer - Use `xac` to collapse all blocks -**Note:** Collapsed blocks are automatically expanded before saving to preserve full content on disk. Use `:SCLFormat` to format, then `:SCLCollapseAllAttrBlocks` to collapse while preserving the ability to expand later. +**Note:** Collapsed blocks are automatically expanded before saving to preserve full content on disk. -## External UDT Support +## Filetype Detection -### Option 1: Create `.plc.json` -```json -{ - "dataTypes": [ - { - "name": "equipmentData", - "struct": [ - {"name": "id", "dataTypeName": "INT"}, - {"name": "status", "dataTypeName": "BOOL"}, - {"name": "temperature", "dataTypeName": "REAL"} - ] - } - ] -} -``` +The plugin includes `ftdetect/scl.vim` which registers the following file extensions as `scl` filetype: +- `*.scl` — SCL source files +- `*.udt` — User-defined type files +- `*.db` — Data block files -### Option 2: Auto-scan `data_types/` folder -The LSP automatically scans for SCL files in `data_types/` folder and extracts TYPE definitions. - -### Option 3: Generate `.plc.json` -```bash -lua ~/dev/tia-lsp/src/plc_json.lua --generate -``` - -## Global Data Block Support - -The plugin automatically scans for `.db` files in the workspace and provides auto-completion for global data blocks and their members. - -### How it works - -1. **Workspace Scanning**: When opening a `.scl` file, the plugin scans the project for `.db` files -2. **DB Parsing**: Parses `DATA_BLOCK` definitions and extracts members from `VAR` sections -3. **Auto-completion**: Provides smart completion for DB names and members - -### Usage - -- Type a space or `#` to trigger completion - global DB names appear alongside local variables -- Type `"` followed by a DB name to get member completions -- Access nested members: `"MyDB".member.submember` - -### Example - -```scl -// Access global DB members -"HmiData".units -"Parameter".machine.unit100.paramTimeStampApplied - -// Inside FB call parameter -#instParameterManager(hmiInterface:="HmiData".units, ...) -``` - -### Project Structure +## Project Structure ``` -tia-lsp/ -├── README.md # This file -├── package.json # NPM scripts (tree-sitter) -├── Makefile # Build commands -├── grammar.js # Tree-sitter grammar (SCL language) -├── tree-sitter.json # Parser config -├── queries/ # Tree-sitter queries -│ └── scl/ # Per-language query dir (SCL) +tia-lsp.nvim/ +├── lua/ +│ ├── tia_lsp/ # Plugin entry point +│ │ ├── init.lua # Main setup (vim.lsp.start, exepath detection) +│ │ └── plugins/ +│ │ └── scl.lua # LazyVim plugin spec (SCL filetype) +│ └── tia/ # Language modules (shared across TIA languages) +│ ├── auto_prefix.lua # Auto-# prefixing +│ ├── attr_toggle.lua # Attribute block expand/collapse +│ ├── blink_cmp_source.lua # blink.cmp completion source +│ ├── builtin_instructions.lua +│ ├── db_parser.lua # Global DB parser +│ ├── fb_parser.lua # Function Block parser +│ ├── multiline_params.lua # Multiline parameter filling +│ ├── udt_parser.lua # UDT parser +│ ├── variables.lua # Variable extraction +│ └── workspace_types.lua # Workspace scanning +├── queries/ +│ └── scl/ # Tree-sitter queries (per-language) │ ├── highlights.scm # Syntax highlighting │ ├── indents.scm # Indentation │ ├── folds.scm # Code folding │ ├── locals.scm # Variable scoping │ └── tags.scm # Navigation tags -├── src/ # LSP server -│ ├── main.lua # Main entry point (JSON-RPC over stdio) -│ ├── parser.lua # SCL parser (will become parser_scl.lua + dispatch) -│ ├── treesitter.lua # Tree-sitter integration -│ ├── json.lua # JSON encoder/decoder -│ ├── plc_json.lua # External UDT loading (language-agnostic) -│ ├── diagnostics.lua # Linter diagnostics -│ └── formatter.lua # Document formatter -└── lua/ - ├── tia_lsp/ # Neovim plugin - │ ├── init.lua # Main plugin setup - │ └── plugins/ - │ └── scl.lua # LazyVim plugin spec (SCL filetype) - └── tia/ # Language modules (shared across TIA languages) - ├── auto_prefix.lua - ├── attr_toggle.lua - ├── blink_cmp_source.lua - ├── builtin_instructions.lua - ├── db_parser.lua - ├── fb_parser.lua - ├── multiline_params.lua - ├── udt_parser.lua - ├── variables.lua - └── workspace_types.lua +├── ftdetect/ +│ └── scl.vim # Filetype detection +└── test/ # Plugin tests + ├── run_tests.lua + ├── test_harness.lua + ├── test_udt.lua + ├── test_db.lua + ├── test_fb.lua + └── test_integration.lua ``` -## Building Tree-sitter Parser - -```bash -# Generate parser from grammar.js -npm run build -# or: tree-sitter generate - -# Run tests -npm test -# or: tree-sitter test -``` - -## SCL Code Example - -```pascal -FUNCTION_BLOCK "Em101Sequence" -VAR - x : INT; - myVar : equipmentData; // User-defined type -END_VAR - -IF x > 5 THEN - myVar.id := 10; -END_IF - -myVar.status := TRUE; -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/tia_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.11+** (uses `vim.lsp.start()`) -- **nvim-treesitter** - Syntax highlighting -- **blink.cmp** - Optional, for completion -- **Lua 5.1+** - LSP server runtime +- **tia-lsp** — the LSP server (install via mason or manually) +- **nvim-treesitter** — Syntax highlighting +- **blink.cmp** — Optional, for completion +- **Lua 5.1+** — LSP server runtime + +## Testing + +```bash +lua test/run_tests.lua +``` + +Tests use files from a reference project (override with `SCL_REF_PROJECT=/path/to/ref lua test/run_tests.lua`). diff --git a/ftdetect/scl.vim b/ftdetect/scl.vim new file mode 100644 index 0000000..35547f0 --- /dev/null +++ b/ftdetect/scl.vim @@ -0,0 +1,3 @@ +au BufRead,BufNewFile *.scl set filetype=scl +au BufRead,BufNewFile *.udt set filetype=scl +au BufRead,BufNewFile *.db set filetype=scl diff --git a/queries/folds.scm b/queries/scl/folds.scm similarity index 100% rename from queries/folds.scm rename to queries/scl/folds.scm diff --git a/queries/highlights.scm b/queries/scl/highlights.scm similarity index 100% rename from queries/highlights.scm rename to queries/scl/highlights.scm diff --git a/queries/indents.scm b/queries/scl/indents.scm similarity index 100% rename from queries/indents.scm rename to queries/scl/indents.scm diff --git a/queries/locals.scm b/queries/scl/locals.scm similarity index 100% rename from queries/locals.scm rename to queries/scl/locals.scm diff --git a/queries/tags.scm b/queries/scl/tags.scm similarity index 100% rename from queries/tags.scm rename to queries/scl/tags.scm diff --git a/test/run_tests.lua b/test/run_tests.lua index 4f622ee..4e9ab6f 100644 --- a/test/run_tests.lua +++ b/test/run_tests.lua @@ -1,11 +1,11 @@ #!/usr/bin/env lua -package.path = package.path .. ";lua/?.lua;src/?.lua" +package.path = package.path .. ";lua/?.lua" local test = dofile("test/test_harness.lua") print("========================================") -print(" SCL LSP - Test Suite") +print(" tia-lsp.nvim - Plugin Test Suite") print("========================================") print(string.format("Reference Project: %s", test.REF_PROJECT)) @@ -28,8 +28,6 @@ local function run_all_tests() run_test_file("test_udt.lua") run_test_file("test_db.lua") run_test_file("test_fb.lua") - run_test_file("test_formatter.lua") - run_test_file("test_plc_json.lua") run_test_file("test_integration.lua") test.report() diff --git a/test/test_integration.lua b/test/test_integration.lua index 49e1214..d9eeac7 100644 --- a/test/test_integration.lua +++ b/test/test_integration.lua @@ -1,17 +1,11 @@ local test = dofile("test/test_harness.lua") -package.path = package.path .. ";lua/?.lua;src/?.lua" +package.path = package.path .. ";lua/?.lua" local udt_parser = require("tia.udt_parser") local db_parser = require("tia.db_parser") local fb_parser = require("tia.fb_parser") -local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or "." -script_path = script_path:gsub("^test/", ""):gsub("/tests$", "") -if script_path == "" then script_path = "." end - -local plc_json = dofile(script_path .. "/src/plc_json.lua") - local function scan_directory(dir, ext) local files = {} local handle = io.popen('find "' .. dir .. '" -type f -name "*.' .. ext .. '" 2>/dev/null') @@ -234,9 +228,6 @@ local function run_integration_tests() print("\n--- Type Resolution ---") test_type_resolution() - - print("\n--- Workspace Type Loading ---") - test_workspace_type_loading() end) end