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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user