Inintal commit
This commit is contained in:
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+195
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+67456
File diff suppressed because it is too large
Load Diff
+267
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user