From 69f5c7b215e1e2a819be51c5649694113add3e2f Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Fri, 20 Feb 2026 23:29:26 +0100 Subject: [PATCH] feat: improve tree-sitter syntax highlighting with named nodes - Add aliases for keywords (VAR_INPUT, BEGIN, IF, etc.) as named nodes - Add aliases for built-in types (INT, BOOL, STRING, etc.) as type_builtin - Add aliases for constants (TRUE, FALSE) - Add aliases for operators (AND, OR, NOT, MOD) - Add prefix node for # variable prefix highlighting - Highlight FB instance names in calls - Highlight FB parameter names as properties - Update README with detailed syntax highlighting features - Update .gitignore for *.wasm files --- .gitignore | 1 + README.md | 14 +- lua/scl/builtin_instructions.lua | 136 +++++++++++++++++ lua/scl/init.lua | 45 ++++-- lua/scl/multiline_params.lua | 83 ++++++++--- queries/highlights.scm | 247 +++++++------------------------ 6 files changed, 294 insertions(+), 232 deletions(-) create mode 100644 lua/scl/builtin_instructions.lua diff --git a/.gitignore b/.gitignore index 8cc4343..649febe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.o *.so +*.wasm *.log .DS_Store diff --git a/README.md b/README.md index 46ee012..37236ac 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,15 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP) ### Syntax Highlighting - Full SCL syntax support via tree-sitter -- ORGANIZATION_BLOCK, FUNCTION_BLOCK, FUNCTION definitions -- Variable declarations (VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_TEMP, etc.) -- Control structures (IF/THEN/ELSE, CASE, FOR, WHILE, REPEAT) -- Code regions and comments -- All SCL data types and operators +- 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 | diff --git a/lua/scl/builtin_instructions.lua b/lua/scl/builtin_instructions.lua new file mode 100644 index 0000000..5059ef8 --- /dev/null +++ b/lua/scl/builtin_instructions.lua @@ -0,0 +1,136 @@ +local M = {} + +M.FUNCTIONS = { + ADD = true, + SUB = true, + MUL = true, + DIV = true, + MIN = true, + MAX = true, + LIMIT = true, + MOVE = true, + CONVERT = true, + ROUND = true, + TRUNC = true, + CEIL = true, + FLOOR = true, + SQR = true, + SQRT = true, + SIN = true, + COS = true, + TAN = true, + ASIN = true, + ACOS = true, + ATAN = true, + LEN = true, + CONCAT = true, + LEFT = true, + RIGHT = true, + MID = true, + AND = true, + OR = true, + XOR = true, + SHL = true, + SHR = true, + ROL = true, + ROR = true, + BCD_I = true, + I_BCD = true, +} + +M.FUNCTION_BLOCKS = { + TON = true, + TOF = true, + TP = true, + TONR = true, + CTU = true, + CTD = true, + CTUD = true, + IEC_TIMER = true, + IEC_TON = true, + IEC_TOF = true, + IEC_TP = true, + IEC_COUNTER = true, + IEC_CTU = true, + IEC_CTD = true, + IEC_CTUD = true, + R_TRIG = true, + F_TRIG = true, + SR = true, + RS = true, + SCHEDULE = true, + BPM = true, + LOG = true, + DATALOG = true, +} + +M.PARAMETERS = { + TON = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + TOF = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + TP = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + TONR = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" }, { name = "R", type = "BOOL" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + CTU = { inputs = { { name = "CU", type = "BOOL" }, { name = "R", type = "BOOL" }, { name = "PV", type = "INT" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "CV", type = "INT" } } }, + CTD = { inputs = { { name = "CD", type = "BOOL" }, { name = "LD", type = "BOOL" }, { name = "PV", type = "INT" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "CV", type = "INT" } } }, + CTUD = { inputs = { { name = "CU", type = "BOOL" }, { name = "CD", type = "BOOL" }, { name = "R", type = "BOOL" }, { name = "LD", type = "BOOL" }, { name = "PV", type = "INT" } }, outputs = { { name = "QU", type = "BOOL" }, { name = "QD", type = "BOOL" }, { name = "CV", type = "INT" } } }, + R_TRIG = { inputs = { { name = "CLK", type = "BOOL" } }, outputs = { { name = "Q", type = "BOOL" } } }, + F_TRIG = { inputs = { { name = "CLK", type = "BOOL" } }, outputs = { { name = "Q", type = "BOOL" } } }, + SR = { inputs = { { name = "S", type = "BOOL" }, { name = "R1", type = "BOOL" } }, outputs = { { name = "Q1", type = "BOOL" } } }, + RS = { inputs = { { name = "S1", type = "BOOL" }, { name = "R", type = "BOOL" } }, outputs = { { name = "Q1", type = "BOOL" } } }, + IEC_TIMER = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + IEC_TON = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + IEC_TOF = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + IEC_TP = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } }, + IEC_COUNTER = { inputs = { { name = "CU", type = "BOOL" }, { name = "CD", type = "BOOL" }, { name = "R", type = "BOOL" }, { name = "LD", type = "BOOL" }, { name = "PV", type = "INT" } }, outputs = { { name = "QU", type = "BOOL" }, { name = "QD", type = "BOOL" }, { name = "CV", type = "INT" } } }, + IEC_CTU = { inputs = { { name = "CU", type = "BOOL" }, { name = "R", type = "BOOL" }, { name = "PV", type = "INT" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "CV", type = "INT" } } }, + IEC_CTD = { inputs = { { name = "CD", type = "BOOL" }, { name = "LD", type = "BOOL" }, { name = "PV", type = "INT" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "CV", type = "INT" } } }, + IEC_CTUD = { inputs = { { name = "CU", type = "BOOL" }, { name = "CD", type = "BOOL" }, { name = "R", type = "BOOL" }, { name = "LD", type = "BOOL" }, { name = "PV", type = "INT" } }, outputs = { { name = "QU", type = "BOOL" }, { name = "QD", type = "BOOL" }, { name = "CV", type = "INT" } } }, + ADD = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + SUB = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + MUL = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + DIV = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + MIN = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + MAX = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + LIMIT = { inputs = { { name = "MN" }, { name = "IN" }, { name = "MX" } }, outputs = { { name = "OUT" } } }, + MOVE = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + CONVERT = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + ROUND = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + TRUNC = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + CEIL = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + FLOOR = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + SQR = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + SQRT = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + SIN = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + COS = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + TAN = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + ASIN = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + ACOS = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + ATAN = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + LEN = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + CONCAT = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + LEFT = { inputs = { { name = "IN" }, { name = "L" } }, outputs = { { name = "OUT" } } }, + RIGHT = { inputs = { { name = "IN" }, { name = "L" } }, outputs = { { name = "OUT" } } }, + MID = { inputs = { { name = "IN" }, { name = "L" }, { name = "P" } }, outputs = { { name = "OUT" } } }, + AND = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + OR = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + XOR = { inputs = { { name = "IN1" }, { name = "IN2" } }, outputs = { { name = "OUT" } } }, + SHL = { inputs = { { name = "IN" }, { name = "N" } }, outputs = { { name = "OUT" } } }, + SHR = { inputs = { { name = "IN" }, { name = "N" } }, outputs = { { name = "OUT" } } }, + ROL = { inputs = { { name = "IN" }, { name = "N" } }, outputs = { { name = "OUT" } } }, + ROR = { inputs = { { name = "IN" }, { name = "N" } }, outputs = { { name = "OUT" } } }, + BCD_I = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, + I_BCD = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } }, +} + +function M.get_parameters(name) + return M.PARAMETERS[name:upper()] +end + +function M.is_known_function_block(name) + return M.FUNCTION_BLOCKS[name:upper()] or false +end + +function M.is_known_function(name) + return M.FUNCTIONS[name:upper()] or false +end + +return M diff --git a/lua/scl/init.lua b/lua/scl/init.lua index c64e446..a032d4d 100644 --- a/lua/scl/init.lua +++ b/lua/scl/init.lua @@ -6,23 +6,40 @@ local M = {} function M.setup(opts) opts = opts or {} + -- Add plugin to runtime path + vim.opt.runtimepath:prepend(vim.fn.stdpath("data") .. "/lazy/scl_lsp") + M.create_commands() - -- Treesitter parser config - local has_ts, ts = pcall(require, "nvim-treesitter.parsers") - if has_ts and ts.get_parser_configs then - local ok, parser_config = pcall(ts.get_parser_configs) - if ok and parser_config then - parser_config.scl = { - install_info = { - files = { "src/parser.c" }, - generate_requires_npm = false, - requires_generate_from_grammar = false, - }, - filetype = "scl", - } - end + -- Treesitter setup for Neovim 0.11+ + local function setup_treesitter() + local lang = vim.treesitter.language + local parser_path = "/home/lazar/dev/scl_lsp/src/parser.so" + + -- Register the SCL language + lang.add("scl", { + path = parser_path, + filetype = "scl", + }) end + + pcall(setup_treesitter) + + -- Install queries + local queries_src = "/home/lazar/dev/scl_lsp/queries/highlights.scm" + local queries_dest = vim.fn.stdpath("data") .. "/site/queries/scl/highlights.scm" + if vim.fn.filereadable(queries_src) == 1 then + vim.fn.mkdir(vim.fn.stdpath("data") .. "/site/queries/scl", "p") + vim.fn.writefile(vim.fn.readfile(queries_src), queries_dest) + end + + -- Enable treesitter manually for SCL files - bypass LazyVim + vim.api.nvim_create_autocmd("FileType", { + pattern = "scl", + callback = function(args) + vim.treesitter.start(args.buf, "scl") + end, + }) -- Register blink.cmp source if opts.cmp then diff --git a/lua/scl/multiline_params.lua b/lua/scl/multiline_params.lua index 5073f3a..81e984a 100644 --- a/lua/scl/multiline_params.lua +++ b/lua/scl/multiline_params.lua @@ -1,5 +1,19 @@ local M = {} +local function get_builtin_interface(var_type) + local builtin = require("scl.builtin_instructions") + local clean_type = var_type:gsub('"', ''):gsub("#", ""):upper() + local params_info = builtin.get_parameters(clean_type) + if params_info then + return { + inputs = params_info.inputs or {}, + outputs = params_info.outputs or {}, + inouts = {}, + } + end + return nil +end + local function get_fb_interface(var_type) local fb_parser = require("scl.fb_parser") local clean_type = var_type:gsub('"', '') @@ -49,44 +63,69 @@ function M.fill_multiline_params() local line_before_cursor = line:sub(1, col) local known_vars = get_known_variables(bufnr) + local builtin = require("scl.builtin_instructions") local fb_name = nil local fb_prefix = "" + local is_builtin_function = false local prefix, var_name = line_before_cursor:match("(#?)([%w_]+)%s*%(%s*$") - if var_name and known_vars[var_name] then - fb_prefix = prefix or "" - fb_name = var_name - end - - if not fb_name then - prefix, var_name = line_before_cursor:match("(#?)([%w_]+)%s*%([^)]-,%s*$") - if var_name and known_vars[var_name] then + if var_name then + if known_vars[var_name] then fb_prefix = prefix or "" fb_name = var_name + elseif builtin.is_known_function(var_name) then + fb_prefix = prefix or "" + fb_name = var_name + is_builtin_function = true end end if not fb_name then - vim.notify("No FB instance found before cursor", vim.log.levels.WARN) + prefix, var_name = line_before_cursor:match("(#?)([%w_]+)%s*%([^)]-,%s*$") + if var_name then + if known_vars[var_name] then + fb_prefix = prefix or "" + fb_name = var_name + elseif builtin.is_known_function(var_name) then + fb_prefix = prefix or "" + fb_name = var_name + is_builtin_function = true + end + end + end + + if not fb_name then + vim.notify("No FB instance or known function found before cursor", vim.log.levels.WARN) return end - local var_type = get_variable_type(fb_name, bufnr) - if not var_type then - vim.notify("Cannot find type for: " .. fb_name, vim.log.levels.WARN) - return + local fb_interface = nil + + if is_builtin_function then + fb_interface = get_builtin_interface(fb_name) + else + local var_type = get_variable_type(fb_name, bufnr) + if not var_type then + vim.notify("Cannot find type for: " .. fb_name, vim.log.levels.WARN) + return + end + + local clean_type = var_type:gsub('"', '') + + fb_interface = get_builtin_interface(clean_type) + + if not fb_interface then + if not is_fb_type(clean_type) then + vim.notify(clean_type .. " is not an FB/Function type", vim.log.levels.WARN) + return + end + fb_interface = get_fb_interface(clean_type) + end end - local clean_type = var_type:gsub('"', '') - if not is_fb_type(clean_type) then - vim.notify(clean_type .. " is not an FB/Function type", vim.log.levels.WARN) - return - end - - local fb_interface = get_fb_interface(clean_type) if not fb_interface then - vim.notify("Cannot get interface for: " .. clean_type, vim.log.levels.WARN) + vim.notify("Cannot get interface for: " .. (clean_type or fb_name), vim.log.levels.WARN) return end @@ -102,7 +141,7 @@ function M.fill_multiline_params() end if #params == 0 then - vim.notify("No parameters found for: " .. clean_type, vim.log.levels.WARN) + vim.notify("No parameters found for: " .. (clean_type or fb_name), vim.log.levels.WARN) return end diff --git a/queries/highlights.scm b/queries/highlights.scm index 375556c..717d26e 100644 --- a/queries/highlights.scm +++ b/queries/highlights.scm @@ -1,211 +1,76 @@ -; Comprehensive SCL highlight queries for Neovim +; SCL highlights -; ============================================ -; Comments -; ============================================ (line_comment) @comment (block_comment) @comment (c_style_comment) @comment -; ============================================ -; Literals -; ============================================ (number) @number (hex_number) @number (string) @string (time_value) @number -; ============================================ -; Constants -; ============================================ -( - (identifier) @constant.builtin - (#match? @constant.builtin "^(TRUE|FALSE|TRUE#|FALSE#)$") -) -( - (type) @type.builtin - (#match? @type.builtin "^(TRUE|FALSE)$") -) +(prefix) @punctuation.special -; ============================================ -; Identifiers -; ============================================ (identifier) @variable -(prefixed_identifier) @variable -; ============================================ -; Types - Basic -; ============================================ -( - (type) @type.builtin - (#match? @type.builtin "^(BOOL|Bool|INT|Int|DINT|DInt|SINT|SInt|USINT|USInt|UINT|UInt|WORD|Word|DWORD|DWord|BYTE|Byte|REAL|Real|LREAL|LReal|TIME|Time|TOD|Tod|DT|Dt|String|STRING|CHAR|Char|WSTRING|WString)$") -) +; FB instance calls - highlight instance name as type +(fb_call + instance: (prefixed_identifier + (identifier) @type)) +(fb_call + instance: (identifier) @type) -; Types - User defined (quoted strings like "UDT_Type") -(type - (string) @type -) - -; ============================================ -; Keywords - Block definitions -; ============================================ -"ORGANIZATION_BLOCK" @keyword -"FUNCTION_BLOCK" @keyword -"FUNCTION" @keyword -"END_ORGANIZATION_BLOCK" @keyword -"END_FUNCTION_BLOCK" @keyword -"END_FUNCTION" @keyword -"BEGIN" @keyword - -; ============================================ -; Keywords - Variable sections -; ============================================ -"VAR_INPUT" @keyword -"VAR_OUTPUT" @keyword -"VAR_IN_OUT" @keyword -"VAR" @keyword -"VAR_TEMP" @keyword -"VAR_CONSTANT" @keyword -"END_VAR" @keyword -"CONSTANT" @keyword -"RETAIN" @keyword -"NON_RETAIN" @keyword - -; ============================================ -; Keywords - Control flow -; ============================================ -"IF" @conditional -"THEN" @conditional -"ELSIF" @conditional -"ELSE" @conditional -"END_IF" @conditional -"CASE" @conditional -"OF" @conditional -"END_CASE" @conditional - -; ============================================ -; Keywords - Loops -; ============================================ -"FOR" @repeat -"TO" @repeat -"BY" @repeat -"DO" @repeat -"END_FOR" @repeat -"WHILE" @repeat -"END_WHILE" @repeat -"REPEAT" @repeat -"UNTIL" @repeat -"END_REPEAT" @repeat -"EXIT" @keyword -"CONTINUE" @keyword - -; ============================================ -; Keywords - Regions -; ============================================ -"REGION" @namespace -"END_REGION" @namespace - -; ============================================ -; Keywords - Other -; ============================================ -"TITLE" @keyword -"VERSION" @keyword -"ARRAY" @keyword -"OF" @keyword - -; ============================================ -; Fields -; ============================================ -(block - name: (string) @namespace) - -(block - name: (identifier) @namespace) - -(var_item +; FB parameter names (IN :=, PT :=, etc.) +(fb_parameter name: (identifier) @property) +; Function call names +(function_call + (identifier) @function) + +; Variable declarations (var_item - type: (type) @type) + name: (identifier) @variable) -; ============================================ -; Functions and function calls -; ============================================ -(function_call - (identifier) @function.call) +; Type references in declarations +(type + (identifier) @type) -(function_call - (string) @function.call) +(type_builtin) @type.builtin -(parameter - (identifier) @parameter) +(keyword) @keyword +(constant) @constant.builtin +(operator) @operator -; ============================================ -; Array access -; ============================================ -(array_access - "[" @punctuation.bracket - "]" @punctuation.bracket) +(assignment) @operator +(binary_expression) @operator +(unary_expression) @operator -; ============================================ -; Field access -; ============================================ (field_access - "." @punctuation.delimiter) - -; ============================================ -; Attribute lists -; ============================================ -(attribute_list - "{" @punctuation.bracket - "}" @punctuation.bracket) - -(attribute_list - "," @punctuation.delimiter) - -(attribute_list - ";" @punctuation.delimiter) - -(attribute + "." (identifier) @property) -; ============================================ -; Operators - Logical -; ============================================ -"AND" @operator -"OR" @operator -"XOR" @operator -"NOT" @operator +(array_access) @property -; ============================================ -; Operators - Comparison -; ============================================ -"=" @operator -"<>" @operator -"<" @operator -">" @operator -"<=" @operator -">=" @operator +(attribute_list) @attribute +(attribute) @attribute +(attribute_value) @string -; ============================================ -; Operators - Arithmetic -; ============================================ -"+" @operator -"-" @operator -"*" @operator -"/" @operator -"MOD" @operator -"**" @operator +(var_item) @property -; ============================================ -; Operators - Assignment -; ============================================ -":=" @operator -"=>" @operator +(function_call) @function.call +(fb_call) @function.call + +(if_statement) @conditional +(elsif_clause) @conditional +(else_clause) @conditional +(case_statement) @conditional +(case_item) @conditional + +(for_statement) @repeat +(while_statement) @repeat +(repeat_statement) @repeat -; ============================================ -; Punctuation -; ============================================ ";" @punctuation.delimiter ":" @punctuation.delimiter "," @punctuation.delimiter @@ -214,17 +79,17 @@ "[" @punctuation.bracket "]" @punctuation.bracket "." @punctuation.delimiter -".." @punctuation.delimiter -"#" @punctuation.special -; ============================================ -; Binary and unary expressions -; ============================================ -(binary_expression) @operator -(unary_expression) @operator +":=" @operator +"=>" @operator +"=" @operator +"<>" @operator +"<" @operator +">" @operator +"<=" @operator +">=" @operator -; ============================================ -; Range expressions -; ============================================ -(range - ".." @punctuation.delimiter) +"+" @operator +"-" @operator +"*" @operator +"/" @operator