From 7db896b84c930f92b68d5da156c62fb710802718 Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Tue, 17 Feb 2026 15:06:36 +0100 Subject: [PATCH] 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 --- README.md | 37 +++- lua/scl/blink_cmp_source.lua | 203 +++++++++++++++++- lua/scl/db_parser.lua | 399 +++++++++++++++++++++++++++++++++++ lua/scl/init.lua | 10 +- lua/scl/workspace_types.lua | 121 ++++++++++- 5 files changed, 759 insertions(+), 11 deletions(-) create mode 100644 lua/scl/db_parser.lua diff --git a/README.md b/README.md index 7fa2ed0..bc25a38 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP) | **Auto-Prefix** | Automatically adds `#` prefix to local variables (like TIA Portal) | | **Multiline Parameters** | Fill FB/Function call parameters across multiple lines with `mp` or `:SCLMultilineParams` | | **Tab Navigation** | Jump between parameters with `` in insert mode | -| **Workspace UDT Scanner** | Scans project for user-defined types | +| **Workspace UDT Scanner** | Scans project for user-defined types (.udt files) | +| **Workspace Global DB Scanner** | Scans project for global data blocks (.db files) | | **blink.cmp Integration** | Smart completion source for blink.cmp | ## Installation @@ -109,8 +110,8 @@ require("scl_lsp").setup({ | Command | Description | |---------|-------------| | `:SCLShowVariables` | Show local variables in current file | -| `:SCLShowWorkspaceTypes` | Show workspace UDTs and FB count | -| `:SCLRescanWorkspaceTypes` | Rescan workspace for types | +| `:SCLShowWorkspaceTypes` | Show workspace UDTs, FBs, and Global DBs count | +| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs | | `:SCLPrefixWord` | Manually prefix current word with `#` | | `:SCLMultilineParams` | Fill multiline parameters for FB/Function call | | `:LspSCLFormat` | Format current SCL file | @@ -155,7 +156,34 @@ The LSP automatically scans for SCL files in `data_types/` folder and extracts T lua /home/lazar/dev/scl_lsp/src/plc_json.lua --generate ``` -## Project Structure +## Global Data Block Support + +The plugin automatically scans for `.db` files in the workspace and provides auto-completion for global data blocks and their members. + +### How it works + +1. **Workspace Scanning**: When opening a `.scl` file, the plugin scans the project for `.db` files +2. **DB Parsing**: Parses `DATA_BLOCK` definitions and extracts members from `VAR` sections +3. **Auto-completion**: Provides smart completion for DB names and members + +### Usage + +- Type a space or `#` to trigger completion - global DB names appear alongside local variables +- Type `"` followed by a DB name to get member completions +- Access nested members: `"MyDB".member.submember` + +### Example + +```scl +// Access global DB members +"HmiData".units +"Parameter".machine.unit100.paramTimeStampApplied + +// Inside FB call parameter +#instParameterManager(hmiInterface:="HmiData".units, ...) +``` + +### Project Structure ``` scl_lsp/ @@ -187,6 +215,7 @@ scl_lsp/ ├── init.lua ├── auto_prefix.lua ├── blink_cmp_source.lua + ├── db_parser.lua ├── fb_parser.lua ├── udt_parser.lua ├── variables.lua diff --git a/lua/scl/blink_cmp_source.lua b/lua/scl/blink_cmp_source.lua index 1286a8e..4750fe0 100644 --- a/lua/scl/blink_cmp_source.lua +++ b/lua/scl/blink_cmp_source.lua @@ -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 diff --git a/lua/scl/db_parser.lua b/lua/scl/db_parser.lua new file mode 100644 index 0000000..5a2013e --- /dev/null +++ b/lua/scl/db_parser.lua @@ -0,0 +1,399 @@ +-- 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 diff --git a/lua/scl/init.lua b/lua/scl/init.lua index 64c7571..d24dac9 100644 --- a/lua/scl/init.lua +++ b/lua/scl/init.lua @@ -138,27 +138,33 @@ function M.show_workspace_types() local ws = require("scl.workspace_types") local udt = require("scl.udt_parser") local fb = require("scl.fb_parser") + local db = require("scl.db_parser") local root = ws.get_project_root() local udt_names = udt.get_all_udt_names() local fb_names = fb.get_all_fb_names() + local db_names = db.get_all_db_names() if not root then vim.notify("No project root detected", vim.log.levels.WARN) return end - vim.notify("Project: " .. root .. "\nUDTs: " .. #udt_names .. "\nFBs/Functions: " .. #fb_names, vim.log.levels.INFO) + vim.notify("Project: " .. root .. "\nUDTs: " .. #udt_names .. "\nFBs/Functions: " .. #fb_names .. "\nGlobal DBs: " .. #db_names, vim.log.levels.INFO) end function M.rescan_workspace_types() local ws = require("scl.workspace_types") local udt = require("scl.udt_parser") local fb = require("scl.fb_parser") + local db = require("scl.db_parser") udt.clear_cache() fb.clear_cache() + db.clear_cache() ws.clear_root_cache() local udt_result = ws.scan_project_udts() local fb_result = ws.scan_project_fbs() + local db_result = ws.scan_project_dbs() vim.notify("Scanned UDTs: " .. udt_result.parsed_count .. "/" .. udt_result.total_files .. - ", FBs: " .. fb_result.parsed_count .. "/" .. fb_result.total_files, vim.log.levels.INFO) + ", FBs: " .. fb_result.parsed_count .. "/" .. fb_result.total_files .. + ", DBs: " .. db_result.parsed_count .. "/" .. db_result.total_files, vim.log.levels.INFO) end function M.create_commands() diff --git a/lua/scl/workspace_types.lua b/lua/scl/workspace_types.lua index ca20dc6..e955371 100644 --- a/lua/scl/workspace_types.lua +++ b/lua/scl/workspace_types.lua @@ -9,10 +9,11 @@ local stat = vim.loop.fs_stat -- Configuration local config = { enabled = true, - project_root = nil, -- nil = auto-detect - library_paths = {}, -- Additional paths to scan for UDTs (e.g., shared libraries) + project_root = nil, + library_paths = {}, type_extensions = { "udt" }, fb_extensions = { "scl" }, + db_extensions = { "db" }, scan_depth = 10, -- Folder names that indicate project root (case-insensitive) project_markers = { @@ -237,6 +238,52 @@ local function scan_for_fbs(dir, depth, results) return results end +-- Scan directory recursively for Global DB files +local function scan_for_dbs(dir, depth, results) + results = results or {} + depth = depth or 0 + + if depth > config.scan_depth then + return results + end + + local handle = scan(dir) + if not handle then + return results + end + + while true do + local name, type = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + local full_path = dir .. "/" .. name + + if type == "directory" then + if not name:match("^%.") and + name ~= "node_modules" and + name ~= ".git" and + name ~= "__pycache__" then + scan_for_dbs(full_path, depth + 1, results) + end + elseif type == "file" then + local ext = name:match("%.([^.]+)$") + if ext then + ext = ext:lower() + for _, allowed_ext in ipairs(config.db_extensions) do + if ext == allowed_ext:lower() then + table.insert(results, full_path) + break + end + end + end + end + end + + return results +end + -- Get all UDT files in project function M.get_project_udt_files(bufnr) local root = M.get_project_root(bufnr) @@ -287,6 +334,31 @@ function M.get_library_fb_files() return all_files end +-- Get all Global DB files in project +function M.get_project_db_files(bufnr) + local root = M.get_project_root(bufnr) + if not root then + return {} + end + + return scan_for_dbs(root, 0) +end + +-- Get Global DB files from library paths +function M.get_library_db_files() + local all_files = {} + for _, lib_path in ipairs(config.library_paths) do + local expanded = vim.fn.expand(lib_path) + if vim.fn.isdirectory(expanded) == 1 then + local files = scan_for_dbs(expanded, 0) + for _, f in ipairs(files) do + table.insert(all_files, f) + end + end + end + return all_files +end + -- Parse all UDT files in project and libraries function M.scan_project_udts(bufnr) local udt_parser = require("scl.udt_parser") @@ -347,6 +419,35 @@ function M.scan_project_fbs(bufnr) } end +-- Parse all Global DB files in project and libraries +function M.scan_project_dbs(bufnr) + local db_parser = require("scl.db_parser") + local files = M.get_project_db_files(bufnr) + local lib_files = M.get_library_db_files() + + for _, f in ipairs(lib_files) do + table.insert(files, f) + end + + local parsed_count = 0 + local errors = {} + + for _, filepath in ipairs(files) do + local db, err = db_parser.parse_db_file(filepath) + if db then + parsed_count = parsed_count + 1 + else + table.insert(errors, string.format("%s: %s", filepath, err or "unknown error")) + end + end + + return { + parsed_count = parsed_count, + total_files = #files, + errors = errors, + } +end + -- Setup workspace scanner function M.setup(opts) opts = opts or {} @@ -367,6 +468,9 @@ function M.setup(opts) if opts.project_markers then config.project_markers = opts.project_markers end + if opts.db_extensions then + config.db_extensions = opts.db_extensions + end config.enabled = opts.workspace_types ~= false @@ -384,12 +488,15 @@ function M.setup(opts) -- Clear cache and rescan local udt_parser = require("scl.udt_parser") local fb_parser = require("scl.fb_parser") + local db_parser = require("scl.db_parser") udt_parser.clear_cache() fb_parser.clear_cache() + db_parser.clear_cache() M.clear_root_cache() M.scan_project_udts(args.buf) M.scan_project_fbs(args.buf) + M.scan_project_dbs(args.buf) end, }) @@ -414,6 +521,16 @@ function M.setup(opts) fb_parser.parse_fb_file(args.file) end, }) + + -- Watch for Global DB .db file changes + vim.api.nvim_create_autocmd({ "BufWritePost", "BufNewFile" }, { + group = augroup, + pattern = "*.db", + callback = function(args) + local db_parser = require("scl.db_parser") + db_parser.parse_db_file(args.file) + end, + }) end return M