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
This commit is contained in:
2026-02-17 15:06:36 +01:00
parent a3ea6e6c0a
commit 7db896b84c
5 changed files with 759 additions and 11 deletions
+200 -3
View File
@@ -8,7 +8,7 @@ function M.new(opts)
end
function M:get_trigger_characters()
return { ".", "(", "#", " " }
return { ".", "(", "#", " ", '"' }
end
function M.is_available(context)
@@ -34,8 +34,8 @@ function M.is_available(context)
local prev_char = col > 1 and line:sub(col - 1, col - 1) or ""
-- Only trigger on our specific characters
if char_at == "." or char_at == "(" or char_at == "#" or
prev_char == "." or prev_char == "(" or prev_char == "#" then
if char_at == "." or char_at == "(" or char_at == "#" or char_at == '"' or
prev_char == "." or prev_char == "(" or prev_char == "#" or prev_char == '"' then
return true
end
end
@@ -155,6 +155,167 @@ function M:get_completions(context, callback)
return
end
-- Check for Global DB completion (quoted DB names like "HmiData")
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
-- Check for "DBName" pattern (quoted DB name access with dot)
local db_member_pattern = trimmed:match('"([%w_]+)"%.([%w_]*)')
-- Check if we're typing a quoted DB name (after " but before closing ")
local is_in_db_name = trimmed:match('"[%w_]*$') ~= nil
-- Check for partial DB name like "HmiData (unclosed quote)
local partial_db_match = trimmed:match('"([%w_]+)$')
local is_partial_db = partial_db_match ~= nil and trimmed:match('"[^"]*$') ~= nil
-- Check if after a colon followed by quote (type declaration context)
local is_after_colon_quote = trimmed:match(':%s*"$') ~= nil
-- Check if cursor is right after a closed quoted DB name (before dot)
local after_closed_db = trimmed:match('"([%w_]+)"%s*%.') ~= nil
if db_member_pattern or after_closed_db then
local db_parser = require("scl.db_parser")
local db_name = trimmed:match('"([%w_]+)"')
if db_name then
local db = db_parser.get_db(db_name)
if db then
local path = trimmed:match('"[^"]+%"%.([%w_.]*)')
-- Resolve nested path through DB members
local current_type = nil
local current_members = db_parser.get_db_members(db_name)
local resolved_path = ""
if path and path ~= "" then
-- Parse the path parts
local path_parts = {}
for part in path:gmatch("[^.]+") do
table.insert(path_parts, part)
end
-- Resolve each part of the path
local found = true
for i, part in ipairs(path_parts) do
local found_member = nil
for _, m in ipairs(current_members) do
if m.name == part then
found_member = m
break
end
end
if found_member then
resolved_path = part
if found_member.is_udt then
-- This member is a UDT type, continue resolving
local udt_parser = require("scl.udt_parser")
current_members = udt_parser.get_udt_members(found_member.type)
current_type = found_member.type
else
-- This is a basic type, can't continue
if i < #path_parts then
-- There are more parts but this isn't a UDT
found = false
break
end
current_type = found_member.type
current_members = {}
end
else
found = false
break
end
end
if not found then
wrapped_callback({ items = {} })
return
end
end
-- If we have a resolved path and it's not complete (ends with .), show next level members
if path and path ~= "" and path:match("%.$") then
-- User wants members of the last type
elseif path and path ~= "" and not path:match("%.$") then
-- User is typing a member name, don't show completions yet
wrapped_callback({ items = {} })
return
end
-- Get members to show (either from DB or from resolved UDT type)
if current_type then
local udt_parser = require("scl.udt_parser")
current_members = udt_parser.get_udt_members(current_type)
end
if #current_members > 0 then
local items = {}
for _, m in ipairs(current_members) do
table.insert(items, {
label = m.name,
kind = m.is_udt and vim.lsp.protocol.CompletionItemKind.Struct or vim.lsp.protocol.CompletionItemKind.Field,
detail = m.raw_type,
})
end
local wrapped_callback = vim.schedule_wrap(function(response)
callback({
items = response.items,
is_incomplete_backward = false,
is_incomplete_forward = false,
})
end)
wrapped_callback({ items = items })
return
end
end
end
end
-- Check for just " pattern (start of DB name completion or partial DB name)
if is_in_db_name or is_partial_db or is_after_colon_quote then
local db_parser = require("scl.db_parser")
-- If cache is empty, try to scan workspace for DBs
if db_parser.get_cache_count() == 0 then
local ws = require("scl.workspace_types")
ws.scan_project_dbs(bufnr)
end
local db_names = db_parser.get_all_db_names()
if #db_names > 0 then
local items = {}
local partial_name = partial_db_match or ""
for _, name in ipairs(db_names) do
-- Filter by partial match if user is typing
if partial_name == "" or name:lower():find(partial_name:lower(), 1, true) == 1 then
table.insert(items, {
label = name, -- Show without quotes in popup
insertText = name, -- Don't include quotes - user already typed opening "
kind = vim.lsp.protocol.CompletionItemKind.Struct,
detail = "Global DB",
-- Custom field to identify this as a DB that needs quote wrapping
custom = { is_global_db = true },
})
end
end
if #items > 0 then
local wrapped_callback = vim.schedule_wrap(function(response)
callback({
items = response.items,
is_incomplete_backward = false,
is_incomplete_forward = false,
})
end)
wrapped_callback({ items = items })
return
end
end
end
local scl_vars = require("scl.variables")
local variables = scl_vars.extract_variables(bufnr)
@@ -527,6 +688,7 @@ function M:get_completions(context, callback)
vim.schedule(function()
local udt_parser = require("scl.udt_parser")
local db_parser = require("scl.db_parser")
-- No member access = show local variables, UDT types, and basic types
if not base_var then
@@ -555,6 +717,29 @@ function M:get_completions(context, callback)
for _, name in ipairs(udt_parser.get_all_udt_names()) do
table.insert(items, { label = '"' .. name .. '"', kind = 14 })
end
-- Add Global DB names (without quotes - will be wrapped in resolve)
local db_parser = require("scl.db_parser")
if db_parser.get_cache_count() == 0 then
local ws = require("scl.workspace_types")
ws.scan_project_dbs(bufnr)
end
-- Check if cursor is after a quote (user typed " first)
local line_before_cursor = line:sub(1, col)
local is_after_quote = line_before_cursor:match('"%s*$') ~= nil
for _, name in ipairs(db_parser.get_all_db_names()) do
-- If user already typed opening quote, don't add quotes. Otherwise wrap in quotes.
local insert_name = is_after_quote and name or '"' .. name .. '"'
table.insert(items, {
label = name, -- No quotes in label
insertText = insert_name,
kind = vim.lsp.protocol.CompletionItemKind.Struct,
detail = "Global DB",
})
end
-- Add basic types
for _, t in ipairs({ "Bool", "Int", "DInt", "Real", "Word", "DWord", "Byte", "Time", "String", "SINT", "USINT", "UINT", "LREAL" }) do
table.insert(items, { label = t, kind = 19 })
@@ -588,6 +773,18 @@ function M:get_completions(context, callback)
for _, name in ipairs(udt_parser.get_all_udt_names()) do
table.insert(items, { label = '"' .. name .. '"', kind = 14 })
end
-- Add Global DB names (without quotes - will be wrapped in resolve)
local line_before_cursor_fallback = line:sub(1, col)
local is_after_quote_fallback = line_before_cursor_fallback:match('"%s*$') ~= nil
for _, name in ipairs(db_parser.get_all_db_names()) do
local insert_name = is_after_quote_fallback and name or '"' .. name .. '"'
table.insert(items, {
label = name,
insertText = insert_name,
kind = vim.lsp.protocol.CompletionItemKind.Struct,
detail = "Global DB",
})
end
for _, t in ipairs({ "Bool", "Int", "DInt", "Real", "Word", "DWord", "Byte", "Time", "String", "SINT", "USINT", "UINT", "LREAL" }) do
table.insert(items, { label = t, kind = 19 })
end