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.
241 lines
6.2 KiB
Lua
241 lines
6.2 KiB
Lua
local test = dofile("test/test_harness.lua")
|
|
|
|
package.path = package.path .. ";lua/?.lua"
|
|
|
|
local fb_parser = require("tia.fb_parser")
|
|
|
|
local function test_fb_parser_basic()
|
|
local content = [[
|
|
FUNCTION_BLOCK "test_fb"
|
|
TITLE = Test FB
|
|
VERSION : 0.1
|
|
VAR_INPUT
|
|
enable : Bool;
|
|
count : Int;
|
|
END_VAR
|
|
|
|
VAR_OUTPUT
|
|
done : Bool;
|
|
result : DInt;
|
|
END_VAR
|
|
|
|
VAR_IN_OUT
|
|
data : "some_udt";
|
|
END_VAR
|
|
|
|
END_FUNCTION_BLOCK
|
|
]]
|
|
local fb, err = fb_parser.parse_fb_content(content, "test.scl")
|
|
test.assert_not_nil(fb, "parse_fb_content should return fb")
|
|
test.assert_equals(fb.name, "test_fb", "FB name should be extracted")
|
|
test.assert_equals(fb.type, "FUNCTION_BLOCK", "FB type should be FUNCTION_BLOCK")
|
|
test.assert_equals(#fb.inputs, 2, "should have 2 inputs")
|
|
test.assert_equals(#fb.outputs, 2, "should have 2 outputs")
|
|
test.assert_equals(#fb.inouts, 1, "should have 1 inout")
|
|
test.assert_equals(fb.inputs[1].name, "enable", "first input name")
|
|
test.assert_equals(fb.inouts[1].is_quoted_type, true, "inout is quoted type")
|
|
end
|
|
|
|
local function test_fb_parser_attributes()
|
|
local content = [[
|
|
FUNCTION_BLOCK "attr_fb"
|
|
TITLE = Attr FB
|
|
VERSION : 0.1
|
|
VAR_INPUT
|
|
attrInput{S7_SetPoint := 'True'}: Bool;
|
|
normalInput : Int;
|
|
END_VAR
|
|
|
|
END_FUNCTION_BLOCK
|
|
]]
|
|
local fb = fb_parser.parse_fb_content(content, "attr.scl")
|
|
test.assert_not_nil(fb, "parse_fb_content should return fb")
|
|
test.assert_equals(fb.inputs[1].attributes.S7_SetPoint, "True", "attribute should be parsed")
|
|
end
|
|
|
|
local function test_fb_parser_comments()
|
|
local content = [[
|
|
FUNCTION_BLOCK "comment_fb"
|
|
TITLE = Comment FB
|
|
VERSION : 0.1
|
|
VAR_INPUT
|
|
input1 : Bool; //enable flag
|
|
input2 : Int; //counter
|
|
END_VAR
|
|
|
|
END_FUNCTION_BLOCK
|
|
]]
|
|
local fb = fb_parser.parse_fb_content(content, "comment.scl")
|
|
test.assert_not_nil(fb, "parse_fb_content should return fb")
|
|
test.assert_equals(fb.inputs[1].comment, "enable flag", "input comment should be extracted")
|
|
end
|
|
|
|
local function test_fb_parser_function()
|
|
local content = [[
|
|
FUNCTION "test_func" : Int
|
|
TITLE = Test Function
|
|
VERSION : 0.1
|
|
VAR_INPUT
|
|
a : Int;
|
|
b : Int;
|
|
END_VAR
|
|
|
|
|
|
BEGIN
|
|
|
|
END_FUNCTION
|
|
]]
|
|
local fb = fb_parser.parse_fb_content(content, "test_func.scl")
|
|
test.assert_not_nil(fb, "parse_fb_content should return fb")
|
|
test.assert_equals(fb.name, "test_func", "Function name should be extracted")
|
|
test.assert_equals(fb.type, "FUNCTION", "FB type should be FUNCTION")
|
|
test.assert_equals(fb.return_type, "Int", "return type should be extracted")
|
|
end
|
|
|
|
local function test_fb_parser_default_values()
|
|
local content = [[
|
|
FUNCTION_BLOCK "default_fb"
|
|
TITLE = Default FB
|
|
VERSION : 0.1
|
|
VAR_INPUT
|
|
withDefault : Int := 5;
|
|
withoutDefault : Bool;
|
|
END_VAR
|
|
|
|
END_FUNCTION_BLOCK
|
|
]]
|
|
local fb = fb_parser.parse_fb_content(content, "default.scl")
|
|
test.assert_not_nil(fb, "parse_fb_content should return fb")
|
|
test.assert(fb.inputs[1].has_default, "first input has default")
|
|
test.assert_equals(fb.inputs[1].default_value, "5", "default value should be extracted")
|
|
test.assert(not fb.inputs[2].has_default, "second input has no default")
|
|
end
|
|
|
|
local function test_fb_cache_operations()
|
|
fb_parser.clear_cache()
|
|
test.assert_equals(fb_parser.get_cache_count(), 0, "cache should be empty after clear")
|
|
|
|
local content = [[
|
|
FUNCTION_BLOCK "cached_fb"
|
|
TITLE = Cached FB
|
|
VERSION : 0.1
|
|
VAR_INPUT
|
|
input1 : Bool;
|
|
END_VAR
|
|
|
|
END_FUNCTION_BLOCK
|
|
]]
|
|
local fb = fb_parser.parse_fb_content(content, "cached.scl")
|
|
test.assert_not_nil(fb, "FB should be parsed")
|
|
test.assert_equals(fb.name, "cached_fb", "FB name should be extracted")
|
|
end
|
|
|
|
local function test_fb_is_type()
|
|
fb_parser.clear_cache()
|
|
test.assert(true, "is_fb_type requires file-based caching - tested separately")
|
|
end
|
|
|
|
local function test_fb_interface()
|
|
fb_parser.clear_cache()
|
|
|
|
local content = [[
|
|
FUNCTION_BLOCK "interface_fb"
|
|
TITLE = Interface FB
|
|
VERSION : 0.1
|
|
VAR_INPUT
|
|
enable : Bool;
|
|
value : Int;
|
|
END_VAR
|
|
|
|
VAR_OUTPUT
|
|
done : Bool;
|
|
error : Bool;
|
|
END_VAR
|
|
|
|
VAR_IN_OUT
|
|
data : "some_type";
|
|
END_VAR
|
|
|
|
END_FUNCTION_BLOCK
|
|
]]
|
|
local fb = fb_parser.parse_fb_content(content, "interface.scl")
|
|
test.assert_not_nil(fb, "FB should be parsed")
|
|
|
|
if fb then
|
|
test.assert_equals(#fb.inputs, 2, "should have 2 inputs")
|
|
test.assert_equals(#fb.outputs, 2, "should have 2 outputs")
|
|
test.assert_equals(#fb.inouts, 1, "should have 1 inout")
|
|
end
|
|
end
|
|
|
|
local function test_fb_reference_files()
|
|
fb_parser.clear_cache()
|
|
|
|
local ref_path = test.REF_PROJECT .. "/Program blocks/01_Machine/MachineMain.scl"
|
|
local file = io.open(ref_path, "r")
|
|
if not file then
|
|
print("Warning: Reference SCL file not found: " .. ref_path)
|
|
return
|
|
end
|
|
local content = file:read("*all")
|
|
file:close()
|
|
|
|
local fb = fb_parser.parse_fb_content(content, ref_path)
|
|
test.assert_not_nil(fb, "should parse reference SCL file")
|
|
test.assert_equals(fb.name, "MachineMain", "FB name should match")
|
|
test.assert(fb.type == "FUNCTION_BLOCK", "should be FUNCTION_BLOCK")
|
|
end
|
|
|
|
local function test_fb_multiple_reference_files()
|
|
fb_parser.clear_cache()
|
|
|
|
local files = {
|
|
test.REF_PROJECT .. "/Program blocks/01_Machine/MachineMain.scl",
|
|
test.REF_PROJECT .. "/Program blocks/01_Machine/ReadInputs.scl",
|
|
test.REF_PROJECT .. "/Program blocks/01_Machine/WriteOutputs.scl",
|
|
}
|
|
|
|
local parsed_count = 0
|
|
for _, filepath in ipairs(files) do
|
|
local file = io.open(filepath, "r")
|
|
if file then
|
|
local content = file:read("*all")
|
|
file:close()
|
|
local fb = fb_parser.parse_fb_content(content, filepath)
|
|
if fb then
|
|
parsed_count = parsed_count + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
test.assert_gt(parsed_count, 0, "should parse at least one reference SCL")
|
|
end
|
|
|
|
local function run_fb_tests()
|
|
test.run_suite("FB Parser Tests", function()
|
|
print("\n--- Basic Tests ---")
|
|
test_fb_parser_basic()
|
|
test_fb_parser_attributes()
|
|
test_fb_parser_comments()
|
|
test_fb_parser_function()
|
|
test_fb_parser_default_values()
|
|
|
|
print("\n--- Cache Tests ---")
|
|
test_fb_cache_operations()
|
|
test_fb_is_type()
|
|
test_fb_interface()
|
|
|
|
print("\n--- Reference Project Tests ---")
|
|
test_fb_reference_files()
|
|
test_fb_multiple_reference_files()
|
|
end)
|
|
end
|
|
|
|
if vim then
|
|
vim.api.nvim_create_user_command("TestFb", function()
|
|
run_fb_tests()
|
|
end, {})
|
|
end
|
|
|
|
run_fb_tests()
|