-- SCL Parser utilities for the LSP server local M = {} function M.extract_variables(content) local variables = {} local variable_positions = {} local lines = {} for line in content:gmatch("([^\n]*)\n") do table.insert(lines, line) end local in_var_section = false local in_struct_section = false local var_type = "unknown" local current_block = nil local function process_line(line, line_num) local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "") -- Detect block start (with or without quotes) local fb_name = trimmed:match('FUNCTION_BLOCK%s+"([^"]+)"') or trimmed:match("^FUNCTION_BLOCK%s+([%w_]+)") if fb_name then current_block = { name = fb_name, kind = "function_block" } in_var_section = false in_struct_section = false return end local ob_name = trimmed:match('ORGANIZATION_BLOCK%s+"([^"]+)"') or trimmed:match("^ORGANIZATION_BLOCK%s+([%w_]+)") if ob_name then current_block = { name = ob_name, kind = "organization_block" } in_var_section = false in_struct_section = false return end local fn_name = trimmed:match('FUNCTION%s+"([^"]+)"') or trimmed:match("^FUNCTION%s+([%w_]+)") if fn_name then current_block = { name = fn_name, kind = "function" } in_var_section = false in_struct_section = false return end if trimmed:match("^END_FUNCTION_BLOCK") or trimmed:match("^END_ORGANIZATION_BLOCK") or trimmed:match("^END_FUNCTION") then current_block = nil in_var_section = false in_struct_section = false return end -- Handle TYPE sections (for .udt files with STRUCT) if trimmed:match("^TYPE") and (trimmed:match("^TYPE%s") or trimmed:match('^TYPE"') or trimmed == "TYPE") then in_struct_section = true var_type = "STRUCT_FIELD" return end if trimmed:match("^END_TYPE") then in_struct_section = false return end if trimmed:match("^VAR[_A-Z]*%s*$") or trimmed:match("^VAR_INPUT%s*$") or trimmed:match("^VAR_OUTPUT%s*$") or trimmed:match("^VAR_IN_OUT%s*$") or trimmed:match("^VAR_TEMP%s*$") or trimmed:match("^VAR[_ ]*CONSTANT%s*$") or trimmed:match("^VAR[_ ]*RETAIN%s*$") or trimmed:match("^VAR[_ ]*NON_RETAIN%s*$") then in_var_section = true in_struct_section = false if trimmed:match("INPUT") then var_type = "VAR_INPUT" elseif trimmed:match("OUTPUT") then var_type = "VAR_OUTPUT" elseif trimmed:match("IN_OUT") then var_type = "VAR_IN_OUT" elseif trimmed:match("TEMP") then var_type = "VAR_TEMP" elseif trimmed:match("CONSTANT") then var_type = "VAR_CONSTANT" elseif trimmed:match("RETAIN") then var_type = "VAR_RETAIN" elseif trimmed:match("NON_RETAIN") then var_type = "VAR_NON_RETAIN" else var_type = "VAR" end return end if trimmed:match("^END_VAR%s*$") then in_var_section = false return end if trimmed:match("^STRUCT%s*$") then in_struct_section = true var_type = "STRUCT_FIELD" return end if trimmed:match("^END_STRUCT%s*$") then in_struct_section = false return end if not in_var_section and not in_struct_section then return end if trimmed:match("^%(%*") or trimmed:match("^%s*//") then return end local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:") if not var_name then var_name = trimmed:match("^([%w_]+)%s*%b{}%s*,") or trimmed:match("^([%w_]+)%s*,") end if var_name and not variables[var_name] then local col = line:find(var_name, 1, true) if col then local type_start = nil local brace_depth = 0 for i = 1, #line do local c = line:sub(i, i) if c == "{" then brace_depth = brace_depth + 1 elseif c == "}" then brace_depth = brace_depth - 1 elseif c == ":" and brace_depth == 0 then type_start = i break end end local var_data_type = "unknown" local var_comment = line:match("//%s*(.+)$") if not var_comment then var_comment = line:match("%(%*(.-)%*%)") end if type_start then local type_part = line:sub(type_start + 1) type_part = type_part:gsub("%s*:=.*$", "") type_part = type_part:gsub("%s*;.*$", "") type_part = type_part:gsub("^%s+", "") type_part = type_part:gsub("%s+$", "") var_data_type = type_part end variables[var_name] = { type = var_type, data_type = var_data_type, line = line_num, character = col - 1, comment = var_comment, } variable_positions[var_name] = { line = line_num, character = col - 1, } end end end for i, line in ipairs(lines) do process_line(line, i - 1) end return variables, variable_positions end function M.extract_types(content) local types = {} local lines = {} for line in content:gmatch("([^\n]*)\n") do table.insert(lines, line) end local in_type_section = false local current_type = nil local brace_depth = 0 local in_struct = false local pending_type_name = nil local pending_comment = nil local function extract_type_name(after_colon) if not after_colon then return nil end local quoted = after_colon:match('^%s*"([^"]+)"') if quoted then return quoted end return after_colon:match("^%s*(%w+)") end for i, line in ipairs(lines) do local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "") local is_comment_only = trimmed:match("^//") if is_comment_only then pending_comment = line:match("//%s*(.+)$") end local is_type_line = trimmed:match("^TYPE") and (trimmed:match("^TYPE%s") or trimmed:match('^TYPE"') or trimmed == "TYPE") if is_type_line then in_type_section = true in_struct = false brace_depth = 0 pending_type_name = trimmed:match('^TYPE%s+"([^"]+)') or trimmed:match("^TYPE%s+(%w+)") if not pending_comment then pending_comment = trimmed:match("//%s*(.+)$") end elseif in_type_section and trimmed:match("^END_TYPE") then in_type_section = false in_struct = false current_type = nil pending_type_name = nil elseif in_type_section then local type_name_with_struct = trimmed:match("^(%w+)%s+.-:%s+STRUCT") if type_name_with_struct then current_type = { name = type_name_with_struct, kind = "struct", fields = {}, start_line = i - 1, comment = pending_comment, } types[type_name_with_struct] = current_type in_struct = true pending_type_name = nil pending_comment = nil elseif trimmed:match("^STRUCT%s*$") then if pending_type_name then current_type = { name = pending_type_name, kind = "struct", fields = {}, start_line = i - 1, comment = pending_comment, } types[pending_type_name] = current_type pending_type_name = nil pending_comment = nil end in_struct = true elseif trimmed:match("^END_STRUCT%s*$") then in_struct = false elseif in_struct and current_type and current_type.fields then local field_name = line:match("^%s*(%w+)%s*:%s*") if field_name then local after_colon = line:match(":%s*(.+)") local field_type = after_colon and extract_type_name(after_colon) local field_comment = line:match("//%s*(.+)$") if not field_comment then field_comment = line:match("%(%*(.-)%*%)") end table.insert(current_type.fields, { name = field_name, type = field_type or "unknown", comment = field_comment, }) end elseif not in_struct and not pending_type_name then local array_match = trimmed:match("^(%w+)%s+:%s+ARRAY") if array_match then local of_type = trimmed:match("OF%s+(%w+)") current_type = { name = array_match, kind = "array", element_type = of_type, start_line = i - 1, } types[array_match] = current_type else local alias_match = trimmed:match("^(%w+)%s+:%s+(%w+)") if alias_match then local base_type = extract_type_name(trimmed:match(":%s+(.+)$")) current_type = { name = alias_match, kind = "alias", base_type = base_type, start_line = i - 1, } types[alias_match] = current_type end end end end end return types end function M.extract_functions(content) local functions = {} local lines = {} for line in content:gmatch("([^\n]*)\n") do table.insert(lines, line) end local function find_block_end(start_line, block_name, end_markers) local depth = 1 for i = start_line + 1, #lines do local line = lines[i] if line:match("^%s*" .. block_name) then depth = depth + 1 end for _, marker in ipairs(end_markers) do if line:match("^%s*" .. marker) then depth = depth - 1 if depth == 0 then return i end end end end return #lines end for i, line in ipairs(lines) do local func_name = line:match('FUNCTION%s+"([^"]+)"') or line:match("^FUNCTION%s+([%w_]+)") if func_name then local end_line = find_block_end(i, "END_FUNCTION", { "END_FUNCTION" }) table.insert(functions, { name = func_name, kind = "function", start = i - 1, final = end_line - 1, }) end local fb_name = line:match('FUNCTION_BLOCK%s+"([^"]+)"') or line:match("^FUNCTION_BLOCK%s+([%w_]+)") if fb_name then local end_line = find_block_end(i, "END_FUNCTION_BLOCK", { "END_FUNCTION_BLOCK" }) table.insert(functions, { name = fb_name, kind = "function_block", start = i - 1, final = end_line - 1, }) end local ob_name = line:match('ORGANIZATION_BLOCK%s+"([^"]+)"') or line:match("^ORGANIZATION_BLOCK%s+([%w_]+)") if ob_name then local end_line = find_block_end(i, "END_ORGANIZATION_BLOCK", { "END_ORGANIZATION_BLOCK" }) table.insert(functions, { name = ob_name, kind = "organization_block", start = i - 1, final = end_line - 1, }) end end return functions end return M