Compare commits

..
4 Commits
Author SHA1 Message Date
lazar 3776ce33ff refactor: change the default path for local dev env 2026-07-20 17:01:37 +02:00
lazar 1acac2b73c docs: rewrite installation with architecture overview, troubleshooting, and cross-references 2026-07-19 15:09:58 +02:00
lazar 03548dab7d feat: comprehensive query files for all block types and VAR sections (Phase 3)
Update all 5 tree-sitter query files to cover the full grammar:

highlights.scm:
- Add highlights for new literals (binary, octal, date, bool, typed_int,
  wstring, char)
- Add highlights for block names (organization_block, function_block,
  function, data_block, type_definition)
- Add highlights for return_statement, goto_statement, title_statement
- Add highlights for bit_access, method_callee
- Add compound assignment operators (+=, -=, *=, /=, etc.)

indents.scm:
- Add function, type_definition, data_block to @indent.begin
- Add all var_*_declaration variants to @indent.begin
- Add struct_type to @indent.begin
- Add END_FUNCTION, END_TYPE, END_DATA_BLOCK, END_STRUCT to @indent.end

folds.scm:
- Add function, type_definition, data_block to @fold
- Add all var_*_declaration variants to @fold
- Add struct_type to @fold

locals.scm:
- Add function, type_definition, data_block as @local.scope
- Add all var_*_declaration variants as @local.scope
- Add struct_type as @local.scope
- Add block name definitions (@definition.function, @definition.class,
  @definition.type)
- Narrow @reference to prefixed_identifier only (reduces noise)

tags.scm:
- Add function, data_block, type_definition tags
- Add FB call references
- Add region_statement as @definition.module
2026-07-18 19:20:33 +02:00
lazar aebffc34e7 fix: load compiled parser.so via vim.treesitter.language.add
nvim-treesitter's :TSInstall reloads the parsers module from source
(package.loaded['nvim-treesitter.parsers'] = nil), which wipes runtime
config modifications. The old get_parser_configs() approach no longer
works for registering custom parsers with :TSInstall.

New approach:
- The mason build script now compiles parser.c into parser.so during
  :MasonInstall tia-lsp (requires cc/gcc)
- The plugin's setup_syntax finds parser.so via:
  1. opts.ts_parser_path (explicit override)
  2. Mason package dir (resolved from exepath('tia-lsp'))
  3. ~/dev/tia-lsp/parser.so (local dev fallback)
- Loads it with vim.treesitter.language.add('scl', { path = parser.so })
- Starts highlighting via vim.treesitter.start(bufnr, 'scl') in the
  FileType autocmd
- Falls back to notifying the user if no parser.so is found
- Still registers with nvim-treesitter's parsers table as best-effort
  for :TSInstall support (won't survive reload, but works in-session)
2026-07-18 18:01:32 +02:00
7 changed files with 621 additions and 346 deletions
+112 -58
View File
@@ -15,77 +15,120 @@ A Neovim/LazyVim plugin that launches the [`tia-lsp`](https://gitea.l-tech.rs/la
| **Attribute Block Toggle** | Interactive expand/collapse of `{...}` blocks with `<Leader>xa` | | **Attribute Block Toggle** | Interactive expand/collapse of `{...}` blocks with `<Leader>xa` |
| **Tree-sitter Highlighting** | Syntax highlighting via tree-sitter queries | | **Tree-sitter Highlighting** | Syntax highlighting via tree-sitter queries |
## Architecture
This plugin is part of a three-repo ecosystem:
```
tia-lsp.nvim ← Neovim plugin (you are here) — launches the LSP client,
│ adds editor features (auto-# prefix, blink.cmp source,
│ attribute toggle, workspace scanning, ...)
tia-lsp ← Standalone LSP server — reads JSON-RPC over stdio,
│ provides hover, completion, diagnostics, formatting
│ Requires: lua src/main.lua + compiled parser.so
tia-mason-registry ← mason.nvim recipe — tells mason how to download tia-lsp,
compile parser.so, and create the tia-lsp executable
```
- **tia-lsp** ([repo](https://gitea.l-tech.rs/lazar/tia-lsp)) — the actual language server, editor-agnostic
- **tia-mason-registry** ([repo](https://gitea.l-tech.rs/lazar/tia-mason-registry)) — a single `package.yaml` for mason
- **tia-lsp.nvim** (this repo) — the Neovim-specific integration
## Installation ## Installation
The plugin auto-detects the mason-installed `tia-lsp` executable via `vim.fn.exepath("tia-lsp")` and falls back to `lua <server_path>` for manual installs. The plugin auto-detects the mason-installed `tia-lsp` executable via `vim.fn.exepath("tia-lsp")`. If not found, it falls back to running `lua <server_path>` (configurable).
### LazyVim + Mason (Recommended) ### LazyVim + Mason (Recommended)
1. Add the mason registry as a lazy.nvim plugin: Mason downloads and builds the `tia-lsp` server; this plugin configures the LSP client.
```lua
-- ~/.config/nvim/lua/plugins/tia-mason-registry.lua
return {
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: **Step 1: Add the mason registry as a lazy.nvim plugin**
```lua
-- ~/.config/nvim/lua/plugins/mason.lua
return {
"mason-org/mason.nvim",
opts = {
registries = {
"file:" .. vim.fn.expand("~/.local/share/nvim/lazy/tia-mason-registry"),
"github:mason-org/mason-registry",
},
},
}
```
3. Add the plugin: mason needs a local copy of the registry to read the install recipe. Adding it as a lazy.nvim plugin gets it cloned automatically:
```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: ```lua
``` -- ~/.config/nvim/lua/plugins/tia-mason-registry.lua
:MasonInstall yq " one-time prerequisite for file: registry return {
:MasonInstall tia-lsp " installs the LSP server url = "https://gitea.l-tech.rs/lazar/tia-mason-registry.git",
``` name = "tia-mason-registry",
}
```
5. Verify: open a `.scl` file and run `:LspInfo` — `tia_lsp` should be attached. **Step 2: Register the registry with mason**
```lua
-- ~/.config/nvim/lua/plugins/mason.lua
return {
"mason-org/mason.nvim",
opts = {
registries = {
"file:" .. vim.fn.expand("~/.local/share/nvim/lazy/tia-mason-registry"),
"github:mason-org/mason-registry",
},
},
}
```
> **Why this works:** lazy.nvim clones `tia-mason-registry` to `~/.local/share/nvim/lazy/tia-mason-registry`. The `file:` registry path points mason to that directory, where it finds `packages/tia-lsp/package.yaml`. Mason needs [`yq`](https://github.com/mikefarah/yq) to parse YAML custom registries — install it once: `:MasonInstall yq`
**Step 3: Install the LSP server**
```
:MasonInstall tia-lsp
```
This clones `tia-lsp` from the source repo, compiles `parser.so` (tree-sitter), and places the `tia-lsp` executable on PATH.
**Step 4: Add this plugin**
```lua
-- ~/.config/nvim/lua/plugins/tia-lsp.lua
return {
"https://gitea.l-tech.rs/lazar/tia-lsp.nvim",
ft = "scl",
lazy = true,
dependencies = {
"neovim/nvim-lspconfig",
"nvim-treesitter/nvim-treesitter",
"saghen/blink.cmp", -- optional, for completion
},
opts = {
lsp = {
-- on_attach = function(client, bufnr) ... end,
},
cmp = true, -- enable blink.cmp integration
workspace_types = true, -- scan project for UDTs and DBs
auto_prefix = true, -- auto-# prefix for local variables
},
}
```
**Step 5: Verify**
Open a `.scl` file and run `:LspInfo`. `tia_lsp` should be attached. Run `:checkhealth` for detailed diagnostics.
---
### Manual Setup (without Mason) ### Manual Setup (without Mason)
Clone and build the server manually, then point the plugin at it.
```bash
# 1. Clone the LSP server
git clone --depth 1 https://gitea.l-tech.rs/lazar/tia-lsp.git ~/tia-lsp
# 2. Compile the tree-sitter parser (required for parsing SCL)
cc -fPIC -I ~/tia-lsp/src/tree_sitter -c ~/tia-lsp/src/parser.c -o ~/tia-lsp/parser.o
cc -shared ~/tia-lsp/parser.o -o ~/tia-lsp/parser.so
```
Then configure the plugin. The plugin first looks for `tia-lsp` on PATH; if not found, it falls back to running `lua <server_path>`. The tree-sitter parser is loaded from `parser.so` adjacent to the server.
```lua ```lua
require("tia_lsp").setup({ require("tia_lsp").setup({
server_path = "/path/to/tia-lsp/src/main.lua", -- only used if tia-lsp is not on PATH server_path = "~/tia-lsp/src/main.lua", -- fallback if tia-lsp not on PATH
ts_parser_url = "https://gitea.l-tech.rs/lazar/tia-lsp.git", ts_parser_url = "https://gitea.l-tech.rs/lazar/tia-lsp.git",
cmp = true, cmp = true,
workspace_types = true, workspace_types = true,
@@ -97,7 +140,7 @@ require("tia_lsp").setup({
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| |--------|------|---------|-------------|
| `server_path` | string | `~/dev/tia-lsp/src/main.lua` | Path to LSP server (only used if `tia-lsp` is not on PATH) | | `server_path` | string | `~/dev/tia-lsp/src/main.lua` | Path to LSP server (fallback when `tia-lsp` not on PATH; change for manual installs) |
| `ts_parser_url` | string | `file://~/dev/tia-lsp` | URL passed to nvim-treesitter for the SCL parser source | | `ts_parser_url` | string | `file://~/dev/tia-lsp` | URL passed to nvim-treesitter for the SCL parser source |
| `lsp.on_attach` | function | `nil` | Custom LSP attach callback | | `lsp.on_attach` | function | `nil` | Custom LSP attach callback |
| `lsp.formatting` | boolean | `false` | Enable format command | | `lsp.formatting` | boolean | `false` | Enable format command |
@@ -211,6 +254,17 @@ tia-lsp.nvim/
- **blink.cmp** — Optional, for completion - **blink.cmp** — Optional, for completion
- **Lua 5.1+** — LSP server runtime - **Lua 5.1+** — LSP server runtime
## Troubleshooting
| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| `:MasonInstall tia-lsp` fails with "unknown package" | Custom registry not registered with mason | Check the `registries` list in your mason config includes the `file:` path to the cloned registry |
| `:MasonInstall tia-lsp` fails with "yq: command not found" | Mason needs `yq` to parse the YAML registry | Run `:MasonInstall yq` to install it, then retry `:MasonInstall tia-lsp` |
| `tia_lsp` not attached after opening `.scl` | Server not on PATH or `server_path` points to wrong location | Run `:LspInfo` to see what server is configured. Check `vim.fn.exepath("tia-lsp")` returns a path |
| LSP starts but no syntax highlighting | Tree-sitter parser (`parser.so`) not found | mason install generates it automatically. For manual setups, run the `cc` commands in the Manual Setup section |
| `parser.so` load error at startup | Compiled for wrong architecture or Lua version | Recompile: clean the old `.so` and run the `cc` build commands again |
| LSP starts but no completions/hover | Server can't find its modules | Mason installs to a managed directory. If using manual setup, ensure `src/` contents are present next to `main.lua` |
## Testing ## Testing
```bash ```bash
+346 -252
View File
@@ -1,328 +1,422 @@
local M = {} local M = {}
local function setup_package_path() local function setup_package_path()
local plugin_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or "" local plugin_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
plugin_path = plugin_path:gsub("/lua/tia_lsp", "") plugin_path = plugin_path:gsub("/lua/tia_lsp", "")
local lua_path = plugin_path .. "/lua" local lua_path = plugin_path .. "/lua"
if not package.path:match(lua_path) then if not package.path:match(lua_path) then
package.path = lua_path .. "/?.lua;" .. package.path package.path = lua_path .. "/?.lua;" .. package.path
end end
end end
function M.setup(opts) function M.setup(opts)
setup_package_path() setup_package_path()
opts = opts or {} opts = opts or {}
local server_path = opts.server_path or vim.fn.expand("~/dev/tia-lsp/src/main.lua") local server_path = opts.server_path or vim.fn.expand("~/Documents/lua/tia-lsp/src/main.lua")
local root_patterns = { ".git", "data_types", "plc.data.json" } local root_patterns = { ".git", "data_types", "plc.data.json" }
local function start_lsp(bufnr) local function start_lsp(bufnr)
local fname = vim.api.nvim_buf_get_name(bufnr) local fname = vim.api.nvim_buf_get_name(bufnr)
if fname == "" then return end if fname == "" then
return
end
local root_dir = vim.fs.root(fname, root_patterns) local root_dir = vim.fs.root(fname, root_patterns) or vim.fn.fnamemodify(fname, ":p:h")
or vim.fn.fnamemodify(fname, ":p:h")
-- Prefer the mason-installed executable (vim.fn.exepath resolves on each -- Prefer the mason-installed executable (vim.fn.exepath resolves on each
-- buffer enter, so a :MasonInstall mid-session is picked up without restart). -- buffer enter, so a :MasonInstall mid-session is picked up without restart).
-- Fall back to running the server via `lua <server_path>` for manual installs. -- Fall back to running the server via `lua <server_path>` for manual installs.
local tia_bin = vim.fn.exepath("tia-lsp") local tia_bin = vim.fn.exepath("tia-lsp")
local cmd = tia_bin ~= "" and { "tia-lsp" } or { "lua", server_path } local cmd = tia_bin ~= "" and { "tia-lsp" } or { "lua", server_path }
vim.lsp.start({ vim.lsp.start({
name = "tia_lsp", name = "tia_lsp",
cmd = cmd, cmd = cmd,
root_dir = root_dir, root_dir = root_dir,
on_attach = function(client, b) on_attach = function(client, b)
if opts.lsp and opts.lsp.on_attach then if opts.lsp and opts.lsp.on_attach then
opts.lsp.on_attach(client, b) opts.lsp.on_attach(client, b)
end end
if opts.lsp and opts.lsp.formatting then if opts.lsp and opts.lsp.formatting then
vim.api.nvim_buf_create_user_command(b, "SCLFormat", function() vim.api.nvim_buf_create_user_command(b, "SCLFormat", function()
vim.lsp.buf.format({ name = "tia_lsp" }) vim.lsp.buf.format({ name = "tia_lsp" })
end, {}) end, {})
end end
end, end,
}) })
end end
-- Create FileType autocmd for future buffers -- Create FileType autocmd for future buffers
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = "scl", pattern = "scl",
callback = function(args) callback = function(args)
start_lsp(args.buf) start_lsp(args.buf)
end, M.notify_parser_install_if_needed(args.buf)
}) -- Start tree-sitter highlighting if the parser is registered
pcall(vim.treesitter.start, args.buf, "scl")
end,
})
-- Create global format command (works on current buffer) -- Create global format command (works on current buffer)
vim.api.nvim_create_user_command("SCLFormat", function() vim.api.nvim_create_user_command("SCLFormat", function()
vim.lsp.buf.format({ name = "tia_lsp" }) vim.lsp.buf.format({ name = "tia_lsp" })
end, {}) end, {})
-- Start LSP for any existing scl buffers (handles lazy loading case) -- Start LSP for any existing scl buffers (handles lazy loading case)
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do 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 if vim.bo[bufnr].filetype == "scl" and vim.api.nvim_buf_is_loaded(bufnr) then
start_lsp(bufnr) start_lsp(bufnr)
end M.notify_parser_install_if_needed(bufnr)
end pcall(vim.treesitter.start, bufnr, "scl")
end
end
M.setup_syntax(opts) M.setup_syntax(opts)
M.setup_multiline_params() M.setup_multiline_params()
M.setup_attr_toggle() M.setup_attr_toggle()
M.create_commands() M.create_commands()
if opts.auto_prefix ~= false then if opts.auto_prefix ~= false then
M.setup_auto_prefix() M.setup_auto_prefix()
end end
end
-- Find a compiled tree-sitter parser (.so) for SCL.
-- Looks in: opts.ts_parser_path, mason package dir, ~/dev/tia-lsp.
-- Returns the path to parser.so, or nil if not found.
function M.find_parser_so(opts)
-- 1. Explicit path from opts
if opts.ts_parser_path and vim.fn.filereadable(opts.ts_parser_path) == 1 then
return opts.ts_parser_path
end
-- 2. Mason install: resolve exepath("tia-lsp") -> real bin -> package dir
local tia_bin = vim.fn.exepath("tia-lsp")
if tia_bin ~= "" then
local real_bin = vim.fn.resolve(vim.fn.fnamemodify(tia_bin, ":p"))
local pkg_dir = vim.fn.fnamemodify(real_bin, ":h:h")
local so = pkg_dir .. "/parser.so"
if vim.fn.filereadable(so) == 1 then
return so
end
end
-- 3. Local dev path
local dev_so = vim.fn.expand("~/dev/tia-lsp/parser.so")
if vim.fn.filereadable(dev_so) == 1 then
return dev_so
end
return nil
end
-- Notify the user once per session if the tree-sitter parser isn't available.
local parser_notified = false
function M.notify_parser_install_if_needed(bufnr)
if parser_notified then
return
end
if vim.bo[bufnr].filetype ~= "scl" then
return
end
local ok = pcall(vim.treesitter.get_parser, bufnr, "scl")
if ok then
return
end
parser_notified = true
vim.schedule(function()
local msg = "SCL: Tree-sitter parser not found. "
if vim.fn.exepath("tia-lsp") ~= "" then
msg = msg .. "Run :MasonInstall tia-lsp to install it."
else
msg = msg .. "Install tia-lsp via mason or compile parser.so manually."
end
vim.notify(msg, vim.log.levels.INFO)
end)
end end
function M.setup_syntax(opts) function M.setup_syntax(opts)
local has_ts, ts = pcall(require, "nvim-treesitter.parsers") -- Load the compiled tree-sitter parser (.so) via Neovim's built-in API.
if has_ts and ts.get_parser_configs then -- This bypasses nvim-treesitter's install system, which reloads its
local ok, parser_config = pcall(ts.get_parser_configs) -- parsers module from source on :TSInstall and wipes runtime config.
if ok and parser_config then local parser_so = M.find_parser_so(opts)
parser_config.scl = { if parser_so then
install_info = { local ok, err = pcall(vim.treesitter.language.add, "scl", { path = parser_so })
-- Override via setup({ ts_parser_url = "https://your-gitea/tia-lsp.git" }). if not ok then
url = opts.ts_parser_url vim.notify(
or "https://gitea.l-tech.rs/lazar/tia-lsp.git", "SCL: Failed to load tree-sitter parser from " .. parser_so .. ": " .. tostring(err),
files = { "src/parser.c" }, vim.log.levels.WARN
generate_requires_npm = false, )
requires_generate_from_grammar = false, end
}, end
filetype = "scl",
}
end
end
if opts.cmp then -- Also register with nvim-treesitter (best-effort, for :TSInstall support
local function try_register() -- in sessions where the user wants to install from source).
local cmp_ok, cmp = pcall(require, "blink.cmp") local has_ts, parsers = pcall(require, "nvim-treesitter.parsers")
local blink_source_ok, blink_source = pcall(require, "tia.blink_cmp_source") if has_ts and type(parsers) == "table" then
if cmp_ok and cmp and cmp.add_source_provider and blink_source_ok then local parser_config
local source_config = { if type(parsers.get_parser_configs) == "function" then
name = "scl", parser_config = parsers.get_parser_configs()
provider = blink_source, else
score_offset = 100, parser_config = parsers
} end
pcall(cmp.add_source_provider, "scl", source_config) if type(parser_config) == "table" then
pcall(cmp.add_filetype_source, "scl", "scl") parser_config.scl = {
install_info = {
url = opts.ts_parser_url or "https://gitea.l-tech.rs/lazar/tia-lsp.git",
files = { "src/parser.c" },
generate_requires_npm = false,
requires_generate_from_grammar = false,
},
filetype = "scl",
}
end
end
vim.notify("SCL: blink.cmp source registered", vim.log.levels.INFO) if opts.cmp then
return true local function try_register()
end local cmp_ok, cmp = pcall(require, "blink.cmp")
return false local blink_source_ok, blink_source = pcall(require, "tia.blink_cmp_source")
end if cmp_ok and cmp and cmp.add_source_provider and blink_source_ok then
local source_config = {
name = "scl",
provider = blink_source,
score_offset = 100,
}
pcall(cmp.add_source_provider, "scl", source_config)
pcall(cmp.add_filetype_source, "scl", "scl")
if not try_register() then vim.notify("SCL: blink.cmp source registered", vim.log.levels.INFO)
vim.api.nvim_create_autocmd("BufEnter", { return true
pattern = "*.scl", end
once = true, return false
callback = try_register, end
})
end
end
if opts.workspace_types ~= false then if not try_register() then
local ok, workspace_types = pcall(require, "tia.workspace_types") vim.api.nvim_create_autocmd("BufEnter", {
if ok then pattern = "*.scl",
workspace_types.setup({ once = true,
project_root = opts.project_root, callback = try_register,
library_paths = opts.library_paths, })
debug = opts.debug, end
}) end
end
end if opts.workspace_types ~= false then
local ok, workspace_types = pcall(require, "tia.workspace_types")
if ok then
workspace_types.setup({
project_root = opts.project_root,
library_paths = opts.library_paths,
debug = opts.debug,
})
end
end
end end
function M.setup_auto_prefix() function M.setup_auto_prefix()
local ok, auto_prefix = pcall(require, "tia.auto_prefix") local ok, auto_prefix = pcall(require, "tia.auto_prefix")
if ok then if ok then
auto_prefix.setup() auto_prefix.setup()
end end
end end
function M.setup_multiline_params() function M.setup_multiline_params()
local ok, mp = pcall(require, "tia.multiline_params") local ok, mp = pcall(require, "tia.multiline_params")
if not ok then if not ok then
vim.notify("SCL multiline_params module not found", vim.log.levels.WARN) vim.notify("SCL multiline_params module not found", vim.log.levels.WARN)
return return
end end
vim.api.nvim_create_user_command("SCLMultilineParams", function() vim.api.nvim_create_user_command("SCLMultilineParams", function()
mp.fill_multiline_params() mp.fill_multiline_params()
end, {}) end, {})
vim.keymap.set("i", "<Space>mp", function() vim.keymap.set("i", "<Space>mp", function()
mp.fill_multiline_params() mp.fill_multiline_params()
end, { noremap = true, silent = true, desc = "SCL: Fill multiline parameters" }) end, { noremap = true, silent = true, desc = "SCL: Fill multiline parameters" })
vim.keymap.set("i", "<Tab>", function() vim.keymap.set("i", "<Tab>", function()
local ft = vim.bo.filetype local ft = vim.bo.filetype
if ft == "scl" or ft == "udt" then if ft == "scl" or ft == "udt" then
if mp.jump_to_next_param() then if mp.jump_to_next_param() then
return "" return ""
end end
end end
return "<Tab>" return "<Tab>"
end, { noremap = true, silent = true, desc = "SCL: Jump to next param or Tab" }) end, { noremap = true, silent = true, desc = "SCL: Jump to next param or Tab" })
end end
function M.prefix_current_word() function M.prefix_current_word()
local ok, auto_prefix = pcall(require, "tia.auto_prefix") local ok, auto_prefix = pcall(require, "tia.auto_prefix")
if ok then if ok then
auto_prefix.prefix_word() auto_prefix.prefix_word()
end end
end end
function M.show_variables() function M.show_variables()
local ok, variables = pcall(require, "tia.variables") local ok, variables = pcall(require, "tia.variables")
if not ok then if not ok then
vim.notify("SCL variables module not found", vim.log.levels.WARN) vim.notify("SCL variables module not found", vim.log.levels.WARN)
return return
end end
local vars = variables.extract_variables() local vars = variables.extract_variables()
if #vars == 0 then if #vars == 0 then
vim.notify("No local variables found", vim.log.levels.INFO) vim.notify("No local variables found", vim.log.levels.INFO)
return return
end end
local lines = { "Local Variables:", "---" } local lines = { "Local Variables:", "---" }
for _, var in ipairs(vars) do for _, var in ipairs(vars) do
local type_info = var.type and (" : " .. var.type) or "" local type_info = var.type and (" : " .. var.type) or ""
table.insert(lines, "#" .. var.name .. type_info) table.insert(lines, "#" .. var.name .. type_info)
end end
vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO) vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO)
end end
function M.show_workspace_types() function M.show_workspace_types()
local ok_ws, ws = pcall(require, "tia.workspace_types") local ok_ws, ws = pcall(require, "tia.workspace_types")
local ok_udt, udt = pcall(require, "tia.udt_parser") local ok_udt, udt = pcall(require, "tia.udt_parser")
local ok_fb, fb = pcall(require, "tia.fb_parser") local ok_fb, fb = pcall(require, "tia.fb_parser")
if not ok_ws or not ok_udt or not ok_fb then if not ok_ws or not ok_udt or not ok_fb then
vim.notify("SCL workspace modules not found", vim.log.levels.WARN) vim.notify("SCL workspace modules not found", vim.log.levels.WARN)
return return
end end
local root = ws.get_project_root() local root = ws.get_project_root()
local udt_names = udt.get_all_udt_names() local udt_names = udt.get_all_udt_names()
local fb_names = fb.get_all_fb_names() local fb_names = fb.get_all_fb_names()
if not root then if not root then
vim.notify("No project root detected", vim.log.levels.WARN) vim.notify("No project root detected", vim.log.levels.WARN)
return return
end end
vim.notify("Project: " .. root .. "\nUDTs: " .. #udt_names .. "\nFBs/Functions: " .. #fb_names, vim.log.levels.INFO) vim.notify("Project: " .. root .. "\nUDTs: " .. #udt_names .. "\nFBs/Functions: " .. #fb_names, vim.log.levels.INFO)
end end
function M.rescan_workspace_types() function M.rescan_workspace_types()
local ok_ws, ws = pcall(require, "tia.workspace_types") local ok_ws, ws = pcall(require, "tia.workspace_types")
local ok_udt, udt = pcall(require, "tia.udt_parser") local ok_udt, udt = pcall(require, "tia.udt_parser")
local ok_fb, fb = pcall(require, "tia.fb_parser") local ok_fb, fb = pcall(require, "tia.fb_parser")
if not ok_ws or not ok_udt or not ok_fb then if not ok_ws or not ok_udt or not ok_fb then
vim.notify("SCL workspace modules not found", vim.log.levels.WARN) vim.notify("SCL workspace modules not found", vim.log.levels.WARN)
return return
end end
udt.clear_cache() udt.clear_cache()
fb.clear_cache() fb.clear_cache()
ws.clear_root_cache() ws.clear_root_cache()
local udt_result = ws.scan_project_udts() local udt_result = ws.scan_project_udts()
local fb_result = ws.scan_project_fbs() local fb_result = ws.scan_project_fbs()
vim.notify("Scanned UDTs: " .. udt_result.parsed_count .. "/" .. udt_result.total_files .. vim.notify(
", FBs: " .. fb_result.parsed_count .. "/" .. fb_result.total_files, vim.log.levels.INFO) "Scanned UDTs: "
.. udt_result.parsed_count
.. "/"
.. udt_result.total_files
.. ", FBs: "
.. fb_result.parsed_count
.. "/"
.. fb_result.total_files,
vim.log.levels.INFO
)
end end
function M.create_commands() function M.create_commands()
vim.api.nvim_create_user_command("SCLShowVariables", function() M.show_variables() end, {}) vim.api.nvim_create_user_command("SCLShowVariables", function()
vim.api.nvim_create_user_command("SCLShowWorkspaceTypes", function() M.show_workspace_types() end, {}) M.show_variables()
vim.api.nvim_create_user_command("SCLRescanWorkspaceTypes", function() M.rescan_workspace_types() end, {}) end, {})
vim.api.nvim_create_user_command("SCLPrefixWord", function() M.prefix_current_word() 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, {})
vim.api.nvim_create_user_command("SCLPrefixWord", function()
M.prefix_current_word()
end, {})
-- Attribute toggle commands -- Attribute toggle commands
vim.api.nvim_create_user_command("SCLToggleAttrBlock", function() vim.api.nvim_create_user_command("SCLToggleAttrBlock", function()
local ok, at = pcall(require, "tia.attr_toggle") local ok, at = pcall(require, "tia.attr_toggle")
if ok then if ok then
at.toggle_attr_block() at.toggle_attr_block()
else else
vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR) vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR)
end end
end, {}) end, {})
vim.api.nvim_create_user_command("SCLExpandAllAttrBlocks", function() vim.api.nvim_create_user_command("SCLExpandAllAttrBlocks", function()
local ok, at = pcall(require, "tia.attr_toggle") local ok, at = pcall(require, "tia.attr_toggle")
if ok then if ok then
at.expand_all_attr_blocks() at.expand_all_attr_blocks()
else else
vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR) vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR)
end end
end, {}) end, {})
vim.api.nvim_create_user_command("SCLCollapseAllAttrBlocks", function() vim.api.nvim_create_user_command("SCLCollapseAllAttrBlocks", function()
local ok, at = pcall(require, "tia.attr_toggle") local ok, at = pcall(require, "tia.attr_toggle")
if ok then if ok then
at.collapse_all_attr_blocks() at.collapse_all_attr_blocks()
else else
vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR) vim.notify("SCL: Failed to load attr_toggle module: " .. tostring(at), vim.log.levels.ERROR)
end end
end, {}) end, {})
end end
function M.setup_attr_toggle() function M.setup_attr_toggle()
local ok, at = pcall(require, "tia.attr_toggle") local ok, at = pcall(require, "tia.attr_toggle")
if not ok then if not ok then
vim.notify("SCL: attr_toggle module not found", vim.log.levels.WARN) vim.notify("SCL: attr_toggle module not found", vim.log.levels.WARN)
return return
end end
at.setup() at.setup()
local function setup_buffer_keymaps(bufnr) local function setup_buffer_keymaps(bufnr)
local opts = { buffer = bufnr, silent = true } local opts = { buffer = bufnr, silent = true }
local ft = vim.bo[bufnr].filetype local ft = vim.bo[bufnr].filetype
if ft ~= "scl" and ft ~= "udt" then if ft ~= "scl" and ft ~= "udt" then
return return
end end
-- Toggle individual attribute block under cursor -- Toggle individual attribute block under cursor
vim.keymap.set({ "n", "i" }, "<Leader>xa", function() vim.keymap.set({ "n", "i" }, "<Leader>xa", function()
at.toggle_attr_block() at.toggle_attr_block()
end, vim.tbl_extend("force", opts, { desc = "SCL: Toggle attribute block" })) end, vim.tbl_extend("force", opts, { desc = "SCL: Toggle attribute block" }))
-- Expand all collapsed blocks -- Expand all collapsed blocks
vim.keymap.set("n", "<Leader>xae", function() vim.keymap.set("n", "<Leader>xae", function()
at.expand_all_attr_blocks() at.expand_all_attr_blocks()
end, vim.tbl_extend("force", opts, { desc = "SCL: Expand all attribute blocks" })) end, vim.tbl_extend("force", opts, { desc = "SCL: Expand all attribute blocks" }))
-- Collapse all attribute blocks -- Collapse all attribute blocks
vim.keymap.set("n", "<Leader>xac", function() vim.keymap.set("n", "<Leader>xac", function()
at.collapse_all_attr_blocks() at.collapse_all_attr_blocks()
end, vim.tbl_extend("force", opts, { desc = "SCL: Collapse all attribute blocks" })) end, vim.tbl_extend("force", opts, { desc = "SCL: Collapse all attribute blocks" }))
end end
-- Set up keybindings for SCL files only -- Set up keybindings for SCL files only
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = { "scl", "udt" }, pattern = { "scl", "udt" },
callback = function(args) callback = function(args)
setup_buffer_keymaps(args.buf) setup_buffer_keymaps(args.buf)
end, end,
}) })
-- Also check if there are already SCL buffers open -- Also check if there are already SCL buffers open
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(bufnr) then if vim.api.nvim_buf_is_loaded(bufnr) then
setup_buffer_keymaps(bufnr) setup_buffer_keymaps(bufnr)
end end
end end
end end
return M return M
+9
View File
@@ -3,12 +3,21 @@
[ [
(organization_block) (organization_block)
(function_block) (function_block)
(function)
(type_definition)
(data_block)
(var_declaration) (var_declaration)
(var_temp_declaration)
(var_constant_declaration)
(var_retain_declaration)
(var_non_retain_declaration)
(var_db_specific_declaration)
(if_statement) (if_statement)
(case_statement) (case_statement)
(for_statement) (for_statement)
(while_statement) (while_statement)
(repeat_statement) (repeat_statement)
(region_statement) (region_statement)
(struct_type)
(block_comment) (block_comment)
] @fold ] @fold
+58 -11
View File
@@ -1,66 +1,99 @@
; SCL highlights ; SCL highlights
; Comments
(line_comment) @comment (line_comment) @comment
(block_comment) @comment (block_comment) @comment
(c_style_comment) @comment (c_style_comment) @comment
; Literals
(number) @number (number) @number
(hex_number) @number (hex_number) @number
(string) @string (binary_number) @number
(octal_number) @number
(time_value) @number (time_value) @number
(date_literal) @number
(bool_literal) @constant.builtin
(typed_int_literal) @number
(wstring_literal) @string
(char_literal) @character
(string) @string
; Prefix
(prefix) @punctuation.special (prefix) @punctuation.special
; Variables and identifiers
(identifier) @variable (identifier) @variable
; FB instance calls - highlight instance name as type ; FB instance calls
(fb_call (fb_call
instance: (prefixed_identifier instance: (prefixed_identifier
(identifier) @type)) (identifier) @type))
(fb_call (fb_call
instance: (identifier) @type) instance: (identifier) @type)
; FB parameter names (IN :=, PT :=, etc.) ; FB parameter names
(fb_parameter (fb_parameter
name: (identifier) @property) name: (identifier) @property)
; Function call names ; Function call names
(function_call (function_call
(identifier) @function) (identifier) @function)
(function_call
(string) @function)
; Method call names
(method_callee
(identifier) @function)
; Variable declarations ; Variable declarations
(var_item (var_item
name: (identifier) @variable) name: (identifier) @variable)
; Type references in declarations ; Type references
(type (type
(identifier) @type) (identifier) @type)
(type_builtin) @type.builtin (type_builtin) @type.builtin
; Keywords and operators
(keyword) @keyword (keyword) @keyword
(constant) @constant.builtin (constant) @constant.builtin
(operator) @operator (operator) @operator
(assignment) @operator ; Block names
(binary_expression) @operator (organization_block
(unary_expression) @operator name: (string) @type)
(function_block
name: (string) @type)
(function
name: (string) @function)
(data_block
name: (string) @type)
(type_definition
name: (string) @type)
; Access
(field_access (field_access
"." "."
(identifier) @property) (identifier) @property)
(array_access) @property (array_access) @property
(bit_access) @property
; Attributes
(attribute_list) @attribute (attribute_list) @attribute
(attribute) @attribute (attribute) @attribute
(attribute_value) @string (attribute_value) @string
(var_item) @property ; Statements
(assignment) @operator
(binary_expression) @operator
(unary_expression) @operator
(chained_assignment) @operator
; Function/FB calls
(function_call) @function.call (function_call) @function.call
(fb_call) @function.call (fb_call) @function.call
; Control flow
(if_statement) @conditional (if_statement) @conditional
(elsif_clause) @conditional (elsif_clause) @conditional
(else_clause) @conditional (else_clause) @conditional
@@ -71,6 +104,13 @@
(while_statement) @repeat (while_statement) @repeat
(repeat_statement) @repeat (repeat_statement) @repeat
(return_statement) @keyword
(goto_statement) @keyword
; Title
(title_statement) @comment
; Punctuation
";" @punctuation.delimiter ";" @punctuation.delimiter
":" @punctuation.delimiter ":" @punctuation.delimiter
"," @punctuation.delimiter "," @punctuation.delimiter
@@ -80,15 +120,22 @@
"]" @punctuation.bracket "]" @punctuation.bracket
"." @punctuation.delimiter "." @punctuation.delimiter
; Operators
":=" @operator ":=" @operator
"=>" @operator "=>" @operator
"+=" @operator
"-=" @operator
"*=" @operator
"/=" @operator
"&=" @operator
"|=" @operator
"^=" @operator
"=" @operator "=" @operator
"<>" @operator "<>" @operator
"<" @operator "<" @operator
">" @operator ">" @operator
"<=" @operator "<=" @operator
">=" @operator ">=" @operator
"+" @operator "+" @operator
"-" @operator "-" @operator
"*" @operator "*" @operator
+26 -2
View File
@@ -1,10 +1,26 @@
; Indentation rules for SCL ; Indentation rules for SCL
; Indent after block keywords ; Indent after block definitions
[ [
(organization_block) (organization_block)
(function_block) (function_block)
(function)
(type_definition)
(data_block)
] @indent.begin
; Indent after VAR sections
[
(var_declaration) (var_declaration)
(var_temp_declaration)
(var_constant_declaration)
(var_retain_declaration)
(var_non_retain_declaration)
(var_db_specific_declaration)
] @indent.begin
; Indent after control structures
[
(if_statement) (if_statement)
(case_statement) (case_statement)
(for_statement) (for_statement)
@@ -13,12 +29,16 @@
(region_statement) (region_statement)
] @indent.begin ] @indent.begin
; Indent after THEN, DO, etc. ; Indent after STRUCT in UDTs and inline structs
(struct_type) @indent.begin
; Indent after THEN, DO, ELSE, ELSIF, BEGIN
[ [
"THEN" "THEN"
"DO" "DO"
"ELSE" "ELSE"
"ELSIF" "ELSIF"
(keyword) @indent.branch
] @indent.branch ] @indent.branch
; Outdent at end keywords ; Outdent at end keywords
@@ -30,8 +50,12 @@
"END_WHILE" "END_WHILE"
"END_REPEAT" "END_REPEAT"
"END_REGION" "END_REGION"
"END_STRUCT"
"END_ORGANIZATION_BLOCK" "END_ORGANIZATION_BLOCK"
"END_FUNCTION_BLOCK" "END_FUNCTION_BLOCK"
"END_FUNCTION"
"END_TYPE"
"END_DATA_BLOCK"
] @indent.end ] @indent.end
; Outdent for ELSE and ELSIF ; Outdent for ELSE and ELSIF
+30 -6
View File
@@ -1,15 +1,39 @@
; Local variable scoping for SCL - matches grammar.js structure ; Local variable scoping for SCL
; Variable declarations define local variables ; Variable declarations define local variables
(var_item (var_item
name: (identifier) @definition.var) name: (identifier) @definition.var)
; Variable sections create scopes
(var_declaration) @local.scope
; Blocks create scopes ; Blocks create scopes
(organization_block) @local.scope (organization_block) @local.scope
(function_block) @local.scope (function_block) @local.scope
(function) @local.scope
(type_definition) @local.scope
(data_block) @local.scope
; Identifiers are references ; VAR sections create scopes
(identifier) @reference (var_declaration) @local.scope
(var_temp_declaration) @local.scope
(var_constant_declaration) @local.scope
(var_retain_declaration) @local.scope
(var_non_retain_declaration) @local.scope
(var_db_specific_declaration) @local.scope
; Struct types create scopes
(struct_type) @local.scope
; Block names are definitions
(organization_block
name: (string) @definition.function)
(function_block
name: (string) @definition.function)
(function
name: (string) @definition.function)
(data_block
name: (string) @definition.class)
(type_definition
name: (string) @definition.type)
; References
(prefixed_identifier
(identifier) @reference)
+24 -1
View File
@@ -1,4 +1,4 @@
; Tag queries for SCL - matches grammar.js structure ; Tag queries for SCL - symbol navigation
; Organization blocks ; Organization blocks
(organization_block (organization_block
@@ -8,6 +8,18 @@
(function_block (function_block
name: (string) @name) @definition.function name: (string) @name) @definition.function
; Functions
(function
name: (string) @name) @definition.function
; Data blocks
(data_block
name: (string) @name) @definition.class
; Type definitions (UDTs)
(type_definition
name: (string) @name) @definition.type
; Variable definitions within blocks ; Variable definitions within blocks
(var_item (var_item
name: (identifier) @name name: (identifier) @name
@@ -25,3 +37,14 @@
(function_call (function_call
(string) @name) @reference.call (string) @name) @reference.call
; FB calls (references)
(fb_call
instance: (identifier) @name) @reference.call
(fb_call
instance: (prefixed_identifier
(identifier) @name)) @reference.call
; Region statements
(region_statement) @definition.module