Merge branch 'feat/db_workspace_scope_recognition' into develop

This commit is contained in:
2026-02-17 15:22:05 +01:00
5 changed files with 759 additions and 11 deletions
+33 -4
View File
@@ -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) | | **Auto-Prefix** | Automatically adds `#` prefix to local variables (like TIA Portal) |
| **Multiline Parameters** | Fill FB/Function call parameters across multiple lines with `<Space>mp` or `:SCLMultilineParams` | | **Multiline Parameters** | Fill FB/Function call parameters across multiple lines with `<Space>mp` or `:SCLMultilineParams` |
| **Tab Navigation** | Jump between parameters with `<Tab>` in insert mode | | **Tab Navigation** | Jump between parameters with `<Tab>` 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 | | **blink.cmp Integration** | Smart completion source for blink.cmp |
## Installation ## Installation
@@ -109,8 +110,8 @@ require("scl_lsp").setup({
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `:SCLShowVariables` | Show local variables in current file | | `:SCLShowVariables` | Show local variables in current file |
| `:SCLShowWorkspaceTypes` | Show workspace UDTs and FB count | | `:SCLShowWorkspaceTypes` | Show workspace UDTs, FBs, and Global DBs count |
| `:SCLRescanWorkspaceTypes` | Rescan workspace for types | | `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs |
| `:SCLPrefixWord` | Manually prefix current word with `#` | | `:SCLPrefixWord` | Manually prefix current word with `#` |
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call | | `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
| `:LspSCLFormat` | Format current SCL file | | `: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 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/ scl_lsp/
@@ -187,6 +215,7 @@ scl_lsp/
├── init.lua ├── init.lua
├── auto_prefix.lua ├── auto_prefix.lua
├── blink_cmp_source.lua ├── blink_cmp_source.lua
├── db_parser.lua
├── fb_parser.lua ├── fb_parser.lua
├── udt_parser.lua ├── udt_parser.lua
├── variables.lua ├── variables.lua
+200 -3
View File
@@ -8,7 +8,7 @@ function M.new(opts)
end end
function M:get_trigger_characters() function M:get_trigger_characters()
return { ".", "(", "#", " " } return { ".", "(", "#", " ", '"' }
end end
function M.is_available(context) 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 "" local prev_char = col > 1 and line:sub(col - 1, col - 1) or ""
-- Only trigger on our specific characters -- Only trigger on our specific characters
if char_at == "." or char_at == "(" or char_at == "#" or if char_at == "." or char_at == "(" or char_at == "#" or char_at == '"' or
prev_char == "." or prev_char == "(" or prev_char == "#" then prev_char == "." or prev_char == "(" or prev_char == "#" or prev_char == '"' then
return true return true
end end
end end
@@ -155,6 +155,167 @@ function M:get_completions(context, callback)
return return
end 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 scl_vars = require("scl.variables")
local variables = scl_vars.extract_variables(bufnr) local variables = scl_vars.extract_variables(bufnr)
@@ -527,6 +688,7 @@ function M:get_completions(context, callback)
vim.schedule(function() vim.schedule(function()
local udt_parser = require("scl.udt_parser") 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 -- No member access = show local variables, UDT types, and basic types
if not base_var then 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 for _, name in ipairs(udt_parser.get_all_udt_names()) do
table.insert(items, { label = '"' .. name .. '"', kind = 14 }) table.insert(items, { label = '"' .. name .. '"', kind = 14 })
end 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 -- Add basic types
for _, t in ipairs({ "Bool", "Int", "DInt", "Real", "Word", "DWord", "Byte", "Time", "String", "SINT", "USINT", "UINT", "LREAL" }) do 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 }) 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 for _, name in ipairs(udt_parser.get_all_udt_names()) do
table.insert(items, { label = '"' .. name .. '"', kind = 14 }) table.insert(items, { label = '"' .. name .. '"', kind = 14 })
end 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 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 }) table.insert(items, { label = t, kind = 19 })
end end
+399
View File
@@ -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
+8 -2
View File
@@ -138,27 +138,33 @@ function M.show_workspace_types()
local ws = require("scl.workspace_types") local ws = require("scl.workspace_types")
local udt = require("scl.udt_parser") local udt = require("scl.udt_parser")
local fb = require("scl.fb_parser") local fb = require("scl.fb_parser")
local db = require("scl.db_parser")
local root = ws.get_project_root() local root = ws.get_project_root()
local udt_names = udt.get_all_udt_names() local udt_names = udt.get_all_udt_names()
local fb_names = fb.get_all_fb_names() local fb_names = fb.get_all_fb_names()
local db_names = db.get_all_db_names()
if not root then if not root then
vim.notify("No project root detected", vim.log.levels.WARN) vim.notify("No project root detected", vim.log.levels.WARN)
return return
end 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 end
function M.rescan_workspace_types() function M.rescan_workspace_types()
local ws = require("scl.workspace_types") local ws = require("scl.workspace_types")
local udt = require("scl.udt_parser") local udt = require("scl.udt_parser")
local fb = require("scl.fb_parser") local fb = require("scl.fb_parser")
local db = require("scl.db_parser")
udt.clear_cache() udt.clear_cache()
fb.clear_cache() fb.clear_cache()
db.clear_cache()
ws.clear_root_cache() ws.clear_root_cache()
local udt_result = ws.scan_project_udts() local udt_result = ws.scan_project_udts()
local fb_result = ws.scan_project_fbs() 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 .. 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 end
function M.create_commands() function M.create_commands()
+119 -2
View File
@@ -9,10 +9,11 @@ local stat = vim.loop.fs_stat
-- Configuration -- Configuration
local config = { local config = {
enabled = true, enabled = true,
project_root = nil, -- nil = auto-detect project_root = nil,
library_paths = {}, -- Additional paths to scan for UDTs (e.g., shared libraries) library_paths = {},
type_extensions = { "udt" }, type_extensions = { "udt" },
fb_extensions = { "scl" }, fb_extensions = { "scl" },
db_extensions = { "db" },
scan_depth = 10, scan_depth = 10,
-- Folder names that indicate project root (case-insensitive) -- Folder names that indicate project root (case-insensitive)
project_markers = { project_markers = {
@@ -237,6 +238,52 @@ local function scan_for_fbs(dir, depth, results)
return results return results
end 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 -- Get all UDT files in project
function M.get_project_udt_files(bufnr) function M.get_project_udt_files(bufnr)
local root = M.get_project_root(bufnr) local root = M.get_project_root(bufnr)
@@ -287,6 +334,31 @@ function M.get_library_fb_files()
return all_files return all_files
end 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 -- Parse all UDT files in project and libraries
function M.scan_project_udts(bufnr) function M.scan_project_udts(bufnr)
local udt_parser = require("scl.udt_parser") local udt_parser = require("scl.udt_parser")
@@ -347,6 +419,35 @@ function M.scan_project_fbs(bufnr)
} }
end 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 -- Setup workspace scanner
function M.setup(opts) function M.setup(opts)
opts = opts or {} opts = opts or {}
@@ -367,6 +468,9 @@ function M.setup(opts)
if opts.project_markers then if opts.project_markers then
config.project_markers = opts.project_markers config.project_markers = opts.project_markers
end end
if opts.db_extensions then
config.db_extensions = opts.db_extensions
end
config.enabled = opts.workspace_types ~= false config.enabled = opts.workspace_types ~= false
@@ -384,12 +488,15 @@ function M.setup(opts)
-- Clear cache and rescan -- Clear cache and rescan
local udt_parser = require("scl.udt_parser") local udt_parser = require("scl.udt_parser")
local fb_parser = require("scl.fb_parser") local fb_parser = require("scl.fb_parser")
local db_parser = require("scl.db_parser")
udt_parser.clear_cache() udt_parser.clear_cache()
fb_parser.clear_cache() fb_parser.clear_cache()
db_parser.clear_cache()
M.clear_root_cache() M.clear_root_cache()
M.scan_project_udts(args.buf) M.scan_project_udts(args.buf)
M.scan_project_fbs(args.buf) M.scan_project_fbs(args.buf)
M.scan_project_dbs(args.buf)
end, end,
}) })
@@ -414,6 +521,16 @@ function M.setup(opts)
fb_parser.parse_fb_file(args.file) fb_parser.parse_fb_file(args.file)
end, 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 end
return M return M