Merge branch 'feat/lsp_goto' into develop
This commit is contained in:
@@ -50,6 +50,18 @@ scl_lsp/
|
|||||||
└── grammar.js # Tree-sitter grammar
|
└── grammar.js # Tree-sitter grammar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Filetype Detection
|
||||||
|
|
||||||
|
The LSP requires the `scl` filetype to be set. Add to your Neovim config (e.g., `ftdetect/scl.vim`):
|
||||||
|
|
||||||
|
```vim
|
||||||
|
au BufRead,BufNewFile *.scl set filetype=scl
|
||||||
|
au BufRead,BufNewFile *.udt set filetype=scl
|
||||||
|
au BufRead,BufNewFile *.db set filetype=scl
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables LSP features (completion, goto definition, hover, etc.) for all SCL-related file types.
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### Module Pattern
|
### Module Pattern
|
||||||
@@ -235,6 +247,17 @@ Rules are checked in order; first match wins.
|
|||||||
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
||||||
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
||||||
|
|
||||||
|
## LSP Features
|
||||||
|
|
||||||
|
### Goto Declaration (`gD`)
|
||||||
|
The LSP supports goto declaration functionality for:
|
||||||
|
- **Functions** - Jump to function definition in `.scl` files
|
||||||
|
- **Function Blocks (FB)** - Jump to FB definition in `.scl` files
|
||||||
|
- **Data Blocks (DB)** - Jump to DB definition in `.scl` and `.db` files
|
||||||
|
- **User-Defined Types (UDT)** - Jump to type definition in `.scl` and `.udt` files
|
||||||
|
|
||||||
|
Usage: Place cursor on a variable or type name and press `gD` (or use LSP client command).
|
||||||
|
|
||||||
## Keybindings (SCL/UDT files)
|
## Keybindings (SCL/UDT files)
|
||||||
|
|
||||||
### Attribute Block Toggle
|
### Attribute Block Toggle
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP)
|
|||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| **Hover** | Press `K` to see variable type and struct fields |
|
| **Hover** | Press `K` to see variable type and struct fields |
|
||||||
| **Completion** | `#` for variables, `.` for struct fields, `(` for functions |
|
| **Completion** | `#` for variables, `.` for struct fields, `(` for functions |
|
||||||
| **Go to Definition** | `gd` jump to variable declaration |
|
| **Go to Definition** | `gD` jump to function, FB, DB, or UDT definition |
|
||||||
| **Document Symbols** | Shows functions, variables, types |
|
| **Document Symbols** | Shows functions, variables, types |
|
||||||
| **Workspace Symbols** | Search across all `.scl` files with fuzzy matching |
|
| **Workspace Symbols** | Search across all `.scl` files with fuzzy matching |
|
||||||
| **Semantic Tokens** | Keywords, variables highlighted |
|
| **Semantic Tokens** | Keywords, variables highlighted |
|
||||||
|
|||||||
+326
-2
@@ -61,6 +61,8 @@ local capabilities = {
|
|||||||
resolveProvider = false,
|
resolveProvider = false,
|
||||||
},
|
},
|
||||||
definitionProvider = true,
|
definitionProvider = true,
|
||||||
|
referencesProvider = true,
|
||||||
|
declarationProvider = true,
|
||||||
documentSymbolProvider = true,
|
documentSymbolProvider = true,
|
||||||
diagnosticProvider = { relatedDocuments = {} },
|
diagnosticProvider = { relatedDocuments = {} },
|
||||||
textDocumentSync = 1,
|
textDocumentSync = 1,
|
||||||
@@ -98,7 +100,7 @@ local function scan_scl_files(root_dir)
|
|||||||
if not root_dir or root_dir == "" then
|
if not root_dir or root_dir == "" then
|
||||||
return files
|
return files
|
||||||
end
|
end
|
||||||
local result = run_command("find " .. root_dir .. ' -type f -name "*.scl" 2>/dev/null')
|
local result = run_command('find "' .. root_dir .. '" -type f -name "*.scl" 2>/dev/null')
|
||||||
if result then
|
if result then
|
||||||
for f in result:gmatch("[^\n]+") do
|
for f in result:gmatch("[^\n]+") do
|
||||||
if f and f ~= "" then
|
if f and f ~= "" then
|
||||||
@@ -164,6 +166,45 @@ function handlers.shutdown(params)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local project_markers = {
|
||||||
|
"Program blocks",
|
||||||
|
"PLC data types",
|
||||||
|
"PLC tags",
|
||||||
|
"program blocks",
|
||||||
|
"plc data types",
|
||||||
|
"plc tags",
|
||||||
|
".git",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function find_project_root(start_path)
|
||||||
|
if not start_path or start_path == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local current = start_path
|
||||||
|
local depth = 0
|
||||||
|
while current and current ~= "/" and current ~= "" and depth < 20 do
|
||||||
|
local handle = io.popen('ls -a "' .. current .. '" 2>/dev/null')
|
||||||
|
if handle then
|
||||||
|
local entries = {}
|
||||||
|
for line in handle:lines() do
|
||||||
|
table.insert(entries, line)
|
||||||
|
end
|
||||||
|
handle:close()
|
||||||
|
local found_marker = false
|
||||||
|
for _, marker in ipairs(project_markers) do
|
||||||
|
for _, entry in ipairs(entries) do
|
||||||
|
if entry == marker then
|
||||||
|
return current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
current = current:match("(.*)/") or ""
|
||||||
|
depth = depth + 1
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
function handlers.exit(params)
|
function handlers.exit(params)
|
||||||
os.exit(0)
|
os.exit(0)
|
||||||
end
|
end
|
||||||
@@ -172,7 +213,8 @@ function handlers.textDocument_didOpen(params)
|
|||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local content = params.textDocument.text
|
local content = params.textDocument.text
|
||||||
local path = uri_to_path(uri)
|
local path = uri_to_path(uri)
|
||||||
local root_dir = path:match("(.*/)") or "."
|
local file_dir = path:match("(.*/)") or "."
|
||||||
|
local root_dir = find_project_root(file_dir) or file_dir
|
||||||
|
|
||||||
local variables, var_positions = parser.extract_variables(content)
|
local variables, var_positions = parser.extract_variables(content)
|
||||||
local functions = parser.extract_functions(content)
|
local functions = parser.extract_functions(content)
|
||||||
@@ -456,6 +498,287 @@ function handlers.textDocument_definition(params)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function handlers.textDocument_declaration(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 not word or word == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_location(doc_uri, doc_line, doc_col, name_len)
|
||||||
|
name_len = name_len or 0
|
||||||
|
return {
|
||||||
|
uri = doc_uri,
|
||||||
|
range = {
|
||||||
|
start = { line = doc_line, character = doc_col },
|
||||||
|
["end"] = { line = doc_line, character = doc_col + name_len },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local locations = {}
|
||||||
|
local seen_locations = {}
|
||||||
|
local search_names = { word }
|
||||||
|
|
||||||
|
local function add_location(loc)
|
||||||
|
local key = loc.uri .. ":" .. tostring(loc.range.start.line) .. ":" .. tostring(loc.range.start.character)
|
||||||
|
if not seen_locations[key] then
|
||||||
|
seen_locations[key] = true
|
||||||
|
table.insert(locations, loc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if doc.variables and doc.variables[word] then
|
||||||
|
local var_type = doc.variables[word].data_type
|
||||||
|
if var_type then
|
||||||
|
var_type = var_type:gsub("%s*;.*$", ""):gsub("%s*:=.*$", ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
local type_name = var_type:match('"([^"]+)"') or var_type:match("([%w_]+)")
|
||||||
|
if type_name and type_name ~= word then
|
||||||
|
table.insert(search_names, type_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for check_uri, check_doc in pairs(documents) do
|
||||||
|
if check_doc.functions then
|
||||||
|
for _, func in ipairs(check_doc.functions) do
|
||||||
|
for _, search_name in ipairs(search_names) do
|
||||||
|
if func.name == search_name then
|
||||||
|
add_location(make_location(check_uri, func.start, 0, #search_name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local root_dirs = {}
|
||||||
|
for _, d in pairs(documents) do
|
||||||
|
if d.root_dir and d.root_dir ~= "" then
|
||||||
|
root_dirs[d.root_dir] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for root_dir, _ in pairs(root_dirs) do
|
||||||
|
local scl_files = scan_scl_files(root_dir)
|
||||||
|
for _, filepath in ipairs(scl_files) do
|
||||||
|
local cached = workspace_symbol_cache[":file:" .. filepath]
|
||||||
|
if not cached then
|
||||||
|
local variables, functions, types = parse_scl_file(filepath)
|
||||||
|
cached = { functions = functions, types = types }
|
||||||
|
workspace_symbol_cache[":file:" .. filepath] = cached
|
||||||
|
end
|
||||||
|
|
||||||
|
if cached.functions then
|
||||||
|
for _, func in ipairs(cached.functions) do
|
||||||
|
for _, search_name in ipairs(search_names) do
|
||||||
|
if func.name == search_name then
|
||||||
|
local file_uri = path_to_uri(filepath)
|
||||||
|
add_location(make_location(file_uri, func.start, 0, #search_name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local handle = io.open(filepath, "r")
|
||||||
|
if handle then
|
||||||
|
local content = handle:read("*a")
|
||||||
|
handle:close()
|
||||||
|
for _, search_name in ipairs(search_names) do
|
||||||
|
local fb_pattern = 'FUNCTION_BLOCK%s+"' .. search_name .. '"'
|
||||||
|
local fb_plain = "^FUNCTION_BLOCK%s+" .. search_name .. "$"
|
||||||
|
if content:match(fb_pattern) or content:match(fb_plain) then
|
||||||
|
for line in content:gmatch("([^\n]*)\n") do
|
||||||
|
if line:match(fb_pattern) or line:match(fb_plain) then
|
||||||
|
local file_uri = path_to_uri(filepath)
|
||||||
|
add_location(make_location(file_uri, 0, 0, #search_name))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local db_pattern = 'DATA_BLOCK%s+"' .. search_name .. '"'
|
||||||
|
local db_plain = "^DATA_BLOCK%s+" .. search_name .. "$"
|
||||||
|
if content:match(db_pattern) or content:match(db_plain) then
|
||||||
|
for line in content:gmatch("([^\n]*)\n") do
|
||||||
|
if line:match(db_pattern) or line:match(db_plain) then
|
||||||
|
local file_uri = path_to_uri(filepath)
|
||||||
|
add_location(make_location(file_uri, 0, 0, #search_name))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if cached.types then
|
||||||
|
for type_name, type_info in pairs(cached.types) do
|
||||||
|
for _, search_name in ipairs(search_names) do
|
||||||
|
if type_name == search_name then
|
||||||
|
local file_uri = path_to_uri(filepath)
|
||||||
|
local start_line = type_info.start_line or 0
|
||||||
|
add_location(make_location(file_uri, start_line, 0, #search_name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for root_dir, _ in pairs(root_dirs) do
|
||||||
|
local udt_result = run_command('find "' .. root_dir .. '" -type f -name "*.udt" 2>/dev/null')
|
||||||
|
if udt_result then
|
||||||
|
for filepath in udt_result:gmatch("[^\n]+") do
|
||||||
|
if filepath and filepath ~= "" then
|
||||||
|
local handle = io.open(filepath, "r")
|
||||||
|
if handle then
|
||||||
|
local file_content = handle:read("*a")
|
||||||
|
handle:close()
|
||||||
|
for _, search_name in ipairs(search_names) do
|
||||||
|
local type_line = file_content:match('TYPE%s+"' .. search_name .. '"')
|
||||||
|
if not type_line then
|
||||||
|
type_line = file_content:match("TYPE%s+" .. search_name)
|
||||||
|
end
|
||||||
|
if type_line then
|
||||||
|
local line_num = 1
|
||||||
|
for line in file_content:gmatch("([^\n]*)\n?") do
|
||||||
|
if line:match('TYPE%s+"' .. search_name .. '"') or line:match("TYPE%s+" .. search_name) then
|
||||||
|
local file_uri = path_to_uri(filepath)
|
||||||
|
add_location(make_location(file_uri, line_num - 1, 0, #search_name))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
line_num = line_num + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local db_result = run_command('find "' .. root_dir .. '" -type f -name "*.db" 2>/dev/null')
|
||||||
|
if db_result then
|
||||||
|
for filepath in db_result:gmatch("[^\n]+") do
|
||||||
|
if filepath and filepath ~= "" then
|
||||||
|
local handle = io.open(filepath, "r")
|
||||||
|
if handle then
|
||||||
|
local file_content = handle:read("*a")
|
||||||
|
handle:close()
|
||||||
|
for _, search_name in ipairs(search_names) do
|
||||||
|
local db_line = file_content:match('DATA_BLOCK%s+"' .. search_name .. '"')
|
||||||
|
if not db_line then
|
||||||
|
db_line = file_content:match("DATA_BLOCK%s+" .. search_name)
|
||||||
|
end
|
||||||
|
if db_line then
|
||||||
|
local line_num = 1
|
||||||
|
for line in file_content:gmatch("([^\n]*)\n?") do
|
||||||
|
if line:match('DATA_BLOCK%s+"' .. search_name .. '"') or line:match("DATA_BLOCK%s+" .. search_name) then
|
||||||
|
local file_uri = path_to_uri(filepath)
|
||||||
|
add_location(make_location(file_uri, line_num - 1, 0, #search_name))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
line_num = line_num + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #locations > 0 then
|
||||||
|
return locations
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function handlers.textDocument_references(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 not word or word == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local references = {}
|
||||||
|
for line_num, line_content in ipairs(lines) do
|
||||||
|
local search_col = 1
|
||||||
|
while search_col <= #line_content do
|
||||||
|
local match_start, match_end = line_content:find("[%w_]+", search_col)
|
||||||
|
if not match_start then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
local matched_word = line_content:sub(match_start, match_end)
|
||||||
|
if matched_word == word then
|
||||||
|
table.insert(references, {
|
||||||
|
uri = uri,
|
||||||
|
range = {
|
||||||
|
start = { line = line_num - 1, character = match_start - 1 },
|
||||||
|
["end"] = { line = line_num - 1, character = match_end },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
search_col = match_end + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #references > 0 then
|
||||||
|
return references
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
function handlers.textDocument_documentSymbol(params)
|
function handlers.textDocument_documentSymbol(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -861,6 +1184,7 @@ local function handle_message(message)
|
|||||||
if message.method == "$/cancelRequest" then
|
if message.method == "$/cancelRequest" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Convert method name from "textDocument/didOpen" to "textDocument_didOpen"
|
-- Convert method name from "textDocument/didOpen" to "textDocument_didOpen"
|
||||||
local method_name = message.method:gsub("/", "_")
|
local method_name = message.method:gsub("/", "_")
|
||||||
local handler = handlers[method_name]
|
local handler = handlers[method_name]
|
||||||
|
|||||||
+101
-52
@@ -11,31 +11,35 @@ function M.extract_variables(content)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local in_var_section = false
|
local in_var_section = false
|
||||||
|
local in_struct_section = false
|
||||||
local var_type = "unknown"
|
local var_type = "unknown"
|
||||||
local current_block = nil
|
local current_block = nil
|
||||||
|
|
||||||
local function process_line(line, line_num)
|
local function process_line(line, line_num)
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
|
||||||
-- Detect block start
|
-- Detect block start (with or without quotes)
|
||||||
local fb_name = trimmed:match('FUNCTION_BLOCK%s+"([^"]+)"')
|
local fb_name = trimmed:match('FUNCTION_BLOCK%s+"([^"]+)"') or trimmed:match("^FUNCTION_BLOCK%s+([%w_]+)")
|
||||||
if fb_name then
|
if fb_name then
|
||||||
current_block = { name = fb_name, kind = "function_block" }
|
current_block = { name = fb_name, kind = "function_block" }
|
||||||
in_var_section = false
|
in_var_section = false
|
||||||
|
in_struct_section = false
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local ob_name = trimmed:match('ORGANIZATION_BLOCK%s+"([^"]+)"')
|
local ob_name = trimmed:match('ORGANIZATION_BLOCK%s+"([^"]+)"') or trimmed:match("^ORGANIZATION_BLOCK%s+([%w_]+)")
|
||||||
if ob_name then
|
if ob_name then
|
||||||
current_block = { name = ob_name, kind = "organization_block" }
|
current_block = { name = ob_name, kind = "organization_block" }
|
||||||
in_var_section = false
|
in_var_section = false
|
||||||
|
in_struct_section = false
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local fn_name = trimmed:match('FUNCTION%s+"([^"]+)"')
|
local fn_name = trimmed:match('FUNCTION%s+"([^"]+)"') or trimmed:match("^FUNCTION%s+([%w_]+)")
|
||||||
if fn_name then
|
if fn_name then
|
||||||
current_block = { name = fn_name, kind = "function" }
|
current_block = { name = fn_name, kind = "function" }
|
||||||
in_var_section = false
|
in_var_section = false
|
||||||
|
in_struct_section = false
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -44,6 +48,19 @@ function M.extract_variables(content)
|
|||||||
trimmed:match("^END_FUNCTION") then
|
trimmed:match("^END_FUNCTION") then
|
||||||
current_block = nil
|
current_block = nil
|
||||||
in_var_section = false
|
in_var_section = false
|
||||||
|
in_struct_section = false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle TYPE sections (for .udt files with STRUCT)
|
||||||
|
if trimmed:match("^TYPE") and (trimmed:match("^TYPE%s") or trimmed:match('^TYPE"') or trimmed == "TYPE") then
|
||||||
|
in_struct_section = true
|
||||||
|
var_type = "STRUCT_FIELD"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if trimmed:match("^END_TYPE") then
|
||||||
|
in_struct_section = false
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,6 +73,7 @@ function M.extract_variables(content)
|
|||||||
trimmed:match("^VAR[_ ]*RETAIN%s*$") or
|
trimmed:match("^VAR[_ ]*RETAIN%s*$") or
|
||||||
trimmed:match("^VAR[_ ]*NON_RETAIN%s*$") then
|
trimmed:match("^VAR[_ ]*NON_RETAIN%s*$") then
|
||||||
in_var_section = true
|
in_var_section = true
|
||||||
|
in_struct_section = false
|
||||||
if trimmed:match("INPUT") then var_type = "VAR_INPUT"
|
if trimmed:match("INPUT") then var_type = "VAR_INPUT"
|
||||||
elseif trimmed:match("OUTPUT") then var_type = "VAR_OUTPUT"
|
elseif trimmed:match("OUTPUT") then var_type = "VAR_OUTPUT"
|
||||||
elseif trimmed:match("IN_OUT") then var_type = "VAR_IN_OUT"
|
elseif trimmed:match("IN_OUT") then var_type = "VAR_IN_OUT"
|
||||||
@@ -72,7 +90,18 @@ function M.extract_variables(content)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if not in_var_section then return end
|
if trimmed:match("^STRUCT%s*$") then
|
||||||
|
in_struct_section = true
|
||||||
|
var_type = "STRUCT_FIELD"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if trimmed:match("^END_STRUCT%s*$") then
|
||||||
|
in_struct_section = false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not in_var_section and not in_struct_section then return end
|
||||||
|
|
||||||
if trimmed:match("^%(%*") or trimmed:match("^%s*//") then
|
if trimmed:match("^%(%*") or trimmed:match("^%s*//") then
|
||||||
return
|
return
|
||||||
@@ -96,7 +125,7 @@ function M.extract_variables(content)
|
|||||||
brace_depth = brace_depth - 1
|
brace_depth = brace_depth - 1
|
||||||
elseif c == ":" and brace_depth == 0 then
|
elseif c == ":" and brace_depth == 0 then
|
||||||
type_start = i
|
type_start = i
|
||||||
break -- Only take the first : outside braces
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local var_data_type = "unknown"
|
local var_data_type = "unknown"
|
||||||
@@ -141,70 +170,90 @@ function M.extract_types(content)
|
|||||||
local in_type_section = false
|
local in_type_section = false
|
||||||
local current_type = nil
|
local current_type = nil
|
||||||
local brace_depth = 0
|
local brace_depth = 0
|
||||||
|
local in_struct = false
|
||||||
|
local pending_type_name = nil
|
||||||
|
|
||||||
|
local function extract_type_name(after_colon)
|
||||||
|
if not after_colon then return nil end
|
||||||
|
local quoted = after_colon:match('^%s*"([^"]+)"')
|
||||||
|
if quoted then return quoted end
|
||||||
|
return after_colon:match("^%s*(%w+)")
|
||||||
|
end
|
||||||
|
|
||||||
for i, line in ipairs(lines) do
|
for i, line in ipairs(lines) do
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
|
||||||
if trimmed:match("^TYPE$") or trimmed:match("^TYPE%s+:=") then
|
local is_type_line = trimmed:match("^TYPE") and (trimmed:match("^TYPE%s") or trimmed:match('^TYPE"') or trimmed == "TYPE")
|
||||||
|
|
||||||
|
if is_type_line then
|
||||||
in_type_section = true
|
in_type_section = true
|
||||||
|
in_struct = false
|
||||||
brace_depth = 0
|
brace_depth = 0
|
||||||
|
pending_type_name = trimmed:match('^TYPE%s+"([^"]+)') or trimmed:match("^TYPE%s+(%w+)")
|
||||||
elseif in_type_section and trimmed:match("^END_TYPE") then
|
elseif in_type_section and trimmed:match("^END_TYPE") then
|
||||||
in_type_section = false
|
in_type_section = false
|
||||||
|
in_struct = false
|
||||||
current_type = nil
|
current_type = nil
|
||||||
|
pending_type_name = nil
|
||||||
elseif in_type_section then
|
elseif in_type_section then
|
||||||
local type_name = trimmed:match("^(%w+):")
|
local type_name_with_struct = trimmed:match("^(%w+)%s+.-:%s+STRUCT")
|
||||||
if type_name then
|
if type_name_with_struct then
|
||||||
local struct_start = trimmed:find("{")
|
current_type = {
|
||||||
local array_match = trimmed:find("ARRAY")
|
name = type_name_with_struct,
|
||||||
|
kind = "struct",
|
||||||
if struct_start then
|
fields = {},
|
||||||
|
start_line = i - 1,
|
||||||
|
}
|
||||||
|
types[type_name_with_struct] = current_type
|
||||||
|
in_struct = true
|
||||||
|
pending_type_name = nil
|
||||||
|
elseif trimmed:match("^STRUCT%s*$") then
|
||||||
|
if pending_type_name then
|
||||||
current_type = {
|
current_type = {
|
||||||
name = type_name,
|
name = pending_type_name,
|
||||||
kind = "struct",
|
kind = "struct",
|
||||||
fields = {},
|
fields = {},
|
||||||
start_line = i - 1,
|
start_line = i - 1,
|
||||||
}
|
}
|
||||||
types[type_name] = current_type
|
types[pending_type_name] = current_type
|
||||||
elseif array_match then
|
pending_type_name = nil
|
||||||
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
|
||||||
end
|
in_struct = true
|
||||||
|
elseif trimmed:match("^END_STRUCT%s*$") then
|
||||||
if current_type and current_type.kind == "struct" then
|
in_struct = false
|
||||||
for open in line:gmatch("{") do brace_depth = brace_depth + 1 end
|
elseif in_struct and current_type and current_type.fields then
|
||||||
for close in line:gmatch("}") do brace_depth = brace_depth - 1 end
|
local field_name = line:match("^%s*(%w+)%s*:%s*")
|
||||||
|
if field_name then
|
||||||
local field_name = line:match("^%s*(%w+):")
|
local after_colon = line:match(":%s*(.+)")
|
||||||
if field_name and field_name ~= current_type.name then
|
local field_type = after_colon and extract_type_name(after_colon)
|
||||||
local field_type = line:match(":%s*(%w+)")
|
|
||||||
table.insert(current_type.fields, {
|
table.insert(current_type.fields, {
|
||||||
name = field_name,
|
name = field_name,
|
||||||
type = field_type or "unknown",
|
type = field_type or "unknown",
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
elseif not in_struct and not pending_type_name then
|
||||||
if brace_depth == 0 then
|
local array_match = trimmed:match("^(%w+)%s+:%s+ARRAY")
|
||||||
current_type = nil
|
if array_match then
|
||||||
|
local of_type = trimmed:match("OF%s+(%w+)")
|
||||||
|
current_type = {
|
||||||
|
name = array_match,
|
||||||
|
kind = "array",
|
||||||
|
element_type = of_type,
|
||||||
|
start_line = i - 1,
|
||||||
|
}
|
||||||
|
types[array_match] = current_type
|
||||||
|
else
|
||||||
|
local alias_match = trimmed:match("^(%w+)%s+:%s+(%w+)")
|
||||||
|
if alias_match then
|
||||||
|
local base_type = extract_type_name(trimmed:match(":%s+(.+)$"))
|
||||||
|
current_type = {
|
||||||
|
name = alias_match,
|
||||||
|
kind = "alias",
|
||||||
|
base_type = base_type,
|
||||||
|
start_line = i - 1,
|
||||||
|
}
|
||||||
|
types[alias_match] = current_type
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -239,7 +288,7 @@ function M.extract_functions(content)
|
|||||||
end
|
end
|
||||||
|
|
||||||
for i, line in ipairs(lines) do
|
for i, line in ipairs(lines) do
|
||||||
local func_name = line:match('FUNCTION%s+"([^"]+)"')
|
local func_name = line:match('FUNCTION%s+"([^"]+)"') or line:match("^FUNCTION%s+([%w_]+)")
|
||||||
if func_name then
|
if func_name then
|
||||||
local end_line = find_block_end(i, "END_FUNCTION", { "END_FUNCTION" })
|
local end_line = find_block_end(i, "END_FUNCTION", { "END_FUNCTION" })
|
||||||
table.insert(functions, {
|
table.insert(functions, {
|
||||||
@@ -250,7 +299,7 @@ function M.extract_functions(content)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local fb_name = line:match('FUNCTION_BLOCK%s+"([^"]+)"')
|
local fb_name = line:match('FUNCTION_BLOCK%s+"([^"]+)"') or line:match("^FUNCTION_BLOCK%s+([%w_]+)")
|
||||||
if fb_name then
|
if fb_name then
|
||||||
local end_line = find_block_end(i, "END_FUNCTION_BLOCK", { "END_FUNCTION_BLOCK" })
|
local end_line = find_block_end(i, "END_FUNCTION_BLOCK", { "END_FUNCTION_BLOCK" })
|
||||||
table.insert(functions, {
|
table.insert(functions, {
|
||||||
@@ -261,7 +310,7 @@ function M.extract_functions(content)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local ob_name = line:match('ORGANIZATION_BLOCK%s+"([^"]+)"')
|
local ob_name = line:match('ORGANIZATION_BLOCK%s+"([^"]+)"') or line:match("^ORGANIZATION_BLOCK%s+([%w_]+)")
|
||||||
if ob_name then
|
if ob_name then
|
||||||
local end_line = find_block_end(i, "END_ORGANIZATION_BLOCK", { "END_ORGANIZATION_BLOCK" })
|
local end_line = find_block_end(i, "END_ORGANIZATION_BLOCK", { "END_ORGANIZATION_BLOCK" })
|
||||||
table.insert(functions, {
|
table.insert(functions, {
|
||||||
|
|||||||
@@ -198,6 +198,21 @@ function M.load_types_from_workspace(root_dir)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Also scan for .udt files
|
||||||
|
local udt_files = scan_files_recursive(root_dir, "*.udt")
|
||||||
|
for _, filepath in ipairs(udt_files) do
|
||||||
|
local content = run_command("cat " .. filepath:gsub(" ", "\\ "))
|
||||||
|
if content then
|
||||||
|
local udt_types = parse_scl_type_file(content)
|
||||||
|
for name, typ in pairs(udt_types) do
|
||||||
|
typ.source = "udt_file"
|
||||||
|
if not types[name] then
|
||||||
|
types[name] = typ
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return types
|
return types
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user