Files
tia-lsp/src/parser.lua
T
lazar 553192e7a1 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
2026-02-20 23:29:26 +01:00

352 lines
10 KiB
Lua

-- 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