diff --git a/AGENTS.md b/AGENTS.md index 8e6c0d9..94cd132 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,8 @@ scl_lsp/ │ ├── diagnostics.lua # Linter │ ├── formatter.lua # Document formatter │ ├── treesitter.lua # Tree-sitter integration -│ ├── plc_json.lua # External UDT loading +│ ├── plc_json.lua # External UDT/DB loading +│ ├── builtin_instructions.lua # TIA Portal built-in types/FBs/functions │ └── json.lua # JSON encoder/decoder ├── lua/scl/ # Neovim plugin (uses require) │ ├── init.lua # Main plugin setup @@ -155,12 +156,30 @@ if ok and module then end ``` +## Diagnostics + +The LSP validates data types and provides warnings for unknown types. It recognizes: +- **Elementary data types** - BOOL, BYTE, WORD, DWORD, LWORD, CHAR, WCHAR, STRING, WSTRING, INT, DINT, LINT, USINT, SINT, UINT, UDINT, ULINT, REAL, LREAL, TIME, DATE, TOD, TIME_OF_DAY, DATE_AND_TIME, DT, S5TIME, LTIME, DTL +- **Timer/counter types** - IEC_TIMER, TON_TIME, TOF_TIME, TONR_TIME, TP_TIME, CTU, CTD, CTUD +- **TIA Portal built-in instructions** - Functions and function blocks +- **Workspace types** - UDTs from `.udt` files and data blocks from `.db` files + ## Completion Triggers - `#` - Local variables (after BEGIN) - `.` - Member access (UDT/DB) - `"` - Global DB names - `(` - FB/Function parameters - ` ` (space) - General completion +- `:` - Type declaration (in VAR section) + +### Completion Types +The LSP provides auto-completion for: +- **Local variables** - Variables declared in VAR sections +- **UDT types** - User-defined types from `.udt` files +- **Global DBs** - Data blocks from `.db` files +- **Elementary types** - BOOL, INT, DINT, REAL, TIME, etc. +- **TIA Portal built-in FBs** - TON, TOF, TP, CTU, CTD, CTUD, R_TRIG, etc. +- **TIA Portal built-in functions** - ADD, SUB, MUL, DIV, SIN, COS, SQRT, etc. ## Formatter Options diff --git a/src/builtin_instructions.lua b/src/builtin_instructions.lua new file mode 100644 index 0000000..17c90c1 --- /dev/null +++ b/src/builtin_instructions.lua @@ -0,0 +1,249 @@ +local M = {} + +M.FUNCTIONS = { + ADD = true, + SUB = true, + MUL = true, + DIV = true, + MOD = true, + ABS = true, + NEG = true, + SQR = true, + SQRT = true, + EXP = true, + LN = true, + LOG = true, + SIN = true, + COS = true, + TAN = true, + ASIN = true, + ACOS = true, + ATAN = true, + MIN = true, + MAX = true, + LIMIT = true, + CALCULATE = true, + CONVERT = true, + ROUND = true, + TRUNC = true, + CEIL = true, + FLOOR = true, + SCALE_X = true, + NORM_X = true, + LEN = true, + CONCAT = true, + LEFT = true, + RIGHT = true, + MID = true, + INSERT = true, + DELETE = true, + REPLACE = true, + FIND = true, + AND = true, + OR = true, + XOR = true, + NOT = true, + SHL = true, + SHR = true, + ROL = true, + ROR = true, + DECO = true, + ENCO = true, + BCD_I = true, + I_BCD = true, + DI_BCD = true, + DI_R = true, + I_DI = true, + R_DI = true, + R_DB = true, + MOVE = true, + FILL = true, + SWAP = true, + PEEK = true, + POKE = true, + GET_ERROR = true, + GET_ERR_ID = true, + RESET_ERR = true, + T_CONV = true, + T_ADD = true, + T_SUB = true, + T_DIFF = true, + T_COMBINE = true, + DT_DATE = true, + DT_TOD = true, + DATE_TO_DTL = true, + DTL_TO_DATE = true, + TOD_TO_DTL = true, + DTL_TO_TOD = true, + TimeToTicks = true, + TicksToTime = true, + GETHIGHNUM = true, + GETLOWNUM = true, + SAMPLE = true, + SAMPLE_TRIG = true, + WRIT_HIST = true, + READ_HIST = true, + COMPRESS = true, + READFILE = true, + WRITEFILE = true, + READ_DB = true, + WRITE_DB = true, + RDREC = true, + WRREC = true, + RALRM = true, + GET_DIAG = true, + DPRINT = true, + CTRL_PWM = true, + CTRL_PTO = true, + STATUS_PTO = true, + HOME = true, + MOVE_JOG = true, + MOVE_VEL = true, + MOVE_AXIS = true, + POS_PULSE = true, + POS_MC = true, + SET_CAM = true, + CAM_TB = true, + CAM_TE = true, + CAM_DOWN = true, + MC_CamIn = true, + MC_CamOut = true, + MC_Superimpose = true, + MC_GearIn = true, + MC_GearOut = true, + MC_Phasing = true, + MC_CombineAxes = true, + MC_Interpolate = true, + MC_MoveLinear = true, + MC_MoveCircular = true, + MC_MoveDirect = true, + MC_Home = true, + MC_Halt = true, + MC_Stop = true, + MC_Power = true, + MC_Reset = true, + MC_ReadParam = true, + MC_WriteParam = true, + MC_ReadAxisError = true, + MC_TouchProbe = true, + MC_AbortTrigger = true, + MC_ExtSetPointGen = true, + MC_Generator = true, + MC_BRIDGE = true, + MC_BRIDGE_OFF = true, + ENABLE = true, + DISABLE = true, + DISPLAY = true, + LOG_EVENT = true, + ALARM = true, + ALARM_DQ = true, + GET_ALARM = true, + GETINST = true, + GETALARMST = true, + CHANGEOVER = true, + REPLY_VALUE = true, + NOTIFY = true, + NOTIFY_WITH_ACK = true, + ASGEN = true, + HMI_R_DISCONNECT = true, + HMI_R_CONNECT = true, + HMI_R_SEND = true, + HMI_R_RECEIVE = true, + HMI_R_GET_STATE = true, + MODBUSPN = true, + MODBUSCP = true, + TSEND = true, + TRCV = true, + TCON = true, + TDISCON = true, + TSEND_C = true, + TRCV_C = true, + TUSEND = true, + TURCV = true, + TCONNECT = true, + TDISCONNECT = true, + GETHNSON = true, + DELHNSON = true, + DNS_CLIENT = true, + DHCP_CLIENT = true, + DCP_CLIENT = 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.DATA_TYPES = { + BOOL = true, + BYTE = true, + WORD = true, + DWORD = true, + LWORD = true, + CHAR = true, + WCHAR = true, + STRING = true, + WSTRING = true, + INT = true, + DINT = true, + LINT = true, + USINT = true, + SINT = true, + UINT = true, + UDINT = true, + ULINT = true, + REAL = true, + LREAL = true, + TIME = true, + DATE = true, + TOD = true, + TIME_OF_DAY = true, + DATE_AND_TIME = true, + DT = true, + S5TIME = true, + LTIME = true, + DTL = true, +} + +function M.is_known_function(name) + return M.FUNCTIONS[name:upper()] or false +end + +function M.is_known_function_block(name) + return M.FUNCTION_BLOCKS[name:upper()] or false +end + +function M.is_known_data_type(name) + return M.DATA_TYPES[name:upper()] or false +end + +function M.is_known_instruction(name) + return M.is_known_function(name) or M.is_known_function_block(name) or false +end + +function M.is_known_type_or_instruction(name) + return M.is_known_data_type(name) or M.is_known_instruction(name) or false +end + +return M diff --git a/src/diagnostics.lua b/src/diagnostics.lua index fb77765..f399a76 100644 --- a/src/diagnostics.lua +++ b/src/diagnostics.lua @@ -16,6 +16,7 @@ end package.path = package.path .. ";" .. script_path .. "/?.lua" local parser = dofile(script_path .. "/parser.lua") +local builtin = dofile(script_path .. "/builtin_instructions.lua") local DiagnosticSeverity = { Error = 1, @@ -156,9 +157,16 @@ function M.validate_diagnostics(content, workspace_types, root_dir) upper_type ~= "WSTRING" and upper_type ~= "TIME" and upper_type ~= "DATE" and upper_type ~= "TIME_OF_DAY" and upper_type ~= "DATE_AND_TIME" and upper_type ~= "S5TIME" and upper_type ~= "LTIME" and + upper_type ~= "USINT" and upper_type ~= "SINT" and upper_type ~= "UINT" and + upper_type ~= "UDINT" and upper_type ~= "LINT" and upper_type ~= "ULINT" and + upper_type ~= "TOD" and upper_type ~= "DTL" and upper_type ~= "IEC_TIMER" and upper_type ~= "TON_TIME" and upper_type ~= "TOF_TIME" and upper_type ~= "TONR_TIME" and - upper_type ~= "TP_TIME" then + upper_type ~= "TP_TIME" and upper_type ~= "IEC_TON" and + upper_type ~= "IEC_TOF" and upper_type ~= "IEC_TP" and + upper_type ~= "IEC_COUNTER" and upper_type ~= "CTU" and + upper_type ~= "CTD" and upper_type ~= "CTUD" and + not builtin.is_known_type_or_instruction(upper_type) then table.insert(diagnostics, { range = range_to_lsp(line_num, 0, line_num, #line), severity = DiagnosticSeverity.Warning, diff --git a/src/main.lua b/src/main.lua index 886f6f5..b76b7fa 100644 --- a/src/main.lua +++ b/src/main.lua @@ -26,6 +26,7 @@ local treesitter = dofile(script_path .. "/treesitter.lua") local plc_json = dofile(script_path .. "/plc_json.lua") local diagnostics = dofile(script_path .. "/diagnostics.lua") local formatter = dofile(script_path .. "/formatter.lua") +local builtin = dofile(script_path .. "/builtin_instructions.lua") local documents = {} @@ -57,7 +58,7 @@ local semanticTokenTypes = { local capabilities = { hoverProvider = true, completionProvider = { - triggerCharacters = { ".", "#", "(" }, + triggerCharacters = { ".", "#", "(", " ", ":", ";", "\n" }, resolveProvider = false, }, definitionProvider = true, @@ -385,6 +386,27 @@ function handlers.textDocument_completion(params) local line = lines[line_idx] local col = params.position.character local trigger = col > 0 and line:sub(col, col) or "" + local line_before = col > 1 and line:sub(1, col - 1) or "" + + -- Get word prefix being typed (for filtering) + local prefix_start = col - 1 + while prefix_start > 0 and line:sub(prefix_start, prefix_start):match("[%w_]") do + prefix_start = prefix_start - 1 + end + local word_prefix = line:sub(prefix_start + 1, col - 1) + + -- Check if we're in VAR section + local in_var_section = false + for i = line_idx - 1, 1, -1 do + local prev_line = lines[i]:gsub("^%s+", ""):gsub("%s+$", "") + if prev_line:match("^END_VAR") or prev_line:match("^VAR_TEMP") or prev_line:match("^VAR_IN_OUT") or prev_line:match("^VAR_OUTPUT") or prev_line:match("^VAR_INPUT") then + break + end + if prev_line:match("^VAR%s*$") or prev_line:match("^VAR$") then + in_var_section = true + break + end + end if trigger == "#" then if doc.variables then @@ -450,6 +472,53 @@ function handlers.textDocument_completion(params) }) end end + else + -- Check context before cursor + local is_var_decl = line_before:match(":%s*$") + + -- Helper to add item with prefix filtering + local function add_item(name, kind_val, detail_text) + if word_prefix == "" or name:upper():find("^" .. word_prefix:upper()) then + table.insert(items, { + label = name, + kind = kind_val, + detail = detail_text, + insertText = name, + insertTextFormat = 1, + }) + end + end + + if in_var_section or is_var_decl then + -- In VAR section: show data types and function blocks + for type_name, _ in pairs(builtin.DATA_TYPES) do + add_item(type_name, 7, "Data type") + end + + for fb_name, _ in pairs(builtin.FUNCTION_BLOCKS) do + add_item(fb_name, 6, "Function block") + end + + -- Also add workspace types if available + if doc.types then + for type_name, type_info in pairs(doc.types) do + add_item(type_name, 7, type_info.kind or "Type") + end + end + else + -- General context: show all + for type_name, _ in pairs(builtin.DATA_TYPES) do + add_item(type_name, 7, "Data type") + end + + for fb_name, _ in pairs(builtin.FUNCTION_BLOCKS) do + add_item(fb_name, 6, "Function block") + end + + for func_name, _ in pairs(builtin.FUNCTIONS) do + add_item(func_name, 3, "Function") + end + end end end diff --git a/src/node-types.json b/src/node-types.json index 22af526..1c13d24 100644 --- a/src/node-types.json +++ b/src/node-types.json @@ -1524,10 +1524,18 @@ "type": "tod", "named": false }, + { + "type": "TOD", + "named": false + }, { "type": "uint", "named": false }, + { + "type": "UInt", + "named": false + }, { "type": "usint", "named": false @@ -1552,4 +1560,4 @@ "type": "}", "named": false } -] \ No newline at end of file +] diff --git a/src/plc_json.lua b/src/plc_json.lua index e4833db..8a504c0 100644 --- a/src/plc_json.lua +++ b/src/plc_json.lua @@ -85,24 +85,33 @@ local function parse_scl_type_file(content) local current_type_name = nil local pending_type_name = nil + content = content:gsub("^([\239\187\191]+)", "") + local lines = {} for line in content:gmatch("[^\n]+") do table.insert(lines, line) end for i, line in ipairs(lines) do local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "") - if trimmed:match("^TYPE%s*:?%s*$") then + if trimmed:match("^TYPE%s") or trimmed:match('^TYPE"') or trimmed == "TYPE" then in_type = true in_struct = false current_type_name = nil pending_type_name = nil + local type_name = trimmed:match('TYPE%s+"([^"]+)"') or trimmed:match("^TYPE%s+(%w+)") + if type_name then + pending_type_name = type_name + end + in_struct = false + current_type_name = nil + pending_type_name = nil elseif trimmed:match("^END_TYPE") then in_type = false in_struct = false current_type_name = nil pending_type_name = nil elseif in_type then - if trimmed:match("^END_STRUCT") then + if trimmed:match("^END_STRUCT") or trimmed:match("^END_STRUCT;") then in_struct = false current_type_name = nil pending_type_name = nil @@ -144,6 +153,48 @@ local function parse_scl_type_file(content) return types end +local function parse_db_file(content) + local types = {} + local in_var = false + local current_type_name = nil + + content = content:gsub("^([\239\187\191]+)", "") + + local lines = {} + for line in content:gmatch("[^\n]+") do table.insert(lines, line) end + + for i, line in ipairs(lines) do + local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "") + + if trimmed:match('^DATA_BLOCK%s+"([^"]+)"') then + current_type_name = trimmed:match('^DATA_BLOCK%s+"([^"]+)"') + types[current_type_name] = { + name = current_type_name, + kind = "struct", + fields = {}, + source = "db_file", + } + elseif trimmed:match("^END_VAR") then + in_var = false + elseif trimmed:match("^VAR%s*$") or trimmed:match("^VAR ") then + in_var = true + elseif in_var and current_type_name and types[current_type_name] and trimmed ~= "" and not trimmed:match("^//") then + local field_name = line:match("^%s*(%w+)%s*:") + if field_name then + local field_type = line:match(":%s*([%w_]+)") + if field_type then + table.insert(types[current_type_name].fields, { + name = field_name, + type = field_type, + }) + end + end + end + end + + return types +end + function M.load_types_from_workspace(root_dir) local types = {} @@ -213,6 +264,21 @@ function M.load_types_from_workspace(root_dir) end end + -- Also scan for .db files + local db_files = scan_files_recursive(root_dir, "*.db") + for _, filepath in ipairs(db_files) do + local content = run_command("cat " .. filepath:gsub(" ", "\\ ")) + if content then + local db_types = parse_db_file(content) + for name, typ in pairs(db_types) do + typ.source = "db_file" + if not types[name] then + types[name] = typ + end + end + end + end + return types end