-- DB Parser for SCL -- Parses .db files to extract global data block definitions and member structures local M = {} -- Cache for parsed DBs: { [db_name] = db_definition } local db_cache = {} -- Inline DB definitions (from comments): { [db_name] = db_definition } local inline_dbs = {} -- DB Definition structure: -- { -- name = "DBName", -- title = "Data Block Title", -- version = "0.1", -- comment = "User comment", -- members = { -- { -- name = "memberName", -- type = "Bool" or '"OtherUdt"', -- attributes = "{ S7_SetPoint := 'True' }", -- comment = "Member comment", -- is_udt = false -- true if type is a quoted UDT reference -- } -- } -- } -- Register an inline DB definition (from comments or other sources) function M.register_inline_db(db) if db and db.name then inline_dbs[db.name] = db db_cache[db.name] = db end end function M.parse_inline_db_comment(comment_lines) local db_name = nil local members = {} for _, line in ipairs(comment_lines) do local name = line:match("@db%s+(%w+)") if name then db_name = name break end end if not db_name then return nil end for _, line in ipairs(comment_lines) do local name, type_str = line:match("^%s*([%w_]+)%s*:%s*([^;]+)") if name and type_str then type_str = type_str:gsub("%s*:=.*$", ""):gsub("%s*$", "") local is_udt = type_str:match('^".+"$') ~= nil local clean_type = type_str:gsub('"', '') table.insert(members, { name = name, type = clean_type, raw_type = type_str, attributes = {}, comment = "", is_udt = is_udt }) end end if #members == 0 then return nil end return { name = db_name, title = "Inline DB", version = "0.1", comment = "", members = members } end function M.clear_cache() for k in pairs(db_cache) do db_cache[k] = nil end for k in pairs(inline_dbs) do inline_dbs[k] = nil end end function M.get_db(db_name) return db_cache[db_name] end function M.get_all_db_names() local names = {} for name, _ in pairs(db_cache) do table.insert(names, name) end return names end function M.get_cache_count() local count = 0 for _ in pairs(db_cache) do count = count + 1 end return count end function M.is_db_type(type_name) if not type_name then return false end local clean_name = type_name:gsub('"', '') return db_cache[clean_name] ~= nil or inline_dbs[clean_name] ~= nil end local function parse_attributes(attr_str) if not attr_str or attr_str == "" then return {} end local attrs = {} for key, value in attr_str:gmatch("([%w_]+)%s*:=%s*'([^']+)'") do attrs[key] = value end for key, value in attr_str:gmatch("([%w_]+)%s*:=%s*(%w+)") do attrs[key] = value end return attrs end function M.parse_db_content(content, filename) local db = { filename = filename, members = {} } local db_name = content:match('DATA_BLOCK%s+"([^"]+)"') if not db_name then return nil, "No DATA_BLOCK definition found" end db.name = db_name db.title = content:match('TITLE%s*=%s*([^\n]+)') or "" db.title = db.title:gsub("^%s*", ""):gsub("%s*$", "") db.version = content:match('VERSION%s*:%s*([0-9.]+)') or "0.1" local comment_lines = {} local in_header = true for line in content:gmatch("[^\r\n]+") do if in_header then local comment = line:match("^%s*//%s*(.+)$") if comment then table.insert(comment_lines, comment) elseif line:match("^%s*VAR") then in_header = false end end end db.comment = table.concat(comment_lines, " ") local all_var_content = {} local var_section_pattern = "VAR%s*(.-)%s*END_VAR" for var_section in content:gmatch(var_section_pattern) do table.insert(all_var_content, var_section) end for _, var_content in ipairs(all_var_content) do for line in var_content:gmatch("[^\r\n]+") do line = line:gsub("^%s*", ""):gsub("%s*$", "") if line ~= "" and not line:match("^//") then local comment = line:match("//%s*(.+)$") or "" line = line:gsub("//.*$", ""):gsub("%s*$", "") local name, attrs_str, type_str = line:match('^([%w_]+)%s*(%b{})%s*:%s*([^;]+)') if not name then name, type_str = line:match('^([%w_]+)%s*:%s*([^;]+)') end if name and type_str then type_str = type_str:gsub("%s*:=.*$", ""):gsub("%s*$", "") local is_udt = type_str:match('^".+"$') ~= nil local clean_type = type_str:gsub('"', '') local member = { name = name, type = clean_type, raw_type = type_str, attributes = attrs_str and parse_attributes(attrs_str) or {}, comment = comment, is_udt = is_udt } table.insert(db.members, member) end end end end return db end function M.parse_db_file(filepath) local file = io.open(filepath, "r") if not file then return nil, "Cannot open file: " .. filepath end local content = file:read("*all") file:close() local db, err = M.parse_db_content(content, filepath) if not db then return nil, err end db_cache[db.name] = db return db end function M.get_db_members(db_name, visited) visited = visited or {} if not db_name then return {} end db_name = db_name:gsub('"', '') if visited[db_name] then return {} end visited[db_name] = true local db = db_cache[db_name] if not db then return {} end local members = {} for _, member in ipairs(db.members) do table.insert(members, member) if member.is_udt and member.type then member.nested_members = M.get_db_members(member.type, visited) end end return members end function M.resolve_db_member_path(db_name, path) if not db_name or not path then return nil, path or "" end local parts = {} for part in path:gmatch("[^.]+") do table.insert(parts, part) end local current_db_name = db_name:gsub('"', '') local current_member = nil for i, part in ipairs(parts) do local db = db_cache[current_db_name] if not db then return nil, table.concat(parts, ".", i) end local found = false for _, member in ipairs(db.members) do if member.name == part then current_member = member if member.is_udt then current_db_name = member.type else if i < #parts then return nil, table.concat(parts, ".", i + 1) end end found = true break end end if not found then return nil, table.concat(parts, ".", i) end end return current_member, "" end function M.get_db_member_completions(db_name, prefix) local members = M.get_db_members(db_name) local items = {} for _, member in ipairs(members) do local label = prefix and (prefix .. "." .. member.name) or member.name local item = { label = label, insertText = member.name, kind = member.is_udt and vim.lsp.protocol.CompletionItemKind.Struct or vim.lsp.protocol.CompletionItemKind.Field, detail = member.raw_type .. (member.comment ~= "" and " - " .. member.comment or ""), documentation = member.comment, } table.insert(items, item) end return items end function M.get_db_type_completions(params) local items = {} for name, db in pairs(db_cache) do local quoted_name = '"' .. name .. '"' local item = { label = quoted_name, kind = vim.lsp.protocol.CompletionItemKind.Struct, detail = db.title ~= "" and db.title or "Global DB", documentation = db.comment ~= "" and db.comment or nil, } if params then local line = params.context.cursor_before_line local col = params.context.cursor.col local start_col = col for i = col, 1, -1 do local char = line:sub(i, i) if char == '"' then start_col = i - 1 break elseif char == ":" then start_col = col - 1 break end end item.textEdit = { newText = quoted_name, range = { start = { line = params.context.cursor.line, character = start_col }, ["end"] = { line = params.context.cursor.line, character = col }, }, } else item.insertText = quoted_name end table.insert(items, item) end return items end function M.scan_buffer_for_inline_dbs(bufnr) bufnr = bufnr or vim.api.nvim_get_current_buf() local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local current_comment_block = {} local in_scl_file = vim.bo[bufnr].filetype == "scl" if not in_scl_file then return end for _, line in ipairs(lines) do if line:match("^%s*//") then local content = line:gsub("^%s*//%s*", "") table.insert(current_comment_block, content) else if #current_comment_block > 0 then local db = M.parse_inline_db_comment(current_comment_block) if db then M.register_inline_db(db) end current_comment_block = {} end end end if #current_comment_block > 0 then local db = M.parse_inline_db_comment(current_comment_block) if db then M.register_inline_db(db) end end end function M.debug_print_cache() print("=== DB Cache Contents ===") print("Total DBs: " .. M.get_cache_count()) for name, db in pairs(db_cache) do print(string.format("DB: %s (v%s)", name, db.version)) print(string.format(" Title: %s", db.title)) print(string.format(" File: %s", db.filename or "unknown")) print(string.format(" Members: %d", #db.members)) for _, member in ipairs(db.members) do print(string.format(" - %s: %s%s", member.name, member.raw_type, member.is_udt and " (UDT)" or "")) end print("") end end return M