Inintal commit

This commit is contained in:
2026-02-14 15:05:12 +01:00
commit 625297106e
21 changed files with 75105 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
-- SCL Diagnostics - Linter functionality for the LSP server
local M = {}
local parser = require("scl_lsp.parser")
local DiagnosticSeverity = {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4,
}
local function position_to_lsp(line, character)
return {
line = line,
character = character,
}
end
local function range_to_lsp(start_line, start_char, end_line, end_char)
return {
start = position_to_lsp(start_line, start_char),
end = position_to_lsp(end_line, end_char),
}
end
function M.validate_diagnostics(content, workspace_types, root_dir)
local diagnostics = {}
local lines = {}
for line in content:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
local variables, _ = parser.extract_variables(content)
local types = parser.extract_types(content)
local functions = parser.extract_functions(content)
local all_types = {}
for k, v in pairs(types) do all_types[k] = v end
for k, v in pairs(workspace_types or {}) do all_types[k] = v end
local in_var_section = false
local var_section_start = nil
for line_idx, line in ipairs(lines) do
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
local line_num = line_idx - 1
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
var_section_start = line_num
elseif trimmed:match("^END_VAR%s*$") then
in_var_section = false
var_section_start = nil
end
if not in_var_section then
local hash_vars = {}
for var in line:gmatch("#[%w_]+") do
local var_name = var:sub(2)
hash_vars[var_name] = true
end
for var_name, _ in pairs(hash_vars) do
if not variables[var_name] and not all_types[var_name] then
local var_start = line:find("#" .. var_name, 1, true)
if var_start then
table.insert(diagnostics, {
range = range_to_lsp(line_num, var_start - 1, line_num, var_start + #var_name - 1),
severity = DiagnosticSeverity.Error,
message = string.format("Undefined variable: %s", var_name),
code = "SCL001",
source = "scl_lsp",
})
end
end
end
end
if in_var_section and trimmed:match("^[%w_]+%s*:") then
local var_name = trimmed:match("^([%w_]+)%s*:")
local var_start = line:find(var_name, 1, true)
if var_name and not var_name:match("^%d") then
local has_hash = line:match("#" .. var_name)
if has_hash then
table.insert(diagnostics, {
range = range_to_lsp(line_num, 0, line_num, #line),
severity = DiagnosticSeverity.Error,
message = "Variable declarations should NOT have '#' prefix in VAR section",
code = "SCL002",
source = "scl_lsp",
})
end
local type_part = line:match(":%s*([^;]+)")
if type_part then
type_part = type_part:gsub("%s*:=.*$", "")
type_part = type_part:gsub("%s*$", "")
type_part = type_part:gsub("^%s+", "")
local base_type = type_part:match("^(%w+)")
if base_type and not all_types[base_type] and
base_type ~= "BOOL" and base_type ~= "INT" and base_type ~= "DINT" and
base_type ~= "REAL" and base_type ~= "LREAL" and base_type ~= "BYTE" and
base_type ~= "WORD" and base_type ~= "DWORD" and base_type ~= "LWORD" and
base_type ~= "CHAR" and base_type ~= "STRING" and base_type ~= "WCHAR" and
base_type ~= "WSTRING" and base_type ~= "TIME" and base_type ~= "DATE" and
base_type ~= "TIME_OF_DAY" and base_type ~= "DATE_AND_TIME" and
base_type ~= "S5TIME" and base_type ~= "LTIME" then
table.insert(diagnostics, {
range = range_to_lsp(line_num, 0, line_num, #line),
severity = DiagnosticSeverity.Warning,
message = string.format("Unknown data type: %s", base_type),
code = "SCL003",
source = "scl_lsp",
})
end
end
end
end
if trimmed:match("^%s*//") or trimmed:match("^%s*%(%*") then
-- Skip comments
else
for assignment in line:gmatch("#[%w_]+%s*:=") do
local var_name = assignment:match("#([%w_]+)")
if var_name and in_var_section then
local assign_start = line:find("#" .. var_name, 1, true)
table.insert(diagnostics, {
range = range_to_lsp(line_num, assign_start - 1, line_num, assign_start + #var_name),
severity = DiagnosticSeverity.Error,
message = "Cannot use '#' prefix for variables in VAR section",
code = "SCL004",
source = "scl_lsp",
})
end
end
end
end
for func_idx, func in ipairs(functions) do
local func_content = ""
for i = func.start + 1, func.final - 1 do
if lines[i + 1] then
func_content = func_content .. lines[i + 1] .. "\n"
end
end
local func_vars, _ = parser.extract_variables(func_content)
local all_vars = {}
for k, v in pairs(variables) do all_vars[k] = v end
for k, v in pairs(func_vars) do
all_vars[k] = v
end
end
return diagnostics
end
return M
+114
View File
@@ -0,0 +1,114 @@
-- SCL Formatter - Document formatting for the LSP server
local M = {}
local function position(line, character)
return { line = line, character = character }
end
local function range(start_line, start_char, end_line, end_char)
return {
start = position(start_line, start_char),
end = position(end_line, end_char),
}
end
function M.format_document(content, options)
options = options or {}
local indent_size = options.indentSize or 4
local indent_char = options.insertSpaces and " " or "\t"
local lines = {}
for line in content:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
if #lines == 0 or content:sub(-1) ~= "\n" then
table.insert(lines, "")
end
local formatted_lines = {}
local indent_level = 0
local function get_indent()
return string.rep(indent_char, indent_level * indent_size)
end
local function is_block_keyword(line)
local kw = line:match('^%s*(%w+)')
if not kw then return false end
local block_keywords = {
IF = true, ELSIF = true, ELSE = true,
FOR = true, WHILE = true, REPEAT = true,
CASE = true,
FUNCTION = true, FUNCTION_BLOCK = true, ORGANIZATION_BLOCK = true,
TYPE = true, STRUCT = true,
}
return block_keywords[kw]
end
local function is_end_block(line)
local kw = line:match('^%s*(%w+)')
if not kw then return false end
local end_keywords = {
END_IF = true, END_FOR = true, END_WHILE = true, END_REPEAT = true,
END_CASE = true, END_FUNCTION = true, END_FUNCTION_BLOCK = true,
END_ORGANIZATION_BLOCK = true, END_TYPE = true, END_STRUCT = true,
}
return end_keywords[kw]
end
local function process_line(line)
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
if trimmed == "" or trimmed:match("^//") or trimmed:match("^%(%*") then
table.insert(formatted_lines, line)
return
end
if is_end_block(trimmed) then
indent_level = math.max(0, indent_level - 1)
end
local formatted_line = get_indent() .. trimmed
table.insert(formatted_lines, formatted_line)
if is_block_keyword(trimmed) and not is_end_block(trimmed) then
if not trimmed:match("CASE%s+.+OF%s*$") and
not trimmed:match("IF%s+.+THEN%s*$") and
not trimmed:match("ELSIF%s+.+THEN%s*$") and
not trimmed:match("ELSE%s*$") and
not trimmed:match("FOR%s+.+DO%s*$") and
not trimmed:match("WHILE%s+.+DO%s*$") and
not trimmed:match("REPEAT%s*$") then
indent_level = indent_level + 1
end
end
end
for _, line in ipairs(lines) do
process_line(line)
end
local result = table.concat(formatted_lines, "\n")
if content:sub(-1) == "\n" then
result = result .. "\n"
end
return {
{
range = range(0, 0, #lines, 0),
newText = result,
},
}
end
function M.get_formatting_options()
return {
insertSpaces = true,
tabSize = 4,
trimTrailingWhitespace = true,
insertFinalNewline = true,
trimFinalNewlines = true,
}
end
return M
+2522
View File
File diff suppressed because it is too large Load Diff
+195
View File
@@ -0,0 +1,195 @@
local json = {}
local function is_array(t)
local i = 0
for _ in pairs(t) do
i = i + 1
if t[i] == nil then
return false
end
end
return true
end
local function escape(s)
s = s:gsub("\\", "\\\\")
s = s:gsub("\"", "\\\"")
s = s:gsub("\n", "\\n")
s = s:gsub("\r", "\\r")
s = s:gsub("\t", "\\t")
return s
end
function json.encode(data)
local t = type(data)
if t == "nil" then
return "null"
elseif t == "boolean" then
return data and "true" or "false"
elseif t == "number" then
return tostring(data)
elseif t == "string" then
return "\"" .. escape(data) .. "\""
elseif t == "table" then
local parts = {}
if is_array(data) then
for _, v in ipairs(data) do
table.insert(parts, json.encode(v))
end
return "[" .. table.concat(parts, ",") .. "]"
else
for k, v in pairs(data) do
table.insert(parts, "\"" .. escape(k) .. "\":" .. json.encode(v))
end
return "{" .. table.concat(parts, ",") .. "}"
end
else
return "null"
end
end
local function skip_whitespace(s, i)
while i <= #s and s:find("^%s", i) do
i = i + 1
end
return i
end
local function parse_string(s, i)
local result = {}
i = i + 1
while i <= #s do
local c = s:sub(i, i)
if c == "\"" then
return table.concat(result), i + 1
elseif c == "\\" and i < #s then
local next_c = s:sub(i + 1, i + 1)
if next_c == "n" then
table.insert(result, "\n")
elseif next_c == "r" then
table.insert(result, "\r")
elseif next_c == "t" then
table.insert(result, "\t")
elseif next_c == "\\\"" then
table.insert(result, "\"")
elseif next_c == "\\\\" then
table.insert(result, "\\")
else
table.insert(result, next_c)
end
i = i + 2
else
table.insert(result, c)
i = i + 1
end
end
return nil, i
end
local function parse_number(s, i)
local start = i
if s:sub(i, i) == "-" then
i = i + 1
end
while i <= #s and s:find("^%d", s:sub(i, i)) do
i = i + 1
end
if s:sub(i, i) == "." then
i = i + 1
while i <= #s and s:find("^%d", s:sub(i, i)) do
i = i + 1
end
end
if s:sub(i, i):find("^[eE]") then
i = i + 1
if s:sub(i, i):find("^[+-]") then
i = i + 1
end
while i <= #s and s:find("^%d", s:sub(i, i)) do
i = i + 1
end
end
return tonumber(s:sub(start, i - 1)), i
end
local function parse_value(s, i)
i = skip_whitespace(s, i)
if i > #s then
return nil, i
end
local c = s:sub(i, i)
if c == "{" then
local obj = {}
i = i + 1
while i <= #s do
i = skip_whitespace(s, i)
if s:sub(i, i) == "}" then
return obj, i + 1
end
if s:sub(i, i) == "\"" then
local key
key, i = parse_string(s, i)
i = skip_whitespace(s, i)
if s:sub(i, i) ~= ":" then
return nil, i
end
i = i + 1
local val
val, i = parse_value(s, i)
obj[key] = val
i = skip_whitespace(s, i)
if s:sub(i, i) == "}" then
return obj, i + 1
end
if s:sub(i, i) == "," then
i = i + 1
end
else
return nil, i
end
end
return nil, i
elseif c == "[" then
local arr = {}
i = i + 1
while i <= #s do
i = skip_whitespace(s, i)
if s:sub(i, i) == "]" then
return arr, i + 1
end
local val
val, i = parse_value(s, i)
table.insert(arr, val)
i = skip_whitespace(s, i)
if s:sub(i, i) == "]" then
return arr, i + 1
end
if s:sub(i, i) == "," then
i = i + 1
end
end
return nil, i
elseif c == "\"" then
return parse_string(s, i)
elseif s:sub(i, i + 3) == "null" then
return nil, i + 4
elseif s:sub(i, i + 3) == "true" then
return true, i + 4
elseif s:sub(i, i + 4) == "false" then
return false, i + 5
else
return parse_number(s, i)
end
end
function json.decode(s)
if type(s) ~= "string" then
return nil, "invalid input"
end
local data, i = parse_value(s, 1)
return data, i
end
return json
+897
View File
@@ -0,0 +1,897 @@
#!/usr/bin/env lua
-- SCL Language Server
-- Language Server Protocol implementation for Siemens SCL
local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
if script_path == "" then
script_path = "."
end
local cwd = io.popen("pwd"):read("*l")
if script_path:sub(1, 1) == "/" then
script_path = script_path:gsub("/+$", "")
elseif cwd then
script_path = cwd .. "/" .. script_path
script_path = script_path:gsub("/+", "/"):gsub("/$", "")
else
script_path = "."
end
package.path = package.path .. ";" .. script_path .. "/?.lua"
local json = dofile(script_path .. "/json.lua")
local parser = dofile(script_path .. "/parser.lua")
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 documents = {}
local semanticTokenModifiers = {}
local semanticTokenTypes = {
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"func",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"operator",
}
local capabilities = {
hoverProvider = true,
completionProvider = {
triggerCharacters = { ".", "#", "(" },
resolveProvider = false,
},
definitionProvider = true,
documentSymbolProvider = true,
diagnosticProvider = { relatedDocuments = {} },
textDocumentSync = 1,
semanticTokensProvider = {
full = { delta = true },
legend = { tokenTypes = semanticTokenTypes, tokenModifiers = semanticTokenModifiers },
range = true,
},
inlayHintProvider = { resolveProvider = false },
workspaceSymbolProvider = true,
documentFormattingProvider = true,
documentRangeFormattingProvider = false,
}
local function uri_to_path(uri)
return uri:gsub("^file://", ""):gsub("%%20", " ")
end
local function path_to_uri(path)
return "file://" .. path:gsub(" ", "%%20")
end
local function run_command(cmd)
local handle = io.popen(cmd)
if not handle then
return nil
end
local result = handle:read("*a")
handle:close()
return result
end
local function scan_scl_files(root_dir)
local files = {}
if not root_dir or root_dir == "" then
return files
end
local result = run_command("find " .. root_dir .. ' -type f -name "*.scl" 2>/dev/null')
if result then
for f in result:gmatch("[^\n]+") do
if f and f ~= "" then
table.insert(files, f)
end
end
end
return files
end
local function parse_scl_file(filepath)
local handle = io.open(filepath, "r")
if not handle then
return nil, nil, nil
end
local content = handle:read("*a")
handle:close()
local variables = parser.extract_variables(content)
local functions = parser.extract_functions(content)
local types = parser.extract_types(content)
return variables, functions, types
end
local workspace_symbol_cache = {}
local function clear_symbol_cache()
workspace_symbol_cache = {}
end
local function fuzzy_match(text, pattern)
if not text or not pattern then
return false
end
text = text:lower()
pattern = pattern:lower()
local text_len = #text
local pattern_len = #pattern
local pattern_idx = 1
for i = 1, text_len do
if pattern_idx <= pattern_len and text:sub(i, i) == pattern:sub(pattern_idx, pattern_idx) then
pattern_idx = pattern_idx + 1
end
end
return pattern_idx > pattern_len
end
local handlers = {}
function handlers.initialize(params)
return {
capabilities = capabilities,
serverInfo = { name = "scl-language-server", version = "1.0.0" },
}
end
function handlers.initialized(params) end
function handlers.shutdown(params)
return nil
end
function handlers.exit(params)
os.exit(0)
end
function handlers.textDocument_didOpen(params)
local uri = params.textDocument.uri
local content = params.textDocument.text
local path = uri_to_path(uri)
local root_dir = path:match("(.*/)") or "."
local variables, var_positions = parser.extract_variables(content)
local functions = parser.extract_functions(content)
local types = parser.extract_types(content)
local external_types = plc_json.get_types(root_dir)
for name, ext_type in pairs(external_types) do
if not types[name] then
types[name] = ext_type
end
end
local tree = nil
local ts_ok = treesitter.init()
if ts_ok then
tree, _ = treesitter.parse_scl(content)
end
documents[uri] = {
content = content,
version = params.textDocument.version,
path = path,
root_dir = root_dir,
variables = variables,
variable_positions = var_positions,
functions = functions,
types = types,
tree = tree,
}
end
function handlers.textDocument_didChange(params)
local uri = params.textDocument.uri
local content = params.contentChanges[1].text
local variables, var_positions = parser.extract_variables(content)
local functions = parser.extract_functions(content)
local types = parser.extract_types(content)
local doc = documents[uri]
local root_dir = doc and doc.root_dir or ""
local external_types = plc_json.get_types(root_dir)
for name, ext_type in pairs(external_types) do
if not types[name] then
types[name] = ext_type
end
end
local tree = nil
local ts_ok = treesitter.init()
if ts_ok then
tree, _ = treesitter.parse_scl(content)
end
documents[uri] = {
content = content,
version = params.textDocument.version,
path = doc and doc.path or "",
root_dir = root_dir,
variables = variables,
variable_positions = var_positions,
functions = functions,
types = types,
tree = tree,
}
clear_symbol_cache()
end
function handlers.textDocument_didClose(params)
documents[params.textDocument.uri] = nil
clear_symbol_cache()
end
function handlers.textDocument_hover(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return nil
end
local lines = {}
for line in doc.content:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
if #lines == 0 then
return nil
end
local line_idx = params.position.line + 1
if line_idx > #lines then
return nil
end
local line = lines[line_idx]
local col = params.position.character + 1
local word_start = col
while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do
word_start = word_start - 1
end
local word_end = col
while word_end <= #line and line:sub(word_end, word_end):match("[%w_]") do
word_end = word_end + 1
end
local word = line:sub(word_start, word_end - 1)
local var_name = word:gsub("^#", "")
if var_name ~= "" and doc.variables and doc.variables[var_name] then
local var_info = doc.variables[var_name]
local detail = string.format(
"**Local Variable** `#%s`\nScope: `%s`\nType: `%s`\nDeclared at line %d",
var_name,
var_info.type,
var_info.data_type or "unknown",
var_info.line + 1
)
return {
contents = { kind = "markdown", value = detail },
range = {
start = { line = params.position.line, character = word_start - 1 },
endPos = { line = params.position.line, character = word_end - 1 },
},
}
end
if doc.types then
for type_name, type_info in pairs(doc.types) do
if type_name == word then
local detail = string.format("**Type**: `%s`\nKind: %s", type_name, type_info.kind)
if type_info.kind == "struct" and type_info.fields then
detail = detail .. "\n\n**Fields:**\n"
for _, field in ipairs(type_info.fields) do
detail = detail .. string.format("- `%s`: %s\n", field.name, field.type)
end
end
return {
contents = { kind = "markdown", value = detail },
range = {
start = { line = params.position.line, character = word_start - 1 },
endPos = { line = params.position.line, character = word_end - 1 },
},
}
end
end
end
return nil
end
function handlers.textDocument_completion(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return { items = {} }
end
local items = {}
local lines = {}
for line in doc.content:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
local line_idx = params.position.line + 1
if line_idx <= #lines then
local line = lines[line_idx]
local col = params.position.character
local trigger = col > 0 and line:sub(col, col) or ""
if trigger == "#" then
if doc.variables then
for name, info in pairs(doc.variables) do
table.insert(items, {
label = "#" .. name,
kind = 13,
detail = "Local variable (" .. (info.type or "unknown") .. ")",
insertText = "#" .. name,
insertTextFormat = 1,
})
end
end
elseif trigger == "." then
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 prefix = line:sub(prefix_start + 1, col - 1)
if prefix ~= "" then
local var_name = prefix:gsub("^#", "")
local var_info = doc.variables and doc.variables[var_name]
local data_type = var_info and var_info.data_type
if data_type and doc.types then
local struct_name = data_type
if doc.types[struct_name] and doc.types[struct_name].fields then
for _, field in ipairs(doc.types[struct_name].fields) do
table.insert(items, {
label = field.name,
kind = 13,
detail = struct_name .. "." .. field.name .. ": " .. field.type,
insertText = field.name,
insertTextFormat = 1,
})
end
end
end
end
if #items == 0 then
local keywords = {
{ label = "AND", detail = "Logical AND", kind = 14 },
{ label = "OR", detail = "Logical OR", kind = 14 },
{ label = "XOR", detail = "Logical XOR", kind = 14 },
{ label = "NOT", detail = "Logical NOT", kind = 14 },
{ label = "MOD", detail = "Modulo", kind = 14 },
}
for _, kw in ipairs(keywords) do
table.insert(items, kw)
end
end
elseif trigger == "(" then
if doc.functions then
for _, func in ipairs(doc.functions) do
table.insert(items, {
label = func.name,
kind = 3,
detail = func.kind:upper(),
insertText = func.name .. "()",
insertTextFormat = 2,
})
end
end
end
end
return { isIncomplete = false, items = items }
end
function handlers.textDocument_definition(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return nil
end
local lines = {}
for line in doc.content:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
if params.position.line + 1 > #lines then
return nil
end
local line = lines[params.position.line + 1]
local col = params.position.character + 1
local word_start = col
while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do
word_start = word_start - 1
end
local word_end = col
while word_end <= #line and line:sub(word_end, word_end):match("[%w_]") do
word_end = word_end + 1
end
local word = line:sub(word_start, word_end - 1):gsub("^#", "")
if doc.variable_positions and doc.variable_positions[word] then
local pos = doc.variable_positions[word]
return {
uri = uri,
range = {
start = { line = pos.line, character = pos.character },
endPos = { line = pos.line, character = pos.character + #word },
},
}
end
return nil
end
function handlers.textDocument_documentSymbol(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return {}
end
local symbols = {}
local kind_map = {
["function"] = 14,
["function_block"] = 11,
["organization_block"] = 25,
["struct"] = 23,
["alias"] = 7,
["array"] = 24,
}
if doc.functions then
for _, func in ipairs(doc.functions) do
table.insert(symbols, {
name = func.kind:upper() .. " " .. func.name,
kind = kind_map[func.kind] or 14,
range = { start = { line = func.start, character = 0 }, endPos = { line = func.final, character = 0 } },
selectionRange = {
start = { line = func.start, character = 0 },
endPos = { line = func.start, character = #func.name },
},
containerName = nil,
})
end
end
if doc.variables then
for var_name, var_info in pairs(doc.variables) do
local var_kind = 13
if var_info.type and (var_info.type:find("CONSTANT") or var_info.type:find("RETAIN")) then
var_kind = 26
end
table.insert(symbols, {
name = "#" .. var_name,
kind = var_kind,
detail = var_info.type or "VAR",
range = {
start = { line = var_info.line, character = 0 },
endPos = { line = var_info.line, character = #var_name },
},
selectionRange = {
start = { line = var_info.line, character = var_info.character },
endPos = { line = var_info.line, character = var_info.character + #var_name },
},
containerName = nil,
})
end
end
if doc.types then
for type_name, type_info in pairs(doc.types) do
local kind = kind_map[type_info.kind] or 7
local detail = type_info.kind:upper()
if type_info.kind == "alias" and type_info.base_type then
detail = detail .. " (" .. type_info.base_type .. ")"
elseif type_info.kind == "struct" and type_info.fields and #type_info.fields > 0 then
detail = detail .. " (" .. #type_info.fields .. " fields)"
end
table.insert(symbols, {
name = "TYPE " .. type_name,
kind = kind,
detail = detail,
range = {
start = { line = type_info.start_line or 0, character = 0 },
endPos = { line = type_info.start_line or 0, character = #type_name },
},
selectionRange = {
start = { line = type_info.start_line or 0, character = 0 },
endPos = { line = type_info.start_line or 0, character = #type_name },
},
containerName = nil,
})
end
end
return symbols
end
function handlers.textDocument_diagnostic(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return { items = {} }
end
local external_types = {}
if doc.root_dir then
external_types = plc_json.get_types(doc.root_dir)
end
local diag_results = diagnostics.validate_diagnostics(doc.content, external_types, doc.root_dir)
return { items = diag_results }
end
function handlers.textDocument_semanticTokensFull(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return { data = {} }
end
local tokens = {}
local lines = {}
for line in doc.content:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
local function add_token(line, startChar, length, tokenType)
local deltaLine = line
local deltaStart = startChar
if #tokens >= 4 then
local prevLine = tokens[#tokens - 3]
local prevStart = tokens[#tokens - 2]
if line == prevLine then
deltaLine = 0
deltaStart = startChar - prevStart
else
deltaLine = line - prevLine
end
end
table.insert(tokens, deltaLine)
table.insert(tokens, deltaStart)
table.insert(tokens, length)
table.insert(tokens, 0)
table.insert(tokens, 0)
end
for i, line in ipairs(lines) do
local lineNum = i - 1
local trimmed = line:gsub("^%s+", "")
if doc.variables then
for var_name, info in pairs(doc.variables) do
if info.line == lineNum then
local startChar = line:find(var_name, 1, true)
if startChar then
add_token(lineNum, startChar - 1, #var_name, "variable")
end
end
end
end
local keywords = {
"IF",
"THEN",
"ELSIF",
"ELSE",
"END_IF",
"FOR",
"TO",
"BY",
"DO",
"END_FOR",
"WHILE",
"END_WHILE",
"REPEAT",
"UNTIL",
"END_REPEAT",
"CASE",
"OF",
"END_CASE",
"VAR_INPUT",
"VAR_OUTPUT",
"VAR_IN_OUT",
"VAR_TEMP",
"VAR",
"END_VAR",
"BEGIN",
"EXIT",
"CONTINUE",
"RETURN",
"TRUE",
"FALSE",
}
for _, kw in ipairs(keywords) do
local startPos = line:find("%f[%a]" .. kw .. "%f[%A]")
if startPos then
add_token(lineNum, startPos - 1, #kw, "keyword")
end
end
end
return { data = tokens }
end
function handlers.textDocument_semanticTokensRange(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return { data = {} }
end
return handlers.textDocument_semanticTokensFull(params)
end
function handlers.textDocument_inlayHint(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return {}
end
local hints = {}
local lines = {}
for line in doc.content:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
for i, line in ipairs(lines) do
local lineNum = i - 1
if doc.variables then
for var_name, info in pairs(doc.variables) do
if info.line == lineNum and info.data_type and info.data_type ~= "unknown" then
local colonPos = line:find(":", 1, true)
if colonPos then
table.insert(hints, {
position = { line = lineNum, character = #line },
label = ": " .. info.data_type,
kind = 1,
paddingLeft = true,
})
end
end
end
end
end
return hints
end
function handlers.workspace_symbol(params)
local query = params.query or ""
local max_results = params.maxResults or 50
if query == "" then
return {}
end
local results = {}
local function add_result(name, kind, uri, line, col, length)
table.insert(results, {
name = name,
kind = kind,
location = {
uri = uri,
range = {
start = { line = line, character = col },
endPos = { line = line, character = col + length },
},
},
})
end
for uri, doc in pairs(documents) do
if doc.functions then
for _, func in ipairs(doc.functions) do
if fuzzy_match(func.name, query) or fuzzy_match(func.kind, query) then
add_result(
func.kind:upper() .. " " .. func.name,
func.kind == "function" and 14 or (func.kind == "function_block" and 11 or 25),
uri,
func.start,
0,
#func.name
)
end
end
end
if doc.variables then
for var_name, var_info in pairs(doc.variables) do
if fuzzy_match(var_name, query) then
add_result("#" .. var_name, 13, uri, var_info.line, var_info.character, #var_name)
end
end
end
if doc.types then
for type_name, type_info in pairs(doc.types) do
if fuzzy_match(type_name, query) then
local kind = 7
if type_info.kind == "struct" then
kind = 23
elseif type_info.kind == "array" then
kind = 24
end
add_result("TYPE " .. type_name, kind, uri, type_info.start_line or 0, 0, #type_name)
end
end
end
end
local scanned_dirs = {}
for _, doc in pairs(documents) do
if doc.root_dir and doc.root_dir ~= "" then
scanned_dirs[doc.root_dir] = true
end
end
local function search_file(filepath)
local cached_file = filepath
if not workspace_symbol_cache[":file:" .. cached_file] then
local variables, functions, types = parse_scl_file(filepath)
workspace_symbol_cache[":file:" .. cached_file] = {
variables = variables,
functions = functions,
types = types,
}
end
local cached_data = workspace_symbol_cache[":file:" .. cached_file]
local file_uri = path_to_uri(filepath)
if cached_data.functions then
for _, func in ipairs(cached_data.functions) do
if fuzzy_match(func.name, query) or fuzzy_match(func.kind, query) then
add_result(
func.kind:upper() .. " " .. func.name,
func.kind == "function" and 14 or (func.kind == "function_block" and 11 or 25),
file_uri,
func.start,
0,
#func.name
)
end
end
end
if cached_data.variables then
for var_name, var_info in pairs(cached_data.variables) do
if fuzzy_match(var_name, query) then
add_result("#" .. var_name, 13, file_uri, var_info.line, var_info.character, #var_name)
end
end
end
if cached_data.types then
for type_name, type_info in pairs(cached_data.types) do
if fuzzy_match(type_name, query) then
local kind = 7
if type_info.kind == "struct" then
kind = 23
elseif type_info.kind == "array" then
kind = 24
end
add_result("TYPE " .. type_name, kind, file_uri, type_info.start_line or 0, 0, #type_name)
end
end
end
end
for root_dir, _ in pairs(scanned_dirs) do
local scl_files = scan_scl_files(root_dir)
for _, filepath in ipairs(scl_files) do
search_file(filepath)
end
end
local final_results = {}
for i = 1, math.min(#results, max_results) do
table.insert(final_results, results[i])
end
return final_results
end
function handlers.textDocument_formatting(params)
local uri = params.textDocument.uri
local doc = documents[uri]
if not doc then
return nil
end
local options = params.options or {}
local format_options = {
insertSpaces = options.insertSpaces,
tabSize = options.tabSize,
indentSize = options.tabSize or 4,
}
return formatter.format_document(doc.content, format_options)
end
local function handle_message(message)
if message.method == "$/cancelRequest" then
return nil
end
local handler = handlers[message.method]
if handler then
local ok, result = pcall(handler, message.params)
if ok then
return { id = message.id, result = result }
else
return { id = message.id, error = { code = -32603, message = tostring(result) } }
end
else
return { id = message.id, error = { code = -32601, message = "Method not found: " .. message.method } }
end
end
local function main()
local stdin = io.input()
local stdout = io.output()
local content_length = nil
while true do
local line = stdin:read("*l")
if not line then
break
end
if line:match("^Content%-Length:%s*(%d+)") then
content_length = tonumber(line:match("^Content%-Length:%s*(%d+)"))
elseif line == "" then
if content_length then
local body = stdin:read(content_length)
local message = json.decode(body)
if message.method == "exit" then
break
end
local response = handle_message(message)
if response then
local response_body = json.encode(response)
stdout:write("Content-Length: " .. #response_body .. "\r\n")
stdout:write("Content-Type: application/json\r\n\r\n")
stdout:write(response_body)
stdout:flush()
end
content_length = nil
end
end
end
end
main()
+1555
View File
File diff suppressed because it is too large Load Diff
+67456
View File
File diff suppressed because it is too large Load Diff
+267
View File
@@ -0,0 +1,267 @@
-- 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 var_type = "unknown"
local current_block = nil
local function process_line(line, line_num)
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
-- Detect block start
local fb_name = trimmed:match('FUNCTION_BLOCK%s+"([^"]+)"')
if fb_name then
current_block = { name = fb_name, kind = "function_block" }
in_var_section = false
return
end
local ob_name = trimmed:match('ORGANIZATION_BLOCK%s+"([^"]+)"')
if ob_name then
current_block = { name = ob_name, kind = "organization_block" }
in_var_section = false
return
end
local fn_name = trimmed:match('FUNCTION%s+"([^"]+)"')
if fn_name then
current_block = { name = fn_name, kind = "function" }
in_var_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
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
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 not in_var_section then return end
if trimmed:match("^%(%*") or trimmed:match("^%s*//") then
return
end
local var_name = trimmed:match("^([%w_]+)%s*:")
if not var_name then
var_name = 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 = line:find(":", 1, true)
local var_data_type = "unknown"
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,
}
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
for i, line in ipairs(lines) do
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
if trimmed:match("^TYPE$") or trimmed:match("^TYPE%s+:=") then
in_type_section = true
brace_depth = 0
elseif in_type_section and trimmed:match("^END_TYPE") then
in_type_section = false
current_type = nil
elseif in_type_section then
local type_name = trimmed:match("^(%w+):")
if type_name then
local struct_start = trimmed:find("{")
local array_match = trimmed:find("ARRAY")
if struct_start then
current_type = {
name = type_name,
kind = "struct",
fields = {},
start_line = i - 1,
}
types[type_name] = current_type
elseif array_match then
local of_type = trimmed:match("OF%s+(%w+)")
if of_type then
current_type = {
name = type_name,
kind = "array",
element_type = of_type,
start_line = i - 1,
}
types[type_name] = current_type
end
else
local field_type = trimmed:match(":%s*(%w+)")
if field_type then
current_type = {
name = type_name,
kind = "alias",
base_type = field_type,
start_line = i - 1,
}
types[type_name] = current_type
end
end
end
if current_type and current_type.kind == "struct" then
for open in line:gmatch("{") do brace_depth = brace_depth + 1 end
for close in line:gmatch("}") do brace_depth = brace_depth - 1 end
local field_name = line:match("^%s*(%w+):")
if field_name and field_name ~= current_type.name then
local field_type = line:match(":%s*(%w+)")
table.insert(current_type.fields, {
name = field_name,
type = field_type or "unknown",
})
end
if brace_depth == 0 then
current_type = nil
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+"([^"]+)"')
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+"([^"]+)"')
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+"([^"]+)"')
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
+268
View File
@@ -0,0 +1,268 @@
-- Load UDT types from external .plc.json files
-- Auto-generate from data_types folder
local M = {}
local cached_types = {}
local function run_command(cmd)
local handle = io.popen(cmd)
if not handle then return nil end
local result = handle:read("*a")
handle:close()
return result
end
local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
if script_path == "" then script_path = "." end
local cwd = io.popen("pwd"):read("*l")
if script_path:sub(1, 1) == "/" then
script_path = script_path:gsub("/+$", "")
elseif cwd then
script_path = cwd .. "/" .. script_path
script_path = script_path:gsub("/+", "/"):gsub("/$", "")
else
script_path = "."
end
local function load_json(content)
local json = dofile(script_path .. "/json.lua")
local ok, data = pcall(function() return json.decode(content) end)
if ok then return data end
return nil
end
local function scan_files_recursive(dir, pattern)
local files = {}
local result = run_command("find " .. dir .. " -type f -name \"" .. pattern .. "\" 2>/dev/null")
if result then
for f in result:gmatch("[^\n]+") do
if f ~= "" then table.insert(files, f) end
end
end
return files
end
local function parse_plc_type(dt)
local type_name = dt.name or dt.Name or dt.baseType or dt.dataTypeName
if not type_name then return nil end
local typ = {
name = type_name,
kind = "alias",
fields = {},
source = "external",
}
if dt.struct or dt.member or dt.members or dt.elements then
typ.kind = "struct"
local members = dt.struct or dt.member or dt.members or dt.elements
if type(members) == "table" then
for _, m in ipairs(members) do
local field_name = m.name or m.Name
local field_type = m.dataTypeName or m.baseType or m.type or m.dataType or "unknown"
if field_name then
table.insert(typ.fields, {
name = field_name,
type = field_type,
})
end
end
end
elseif dt.baseType or dt.baseTypeName then
typ.kind = "alias"
typ.base_type = dt.baseType or dt.baseTypeName
end
return typ
end
local function parse_scl_type_file(content)
local types = {}
local in_type = false
local in_struct = false
local current_type_name = nil
local pending_type_name = nil
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
in_type = true
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
in_struct = false
current_type_name = nil
pending_type_name = nil
elseif trimmed:match("^STRUCT") then
if pending_type_name then
current_type_name = pending_type_name
in_struct = true
types[pending_type_name] = { name = pending_type_name, kind = "struct", fields = {}, source = "data_types_folder" }
pending_type_name = nil
end
elseif not in_struct and trimmed:match("^%w+%s*:") and not trimmed:match("^STRUCT") then
local type_name = trimmed:match("^(%w+)%s*:")
if type_name then
local next_line = lines[i + 1]
local next_trimmed = next_line and next_line:gsub("^%s+", ""):gsub("%s+$", "")
if next_trimmed and next_trimmed:match("^STRUCT") then
pending_type_name = type_name
else
pending_type_name = nil
current_type_name = nil
in_struct = false
local base_type = trimmed:match(":%s*([%w_]+)")
types[type_name] = { name = type_name, kind = "alias", base_type = base_type or "UNKNOWN", source = "data_types_folder" }
end
end
elseif in_struct and current_type_name and types[current_type_name] then
local field_name = line:match("^%s*(%w+)%s*:")
if field_name and field_name ~= "END_STRUCT" then
local field_type = line:match(":%s*([%w_]+)")
table.insert(types[current_type_name].fields, {
name = field_name,
type = field_type or "unknown",
})
end
end
end
end
return types
end
function M.load_types_from_workspace(root_dir)
local types = {}
if not root_dir or root_dir == "" then return types end
-- Scan for .plc.json files
local plc_files = scan_files_recursive(root_dir, "*.plc.json")
for _, filepath in ipairs(plc_files) do
local content = run_command("cat " .. filepath:gsub(" ", "\\ "))
if content then
local data = load_json(content)
if data and (data.dataTypes or data.DataTypes or data.types or data.Types) then
local typeList = data.dataTypes or data.DataTypes or data.types or data.Types
for _, dt in ipairs(typeList) do
local typ = parse_plc_type(dt)
if typ and not types[typ.name] then
types[typ.name] = typ
end
end
end
end
end
-- Scan for plc.data.json files
local plc_data_files = scan_files_recursive(root_dir, "plc.data.json")
for _, filepath in ipairs(plc_data_files) do
local content = run_command("cat " .. filepath:gsub(" ", "\\ "))
if content then
local data = load_json(content)
if data and (data.dataTypes or data.DataTypes) then
for _, dt in ipairs(data.dataTypes or data.DataTypes) do
local typ = parse_plc_type(dt)
if typ and not types[typ.name] then
types[typ.name] = typ
end
end
end
end
end
-- Auto-generate from data_types folder (SCL files)
local data_types_files = scan_files_recursive(root_dir, "*.scl")
for _, filepath in ipairs(data_types_files) do
local content = run_command("cat " .. filepath:gsub(" ", "\\ "))
if content then
local scl_types = parse_scl_type_file(content)
for name, typ in pairs(scl_types) do
if not types[name] then
types[name] = typ
end
end
end
end
return types
end
function M.get_types(root_dir)
if not root_dir or root_dir == "" then
root_dir = "workspace"
end
if cached_types[root_dir] then
return cached_types[root_dir]
end
local types = M.load_types_from_workspace(root_dir)
cached_types[root_dir] = types
return types
end
function M.generate_plc_json(root_dir, output_file)
local json = dofile(script_path .. "/json.lua")
if not root_dir or root_dir == "" then return false end
local types = M.load_types_from_workspace(root_dir)
local dataTypes = {}
for _, typ in pairs(types) do
if typ.kind == "struct" then
local members = {}
for _, field in ipairs(typ.fields) do
table.insert(members, {
name = field.name,
dataTypeName = field.type,
})
end
table.insert(dataTypes, {
name = typ.name,
struct = members,
})
elseif typ.kind == "alias" then
table.insert(dataTypes, {
name = typ.name,
baseType = typ.base_type or "UNKNOWN",
})
end
end
local output = {
plcProject = {
dataTypes = dataTypes,
},
}
local content = json.encode(output)
local file = io.open(output_file, "w")
if file then
file:write(content)
file:close()
return true
end
return false
end
function M.clear_cache()
cached_types = {}
end
return M
+54
View File
@@ -0,0 +1,54 @@
#ifndef TREE_SITTER_ALLOC_H_
#define TREE_SITTER_ALLOC_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
// Allow clients to override allocation functions
#ifdef TREE_SITTER_REUSE_ALLOCATOR
extern void *(*ts_current_malloc)(size_t size);
extern void *(*ts_current_calloc)(size_t count, size_t size);
extern void *(*ts_current_realloc)(void *ptr, size_t size);
extern void (*ts_current_free)(void *ptr);
#ifndef ts_malloc
#define ts_malloc ts_current_malloc
#endif
#ifndef ts_calloc
#define ts_calloc ts_current_calloc
#endif
#ifndef ts_realloc
#define ts_realloc ts_current_realloc
#endif
#ifndef ts_free
#define ts_free ts_current_free
#endif
#else
#ifndef ts_malloc
#define ts_malloc malloc
#endif
#ifndef ts_calloc
#define ts_calloc calloc
#endif
#ifndef ts_realloc
#define ts_realloc realloc
#endif
#ifndef ts_free
#define ts_free free
#endif
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ALLOC_H_
+347
View File
@@ -0,0 +1,347 @@
#ifndef TREE_SITTER_ARRAY_H_
#define TREE_SITTER_ARRAY_H_
#ifdef __cplusplus
extern "C" {
#endif
#include "./alloc.h"
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4101)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#endif
#define Array(T) \
struct { \
T *contents; \
uint32_t size; \
uint32_t capacity; \
}
/// Initialize an array.
#define array_init(self) \
((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL)
/// Create an empty array.
#define array_new() \
{ NULL, 0, 0 }
/// Get a pointer to the element at a given `index` in the array.
#define array_get(self, _index) \
(assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index])
/// Get a pointer to the first element in the array.
#define array_front(self) array_get(self, 0)
/// Get a pointer to the last element in the array.
#define array_back(self) array_get(self, (self)->size - 1)
/// Clear the array, setting its size to zero. Note that this does not free any
/// memory allocated for the array's contents.
#define array_clear(self) ((self)->size = 0)
/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is
/// less than the array's current capacity, this function has no effect.
#define array_reserve(self, new_capacity) \
((self)->contents = _array__reserve( \
(void *)(self)->contents, &(self)->capacity, \
array_elem_size(self), new_capacity) \
)
/// Free any memory allocated for this array. Note that this does not free any
/// memory allocated for the array's contents.
#define array_delete(self) _array__delete((self), (void *)(self)->contents, sizeof(*self))
/// Push a new `element` onto the end of the array.
#define array_push(self, element) \
do { \
(self)->contents = _array__grow( \
(void *)(self)->contents, (self)->size, &(self)->capacity, \
1, array_elem_size(self) \
); \
(self)->contents[(self)->size++] = (element); \
} while(0)
/// Increase the array's size by `count` elements.
/// New elements are zero-initialized.
#define array_grow_by(self, count) \
do { \
if ((count) == 0) break; \
(self)->contents = _array__grow( \
(self)->contents, (self)->size, &(self)->capacity, \
count, array_elem_size(self) \
); \
memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \
(self)->size += (count); \
} while (0)
/// Append all elements from one array to the end of another.
#define array_push_all(self, other) \
array_extend((self), (other)->size, (other)->contents)
/// Append `count` elements to the end of the array, reading their values from the
/// `contents` pointer.
#define array_extend(self, count, other_contents) \
(self)->contents = _array__splice( \
(void*)(self)->contents, &(self)->size, &(self)->capacity, \
array_elem_size(self), (self)->size, 0, count, other_contents \
)
/// Remove `old_count` elements from the array starting at the given `index`. At
/// the same index, insert `new_count` new elements, reading their values from the
/// `new_contents` pointer.
#define array_splice(self, _index, old_count, new_count, new_contents) \
(self)->contents = _array__splice( \
(void *)(self)->contents, &(self)->size, &(self)->capacity, \
array_elem_size(self), _index, old_count, new_count, new_contents \
)
/// Insert one `element` into the array at the given `index`.
#define array_insert(self, _index, element) \
(self)->contents = _array__splice( \
(void *)(self)->contents, &(self)->size, &(self)->capacity, \
array_elem_size(self), _index, 0, 1, &(element) \
)
/// Remove one element from the array at the given `index`.
#define array_erase(self, _index) \
_array__erase((void *)(self)->contents, &(self)->size, array_elem_size(self), _index)
/// Pop the last element off the array, returning the element by value.
#define array_pop(self) ((self)->contents[--(self)->size])
/// Assign the contents of one array to another, reallocating if necessary.
#define array_assign(self, other) \
(self)->contents = _array__assign( \
(void *)(self)->contents, &(self)->size, &(self)->capacity, \
(const void *)(other)->contents, (other)->size, array_elem_size(self) \
)
/// Swap one array with another
#define array_swap(self, other) \
do { \
struct Swap swapped_contents = _array__swap( \
(void *)(self)->contents, &(self)->size, &(self)->capacity, \
(void *)(other)->contents, &(other)->size, &(other)->capacity \
); \
(self)->contents = swapped_contents.self_contents; \
(other)->contents = swapped_contents.other_contents; \
} while (0)
/// Get the size of the array contents
#define array_elem_size(self) (sizeof *(self)->contents)
/// Search a sorted array for a given `needle` value, using the given `compare`
/// callback to determine the order.
///
/// If an existing element is found to be equal to `needle`, then the `index`
/// out-parameter is set to the existing value's index, and the `exists`
/// out-parameter is set to true. Otherwise, `index` is set to an index where
/// `needle` should be inserted in order to preserve the sorting, and `exists`
/// is set to false.
#define array_search_sorted_with(self, compare, needle, _index, _exists) \
_array__search_sorted(self, 0, compare, , needle, _index, _exists)
/// Search a sorted array for a given `needle` value, using integer comparisons
/// of a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_with`.
#define array_search_sorted_by(self, field, needle, _index, _exists) \
_array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists)
/// Insert a given `value` into a sorted array, using the given `compare`
/// callback to determine the order.
#define array_insert_sorted_with(self, compare, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_with(self, compare, &(value), &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
/// Insert a given `value` into a sorted array, using integer comparisons of
/// a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_by`.
#define array_insert_sorted_by(self, field, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_by(self, field, (value) field, &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
// Private
// Pointers to individual `Array` fields (rather than the entire `Array` itself)
// are passed to the various `_array__*` functions below to address strict aliasing
// violations that arises when the _entire_ `Array` struct is passed as `Array(void)*`.
//
// The `Array` type itself was not altered as a solution in order to avoid breakage
// with existing consumers (in particular, parsers with external scanners).
/// This is not what you're looking for, see `array_delete`.
static inline void _array__delete(void *self, void *contents, size_t self_size) {
if (contents) ts_free(contents);
if (self) memset(self, 0, self_size);
}
/// This is not what you're looking for, see `array_erase`.
static inline void _array__erase(void* self_contents, uint32_t *size,
size_t element_size, uint32_t index) {
assert(index < *size);
char *contents = (char *)self_contents;
memmove(contents + index * element_size, contents + (index + 1) * element_size,
(*size - index - 1) * element_size);
(*size)--;
}
/// This is not what you're looking for, see `array_reserve`.
static inline void *_array__reserve(void *contents, uint32_t *capacity,
size_t element_size, uint32_t new_capacity) {
void *new_contents = contents;
if (new_capacity > *capacity) {
if (contents) {
new_contents = ts_realloc(contents, new_capacity * element_size);
} else {
new_contents = ts_malloc(new_capacity * element_size);
}
*capacity = new_capacity;
}
return new_contents;
}
/// This is not what you're looking for, see `array_assign`.
static inline void *_array__assign(void* self_contents, uint32_t *self_size, uint32_t *self_capacity,
const void *other_contents, uint32_t other_size, size_t element_size) {
void *new_contents = _array__reserve(self_contents, self_capacity, element_size, other_size);
*self_size = other_size;
memcpy(new_contents, other_contents, *self_size * element_size);
return new_contents;
}
struct Swap {
void *self_contents;
void *other_contents;
};
/// This is not what you're looking for, see `array_swap`.
// static inline void _array__swap(Array *self, Array *other) {
static inline struct Swap _array__swap(void *self_contents, uint32_t *self_size, uint32_t *self_capacity,
void *other_contents, uint32_t *other_size, uint32_t *other_capacity) {
void *new_self_contents = other_contents;
uint32_t new_self_size = *other_size;
uint32_t new_self_capacity = *other_capacity;
void *new_other_contents = self_contents;
*other_size = *self_size;
*other_capacity = *self_capacity;
*self_size = new_self_size;
*self_capacity = new_self_capacity;
struct Swap out = {
.self_contents = new_self_contents,
.other_contents = new_other_contents,
};
return out;
}
/// This is not what you're looking for, see `array_push` or `array_grow_by`.
static inline void *_array__grow(void *contents, uint32_t size, uint32_t *capacity,
uint32_t count, size_t element_size) {
void *new_contents = contents;
uint32_t new_size = size + count;
if (new_size > *capacity) {
uint32_t new_capacity = *capacity * 2;
if (new_capacity < 8) new_capacity = 8;
if (new_capacity < new_size) new_capacity = new_size;
new_contents = _array__reserve(contents, capacity, element_size, new_capacity);
}
return new_contents;
}
/// This is not what you're looking for, see `array_splice`.
static inline void *_array__splice(void *self_contents, uint32_t *size, uint32_t *capacity,
size_t element_size,
uint32_t index, uint32_t old_count,
uint32_t new_count, const void *elements) {
uint32_t new_size = *size + new_count - old_count;
uint32_t old_end = index + old_count;
uint32_t new_end = index + new_count;
assert(old_end <= *size);
void *new_contents = _array__reserve(self_contents, capacity, element_size, new_size);
char *contents = (char *)new_contents;
if (*size > old_end) {
memmove(
contents + new_end * element_size,
contents + old_end * element_size,
(*size - old_end) * element_size
);
}
if (new_count > 0) {
if (elements) {
memcpy(
(contents + index * element_size),
elements,
new_count * element_size
);
} else {
memset(
(contents + index * element_size),
0,
new_count * element_size
);
}
}
*size += new_count - old_count;
return new_contents;
}
/// A binary search routine, based on Rust's `std::slice::binary_search_by`.
/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`.
#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \
do { \
*(_index) = start; \
*(_exists) = false; \
uint32_t size = (self)->size - *(_index); \
if (size == 0) break; \
int comparison; \
while (size > 1) { \
uint32_t half_size = size / 2; \
uint32_t mid_index = *(_index) + half_size; \
comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \
if (comparison <= 0) *(_index) = mid_index; \
size -= half_size; \
} \
comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \
if (comparison == 0) *(_exists) = true; \
else if (comparison < 0) *(_index) += 1; \
} while (0)
/// Helper macro for the `_sorted_by` routines below. This takes the left (existing)
/// parameter by reference in order to work with the generic sorting function above.
#define _compare_int(a, b) ((int)*(a) - (int)(b))
#ifdef _MSC_VER
#pragma warning(pop)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ARRAY_H_
+286
View File
@@ -0,0 +1,286 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSStateId;
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
typedef struct TSLanguageMetadata {
uint8_t major_version;
uint8_t minor_version;
uint8_t patch_version;
} TSLanguageMetadata;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
// Used to index the field and supertype maps.
typedef struct {
uint16_t index;
uint16_t length;
} TSMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
void (*log)(const TSLexer *, const char *, ...);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
uint16_t reserved_word_set_id;
} TSLexerMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
typedef struct {
int32_t start;
int32_t end;
} TSCharacterRange;
struct TSLanguage {
uint32_t abi_version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexerMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
const TSStateId *primary_state_ids;
const char *name;
const TSSymbol *reserved_words;
uint16_t max_reserved_word_set_size;
uint32_t supertype_count;
const TSSymbol *supertype_symbols;
const TSMapSlice *supertype_map_slices;
const TSSymbol *supertype_map_entries;
TSLanguageMetadata metadata;
};
static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) {
uint32_t index = 0;
uint32_t size = len - index;
while (size > 1) {
uint32_t half_size = size / 2;
uint32_t mid_index = index + half_size;
const TSCharacterRange *range = &ranges[mid_index];
if (lookahead >= range->start && lookahead <= range->end) {
return true;
} else if (lookahead > range->end) {
index = mid_index;
}
size -= half_size;
}
const TSCharacterRange *range = &ranges[index];
return (lookahead >= range->start && lookahead <= range->end);
}
/*
* Lexer Macros
*/
#ifdef _MSC_VER
#define UNUSED __pragma(warning(suppress : 4101))
#else
#define UNUSED __attribute__((unused))
#endif
#define START_LEXER() \
bool result = false; \
bool skip = false; \
UNUSED \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define ADVANCE_MAP(...) \
{ \
static const uint16_t map[] = { __VA_ARGS__ }; \
for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \
if (map[i] == lookahead) { \
state = map[i + 1]; \
goto next_state; \
} \
} \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT)
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value) \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value), \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_name, children, precedence, prod_id) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_name, \
.child_count = children, \
.dynamic_precedence = precedence, \
.production_id = prod_id \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_
+149
View File
@@ -0,0 +1,149 @@
-- Tree-sitter integration for SCL LSP
-- Uses tree-sitter to parse SCL files more accurately
local M = {}
local ts = nil
function M.init()
local ok, treesitter = pcall(require, "vim.treesitter")
if ok then
ts = treesitter
return true
end
return false
end
function M.parse_scl(content)
if not ts then
return nil, "Tree-sitter not available"
end
local ok, parser = pcall(ts.get_parser, 0, "scl")
if not ok or not parser then
return nil, "SCL parser not available"
end
local tree = parser:parse_string(content)
if not tree then
return nil, "Failed to parse content"
end
return tree, nil
end
function M.extract_variables_ts(tree, bufnr)
local variables = {}
local variable_positions = {}
if not tree or not ts then return variables, variable_positions end
local root = tree:root()
local var_types = {
var_declaration = "VAR",
var_input_declaration = "VAR_INPUT",
var_output_declaration = "VAR_OUTPUT",
var_in_out_declaration = "VAR_IN_OUT",
var_temp_declaration = "VAR_TEMP",
var_constant_declaration = "VAR_CONSTANT",
var_retain_declaration = "VAR_RETAIN",
var_non_retain_declaration = "VAR_NON_RETAIN",
}
for node_type, var_type in pairs(var_types) do
for node in root:iter_descendants_of_type(node_type) do
local var_item = node:child_by_field_name("item")
if var_item then
local name_node = var_item:child_by_field_name("name")
if name_node then
local start_row, start_col, end_row, end_col = name_node:range()
local name = ts.get_node_text(name_node, bufnr)
if name and not variables[name] then
variables[name] = {
type = var_type,
line = start_row,
character = start_col,
}
variable_positions[name] = {
line = start_row,
character = start_col,
}
end
end
end
end
end
return variables, variable_positions
end
function M.extract_functions_ts(tree, bufnr)
local functions = {}
if not tree or not ts then return functions end
local root = tree:root()
local function extract_block(block_type, kind)
for node in root:iter_descendants_of_type(block_type) do
local name_node = node:child_by_field_name("name")
if name_node then
local start_row, start_col, end_row, end_col = node:range()
local name = ts.get_node_text(name_node, bufnr)
table.insert(functions, {
name = name,
kind = kind,
start = start_row,
final = end_row,
})
end
end
end
extract_block("function", "function")
extract_block("function_block", "function_block")
extract_block("organization_block", "organization_block")
return functions
end
function M.get_node_at_position(tree, row, col)
if not tree then return nil end
local root = tree:root()
for node in root:iter_descendants() do
local start_row, start_col, end_row, end_col = node:range()
if start_row <= row and end_row >= row then
if start_col <= col and end_col >= col then
return node
end
end
end
return nil
end
function M.get_variable_type_at_cursor(tree, bufnr, row, col)
if not tree or not ts then return nil end
local node = M.get_node_at_position(tree, row, col)
if not node then return nil end
local node_type = node:type()
if node_type == "identifier" then
local parent = node:parent()
if parent and parent:type() == "prefixed_identifier" then
local name = ts.get_node_text(node, bufnr)
local vars, _ = M.extract_variables_ts(tree, bufnr)
if vars and vars[name] then
return vars[name].type
end
end
end
return nil
end
return M