Files
tia-lsp.nvim/lua/scl/db_parser.lua
T
lazar 7db896b84c feat: Add Global Data Block recognition in workspace scope
- Add db_parser.lua to parse .db files (DATA_BLOCK with VAR sections)
- Update workspace_types.lua to scan for .db files in project
- Update blink_cmp_source.lua to provide DB auto-completion:
  - DB names available in general completion (without quotes)
  - Type quote to get DB members
  - Multi-level nested member access support
- Add DB count to workspace commands
- Update README with Global DB documentation
2026-02-17 15:06:36 +01:00

400 lines
9.2 KiB
Lua

-- DB Parser for SCL
-- Parses .db files to extract global data block definitions and member structures
local M = {}
local db_cache = {}
local inline_dbs = {}
function M.register_inline_db(db)
if db and db.name then
inline_dbs[db.name] = db
db_cache[db.name] = db
end
end
function M.parse_inline_db_comment(comment_lines)
local db_name = nil
local members = {}
for _, line in ipairs(comment_lines) do
local name = line:match("@db%s+(%w+)")
if name then
db_name = name
break
end
end
if not db_name then
return nil
end
for _, line in ipairs(comment_lines) do
local name, type_str = line:match("^%s*([%w_]+)%s*:%s*([^;]+)")
if name and type_str then
type_str = type_str:gsub("%s*:=.*$", ""):gsub("%s*$", "")
local is_udt = type_str:match('^".+"$') ~= nil
local clean_type = type_str:gsub('"', '')
table.insert(members, {
name = name,
type = clean_type,
raw_type = type_str,
attributes = {},
comment = "",
is_udt = is_udt
})
end
end
if #members == 0 then
return nil
end
return {
name = db_name,
title = "Inline DB",
version = "0.1",
comment = "",
members = members
}
end
function M.clear_cache()
for k in pairs(db_cache) do
db_cache[k] = nil
end
for k in pairs(inline_dbs) do
inline_dbs[k] = nil
end
end
function M.get_db(db_name)
return db_cache[db_name]
end
function M.get_all_db_names()
local names = {}
for name, _ in pairs(db_cache) do
table.insert(names, name)
end
return names
end
function M.get_cache_count()
local count = 0
for _ in pairs(db_cache) do
count = count + 1
end
return count
end
function M.is_db_type(type_name)
if not type_name then
return false
end
local clean_name = type_name:gsub('"', '')
return db_cache[clean_name] ~= nil or inline_dbs[clean_name] ~= nil
end
local function parse_attributes(attr_str)
if not attr_str or attr_str == "" then
return {}
end
local attrs = {}
for key, value in attr_str:gmatch("(%w+)%s*:=%s*'([^']+)'") do
attrs[key] = value
end
for key, value in attr_str:gmatch("(%w+)%s*:=%s*(%w+)") do
attrs[key] = value
end
return attrs
end
function M.parse_db_content(content, filename)
local db = {
filename = filename,
members = {}
}
local db_name = content:match('DATA_BLOCK%s+"([^"]+)"')
if not db_name then
return nil, "No DATA_BLOCK definition found"
end
db.name = db_name
db.title = content:match('TITLE%s*=%s*([^\n]+)') or ""
db.title = db.title:gsub("^%s*", ""):gsub("%s*$", "")
db.version = content:match('VERSION%s*:%s*([0-9.]+)') or "0.1"
local comment_lines = {}
local in_header = true
for line in content:gmatch("[^\r\n]+") do
if in_header then
local comment = line:match("^%s*//%s*(.+)$")
if comment then
table.insert(comment_lines, comment)
elseif line:match("^%s*VAR") then
in_header = false
end
end
end
db.comment = table.concat(comment_lines, " ")
local all_var_content = {}
local var_section_pattern = "VAR%s*(.-)%s*END_VAR"
for var_section in content:gmatch(var_section_pattern) do
table.insert(all_var_content, var_section)
end
for _, var_content in ipairs(all_var_content) do
for line in var_content:gmatch("[^\r\n]+") do
line = line:gsub("^%s*", ""):gsub("%s*$", "")
if line ~= "" and not line:match("^//") then
local comment = line:match("//%s*(.+)$") or ""
line = line:gsub("//.*$", ""):gsub("%s*$", "")
local name, attrs_str, type_str = line:match('^([%w_]+)%s*(%b{})%s*:%s*([^;]+)')
if not name then
name, type_str = line:match('^([%w_]+)%s*:%s*([^;]+)')
end
if name and type_str then
type_str = type_str:gsub("%s*:=.*$", ""):gsub("%s*$", "")
local is_udt = type_str:match('^".+"$') ~= nil
local clean_type = type_str:gsub('"', '')
local member = {
name = name,
type = clean_type,
raw_type = type_str,
attributes = attrs_str and parse_attributes(attrs_str) or {},
comment = comment,
is_udt = is_udt
}
table.insert(db.members, member)
end
end
end
end
return db
end
function M.parse_db_file(filepath)
local file = io.open(filepath, "r")
if not file then
return nil, "Cannot open file: " .. filepath
end
local content = file:read("*all")
file:close()
local db, err = M.parse_db_content(content, filepath)
if not db then
return nil, err
end
db_cache[db.name] = db
return db
end
function M.get_db_members(db_name, visited)
visited = visited or {}
if not db_name then
return {}
end
db_name = db_name:gsub('"', '')
if visited[db_name] then
return {}
end
visited[db_name] = true
local db = db_cache[db_name]
if not db then
return {}
end
local members = {}
for _, member in ipairs(db.members) do
table.insert(members, member)
if member.is_udt and member.type then
member.nested_members = M.get_db_members(member.type, visited)
end
end
return members
end
function M.resolve_db_member_path(db_name, path)
if not db_name or not path then
return nil, path or ""
end
local parts = {}
for part in path:gmatch("[^.]+") do
table.insert(parts, part)
end
local current_db_name = db_name:gsub('"', '')
local current_member = nil
for i, part in ipairs(parts) do
local db = db_cache[current_db_name]
if not db then
return nil, table.concat(parts, ".", i)
end
local found = false
for _, member in ipairs(db.members) do
if member.name == part then
current_member = member
if member.is_udt then
current_db_name = member.type
else
if i < #parts then
return nil, table.concat(parts, ".", i + 1)
end
end
found = true
break
end
end
if not found then
return nil, table.concat(parts, ".", i)
end
end
return current_member, ""
end
function M.get_db_member_completions(db_name, prefix)
local members = M.get_db_members(db_name)
local items = {}
for _, member in ipairs(members) do
local label = prefix and (prefix .. "." .. member.name) or member.name
local item = {
label = label,
insertText = member.name,
kind = member.is_udt and vim.lsp.protocol.CompletionItemKind.Struct or vim.lsp.protocol.CompletionItemKind.Field,
detail = member.raw_type .. (member.comment ~= "" and " - " .. member.comment or ""),
documentation = member.comment,
}
table.insert(items, item)
end
return items
end
function M.get_db_type_completions(params)
local items = {}
for name, db in pairs(db_cache) do
local quoted_name = '"' .. name .. '"'
local item = {
label = quoted_name,
kind = vim.lsp.protocol.CompletionItemKind.Struct,
detail = db.title ~= "" and db.title or "Global DB",
documentation = db.comment ~= "" and db.comment or nil,
}
if params then
local line = params.context.cursor_before_line
local col = params.context.cursor.col
local start_col = col
for i = col, 1, -1 do
local char = line:sub(i, i)
if char == '"' then
start_col = i - 1
break
elseif char == ":" then
start_col = col - 1
break
end
end
item.textEdit = {
newText = quoted_name,
range = {
start = { line = params.context.cursor.line, character = start_col },
["end"] = { line = params.context.cursor.line, character = col },
},
}
else
item.insertText = quoted_name
end
table.insert(items, item)
end
return items
end
function M.scan_buffer_for_inline_dbs(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local current_comment_block = {}
local in_scl_file = vim.bo[bufnr].filetype == "scl"
if not in_scl_file then
return
end
for _, line in ipairs(lines) do
if line:match("^%s*//") then
local content = line:gsub("^%s*//%s*", "")
table.insert(current_comment_block, content)
else
if #current_comment_block > 0 then
local db = M.parse_inline_db_comment(current_comment_block)
if db then
M.register_inline_db(db)
end
current_comment_block = {}
end
end
end
if #current_comment_block > 0 then
local db = M.parse_inline_db_comment(current_comment_block)
if db then
M.register_inline_db(db)
end
end
end
function M.debug_print_cache()
print("=== DB Cache Contents ===")
print("Total DBs: " .. M.get_cache_count())
for name, db in pairs(db_cache) do
print(string.format("DB: %s (v%s)", name, db.version))
print(string.format(" Title: %s", db.title))
print(string.format(" File: %s", db.filename or "unknown"))
print(string.format(" Members: %d", #db.members))
for _, member in ipairs(db.members) do
print(string.format(" - %s: %s%s",
member.name,
member.raw_type,
member.is_udt and " (UDT)" or ""))
end
print("")
end
end
return M