refactor: rename scl_lsp → tia_lsp (project umbrella for future STL/LAD/FBD)
Rename the project from scl_lsp to tia_lsp to future-proof for additional
TIA Portal languages (STL/AWL, LAD, FBD). The 'scl' language name is kept
as-is for the SCL filetype and tree-sitter parser; future languages get
their own names (stl, lad, fbd).
Project-wide changes:
- lua/scl_lsp/ → lua/tia_lsp/ (plugin entry point)
- lua/scl/ → lua/tia/ (language modules, shared across TIA langs)
- require('scl.*') → require('tia.*')
- require('scl_lsp') → require('tia_lsp')
- LSP client name: 'scl_lsp' → 'tia_lsp'
- Diagnostic source: 'scl_lsp' → 'tia_lsp' (src/diagnostics.lua)
- package.json name: 'scl-language-server' → 'tia-lsp'
lua/tia_lsp/init.lua:
- Add vim.fn.exepath('tia-lsp') detection so the plugin prefers the
mason-installed executable and falls back to { 'lua', server_path }
for manual installs.
- Add opts.ts_parser_url to configure the tree-sitter parser source.
- Remove hardcoded ~/dev/scl_lsp paths in favor of ~/dev/tia-lsp and
the new ts_parser_url option.
Cleanup:
- Delete lua/tia/init.lua (dead code, unreferenced duplicate of tia_lsp).
- Delete scl_lsp.sh (replaced by the mason-installed bin/tia-lsp wrapper).
- Update README.md and AGENTS.md to reflect the two-repo split
(tia-lsp server + tia-lsp.nvim plugin) and document Mason installation.
Unchanged (intentional):
- grammar.js, src/grammar.json, src/parser.c: tree-sitter language name
remains 'scl' (it's the language, not the project).
- Filetype patterns ('scl', 'udt', 'db') in autocmds.
- :SCL* command names (per-filetype prefix; future :STL* etc.).
Tests pass with the same pre-existing failures as before the rename
(formatter VAR_INPUT test, udt_parser line 199 const-variable bug,
plc_json reference-project-missing). No new failures introduced.
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
-- DB Parser for SCL
|
||||
-- Parses .db files to extract global data block definitions and member structures
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Cache for parsed DBs: { [db_name] = db_definition }
|
||||
local db_cache = {}
|
||||
|
||||
-- Inline DB definitions (from comments): { [db_name] = db_definition }
|
||||
local inline_dbs = {}
|
||||
|
||||
-- DB Definition structure:
|
||||
-- {
|
||||
-- name = "DBName",
|
||||
-- title = "Data Block Title",
|
||||
-- version = "0.1",
|
||||
-- comment = "User comment",
|
||||
-- members = {
|
||||
-- {
|
||||
-- name = "memberName",
|
||||
-- type = "Bool" or '"OtherUdt"',
|
||||
-- attributes = "{ S7_SetPoint := 'True' }",
|
||||
-- comment = "Member comment",
|
||||
-- is_udt = false -- true if type is a quoted UDT reference
|
||||
-- }
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- Register an inline DB definition (from comments or other sources)
|
||||
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
|
||||
Reference in New Issue
Block a user