Inintal commit
This commit is contained in:
@@ -0,0 +1,830 @@
|
||||
-- blink.cmp completion source for SCL
|
||||
-- Provides completion for UDT types, members, and variables
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.new(opts)
|
||||
return setmetatable({ opts = opts or {} }, { __index = M })
|
||||
end
|
||||
|
||||
function M:get_trigger_characters()
|
||||
return { ".", "(", "#", " " }
|
||||
end
|
||||
|
||||
function M.is_available(context)
|
||||
local ft = vim.bo[context.bufnr].filetype
|
||||
if ft ~= "scl" and ft ~= "udt" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Get current line directly from buffer
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local row = cursor[1]
|
||||
local col = cursor[2]
|
||||
local line = vim.api.nvim_buf_get_lines(context.bufnr, row - 1, row, false)[1] or ""
|
||||
|
||||
-- Don't trigger on empty line (entering insert mode with o, i, etc.)
|
||||
if line == "" or line:match("^%s*$") then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Trigger on specific characters
|
||||
if col > 0 then
|
||||
local char_at = line:sub(col, col)
|
||||
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
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function M.get_completion_types()
|
||||
return { "syntax", "words", "line", "trigger" }
|
||||
end
|
||||
|
||||
-- Check if we're after BEGIN keyword
|
||||
local function is_after_begin(bufnr)
|
||||
local cursor_row = vim.api.nvim_buf_get_mark(bufnr, ".")[1] - 1
|
||||
|
||||
for i = cursor_row, 0, -1 do
|
||||
local line = vim.api.nvim_buf_get_lines(bufnr, i, i + 1, false)[1] or ""
|
||||
if line:match("^%s*BEGIN%s*$") or line:match("^%s*BEGIN%s*;") then
|
||||
return true
|
||||
end
|
||||
if line:match("^%s*ORGANIZATION_BLOCK%s+") or
|
||||
line:match("^%s*FUNCTION_BLOCK%s+") or
|
||||
line:match("^%s*FUNCTION%s+") then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check if we're inside a VAR declaration section
|
||||
local function is_in_var_section(bufnr)
|
||||
local cursor_row = vim.api.nvim_buf_get_mark(bufnr, ".")[1] - 1
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, cursor_row + 1, false)
|
||||
local in_var = false
|
||||
for i = 1, cursor_row + 1 do
|
||||
local line = lines[i] or ""
|
||||
if line:match("^%s*VAR") then
|
||||
in_var = true
|
||||
elseif line:match("^%s*END_VAR") then
|
||||
in_var = false
|
||||
end
|
||||
end
|
||||
return in_var
|
||||
end
|
||||
|
||||
-- Get all known local variables
|
||||
local function get_known_variables(bufnr)
|
||||
local scl_vars = require("scl.variables")
|
||||
local variables = scl_vars.extract_variables(bufnr)
|
||||
local known = {}
|
||||
for _, v in ipairs(variables) do
|
||||
known[v.name] = true
|
||||
end
|
||||
return known
|
||||
end
|
||||
|
||||
-- Check if a type is an FB/Function and get its interface
|
||||
local function get_fb_interface(var_type)
|
||||
local fb_parser = require("scl.fb_parser")
|
||||
local clean_type = var_type:gsub('"', '')
|
||||
|
||||
-- If cache is empty, try to scan workspace for FBs
|
||||
if fb_parser.get_cache_count() == 0 then
|
||||
local ws = require("scl.workspace_types")
|
||||
ws.scan_project_fbs(vim.api.nvim_get_current_buf())
|
||||
end
|
||||
|
||||
return fb_parser.get_fb_interface(clean_type)
|
||||
end
|
||||
|
||||
-- Check if a type is an FB/Function
|
||||
local function is_fb_type(var_type)
|
||||
local fb_parser = require("scl.fb_parser")
|
||||
local clean_type = var_type:gsub('"', '')
|
||||
|
||||
-- If cache is empty, try to scan workspace for FBs
|
||||
if fb_parser.get_cache_count() == 0 then
|
||||
local ws = require("scl.workspace_types")
|
||||
ws.scan_project_fbs(vim.api.nvim_get_current_buf())
|
||||
end
|
||||
|
||||
return fb_parser.is_fb_type(clean_type)
|
||||
end
|
||||
|
||||
function M:get_completions(context, callback)
|
||||
local bufnr = context.bufnr
|
||||
local line = context.line
|
||||
local cursor = context.cursor
|
||||
local col = cursor[2] or cursor[1] or 0
|
||||
|
||||
-- Check context for FB parameter completion
|
||||
-- We want to show FB params when:
|
||||
-- - Just opened paren: fbName(
|
||||
-- - After a comma: fbName(param1 := ,
|
||||
-- - At start of parameter: fbName(param (before :)
|
||||
--
|
||||
-- We DON'T want to show FB params when:
|
||||
-- - After := or => with an existing value: fbName(param1 := myVar
|
||||
-- - After := or => with trailing value: fbName(param1 := value,
|
||||
|
||||
local line_before_cursor = line:sub(1, col)
|
||||
|
||||
-- Check if we're after a comma (ready for next parameter)
|
||||
local is_after_comma = line_before_cursor:match(",%s*$") ~= nil
|
||||
|
||||
-- Check if we're typing a parameter value (after := or => with content)
|
||||
-- This is true if we have ":= something" or "=> something" without a comma after
|
||||
local has_param_assignment = line:match("%w+%s*:=%s*%S") ~= nil or line:match("%w+%s*=>%s*%S") ~= nil
|
||||
|
||||
-- If we have a param assignment but NOT after a comma, we're typing a value
|
||||
local is_after_param_assignment = has_param_assignment and not is_after_comma
|
||||
|
||||
-- Check filetype
|
||||
local ft = vim.bo[bufnr].filetype
|
||||
if ft ~= "scl" and ft ~= "udt" then
|
||||
callback({ items = {} })
|
||||
return
|
||||
end
|
||||
|
||||
local scl_vars = require("scl.variables")
|
||||
local variables = scl_vars.extract_variables(bufnr)
|
||||
|
||||
-- Get all known local variables and their types
|
||||
local known_vars = {}
|
||||
local var_types = {}
|
||||
for _, v in ipairs(variables) do
|
||||
known_vars[v.name] = true
|
||||
var_types[v.name] = v.type
|
||||
end
|
||||
|
||||
-- Skip FB parameter completions if user is typing a parameter value (after := or =>)
|
||||
if not is_after_param_assignment then
|
||||
-- Search for any FB call pattern in the entire line
|
||||
-- This handles cases where completion triggers before full text is typed
|
||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
|
||||
-- Find all potential FB calls in the line
|
||||
for fb_name in trimmed:gmatch("#?([%w_]+)%s*%(%s*%)?") do
|
||||
if known_vars[fb_name] then
|
||||
local var_type = var_types[fb_name]
|
||||
if var_type then
|
||||
local clean_type = var_type:gsub('"', '')
|
||||
if is_fb_type(clean_type) then
|
||||
local fb_interface = get_fb_interface(clean_type)
|
||||
if fb_interface then
|
||||
local wrapped_callback = vim.schedule_wrap(function(response)
|
||||
callback({
|
||||
items = response.items,
|
||||
is_incomplete_backward = false,
|
||||
is_incomplete_forward = false,
|
||||
})
|
||||
end)
|
||||
|
||||
local items = {}
|
||||
for _, inp in ipairs(fb_interface.inputs or {}) do
|
||||
local doc_value = inp.type
|
||||
if inp.comment and inp.comment ~= "" then
|
||||
doc_value = doc_value .. "\n\n" .. inp.comment
|
||||
end
|
||||
if inp.default_value then
|
||||
doc_value = doc_value .. "\n\nDefault: " .. inp.default_value
|
||||
end
|
||||
table.insert(items, {
|
||||
label = inp.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inp.type .. " (Input)",
|
||||
insertText = inp.name .. " := ",
|
||||
documentation = {
|
||||
kind = "markdown",
|
||||
value = doc_value,
|
||||
},
|
||||
})
|
||||
end
|
||||
for _, inout in ipairs(fb_interface.inouts or {}) do
|
||||
local doc_value = inout.type
|
||||
if inout.comment and inout.comment ~= "" then
|
||||
doc_value = doc_value .. "\n\n" .. inout.comment
|
||||
end
|
||||
if inout.default_value then
|
||||
doc_value = doc_value .. "\n\nDefault: " .. inout.default_value
|
||||
end
|
||||
table.insert(items, {
|
||||
label = inout.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inout.type .. " (InOut)",
|
||||
insertText = inout.name .. " := ",
|
||||
documentation = {
|
||||
kind = "markdown",
|
||||
value = doc_value,
|
||||
},
|
||||
})
|
||||
end
|
||||
for _, out in ipairs(fb_interface.outputs or {}) do
|
||||
local doc_value = out.type
|
||||
if out.comment and out.comment ~= "" then
|
||||
doc_value = doc_value .. "\n\n" .. out.comment
|
||||
end
|
||||
if out.default_value then
|
||||
doc_value = doc_value .. "\n\nDefault: " .. out.default_value
|
||||
end
|
||||
table.insert(items, {
|
||||
label = out.name .. " =>",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = out.type .. " (Output)",
|
||||
insertText = out.name .. " => ",
|
||||
documentation = {
|
||||
kind = "markdown",
|
||||
value = doc_value,
|
||||
},
|
||||
})
|
||||
end
|
||||
wrapped_callback({ items = items })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end -- if not is_after_param_assignment
|
||||
|
||||
-- If no FB call found on current line, search previous lines
|
||||
-- This handles multi-line parameter completion
|
||||
-- Skip if user is typing a parameter value (after := or =>)
|
||||
if not is_after_param_assignment then
|
||||
local cursor_row = vim.api.nvim_buf_get_mark(bufnr, ".")[1] - 1
|
||||
local prev_lines = vim.api.nvim_buf_get_lines(bufnr, 0, cursor_row, false)
|
||||
|
||||
-- Check if any previous line has a closed FB call (ending with ");")
|
||||
-- If so, don't provide completions
|
||||
local has_closed_call = false
|
||||
for i = 1, #prev_lines do
|
||||
if prev_lines[i]:match("%);%s*$") then
|
||||
has_closed_call = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not has_closed_call then
|
||||
for i = #prev_lines, 1, -1 do
|
||||
local prev_line = prev_lines[i]:gsub("%s+$", "")
|
||||
for fb_name in prev_line:gmatch("#?([%w_]+)%s*%(%s*%)?") do
|
||||
if known_vars[fb_name] then
|
||||
local var_type = var_types[fb_name]
|
||||
if var_type then
|
||||
local clean_type = var_type:gsub('"', '')
|
||||
if is_fb_type(clean_type) then
|
||||
local fb_interface = get_fb_interface(clean_type)
|
||||
if fb_interface then
|
||||
local wrapped_callback = vim.schedule_wrap(function(response)
|
||||
callback({
|
||||
items = response.items,
|
||||
is_incomplete_backward = false,
|
||||
is_incomplete_forward = false,
|
||||
})
|
||||
end)
|
||||
|
||||
local items = {}
|
||||
for _, inp in ipairs(fb_interface.inputs or {}) do
|
||||
local doc_value = inp.type
|
||||
if inp.comment and inp.comment ~= "" then
|
||||
doc_value = doc_value .. "\n\n" .. inp.comment
|
||||
end
|
||||
if inp.default_value then
|
||||
doc_value = doc_value .. "\n\nDefault: " .. inp.default_value
|
||||
end
|
||||
table.insert(items, {
|
||||
label = inp.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inp.type .. " (Input)",
|
||||
insertText = inp.name .. " := ",
|
||||
documentation = {
|
||||
kind = "markdown",
|
||||
value = doc_value,
|
||||
},
|
||||
})
|
||||
end
|
||||
for _, inout in ipairs(fb_interface.inouts or {}) do
|
||||
local doc_value = inout.type
|
||||
if inout.comment and inout.comment ~= "" then
|
||||
doc_value = doc_value .. "\n\n" .. inout.comment
|
||||
end
|
||||
if inout.default_value then
|
||||
doc_value = doc_value .. "\n\nDefault: " .. inout.default_value
|
||||
end
|
||||
table.insert(items, {
|
||||
label = inout.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inout.type .. " (InOut)",
|
||||
insertText = inout.name .. " := ",
|
||||
documentation = {
|
||||
kind = "markdown",
|
||||
value = doc_value,
|
||||
},
|
||||
})
|
||||
end
|
||||
for _, out in ipairs(fb_interface.outputs or {}) do
|
||||
local doc_value = out.type
|
||||
if out.comment and out.comment ~= "" then
|
||||
doc_value = doc_value .. "\n\n" .. out.comment
|
||||
end
|
||||
if out.default_value then
|
||||
doc_value = doc_value .. "\n\nDefault: " .. out.default_value
|
||||
end
|
||||
table.insert(items, {
|
||||
label = out.name .. " =>",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = out.type .. " (Output)",
|
||||
insertText = out.name .. " => ",
|
||||
documentation = {
|
||||
kind = "markdown",
|
||||
value = doc_value,
|
||||
},
|
||||
})
|
||||
end
|
||||
wrapped_callback({ items = items })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local base_var = nil
|
||||
local member_path = nil
|
||||
|
||||
-- Strip leading whitespace for regex matching
|
||||
local trim_line = line:gsub("^%s+", "")
|
||||
|
||||
-- Note: known_vars is already defined above
|
||||
|
||||
-- Function to extract base_var and member_path from a var.member... pattern
|
||||
local function extract_from_pattern(full_text)
|
||||
if not full_text then return nil, nil end
|
||||
|
||||
-- Handle # prefixed
|
||||
local var_pattern = full_text:match("^#([%w_]+)")
|
||||
if var_pattern then
|
||||
local after = full_text:sub(#var_pattern + 2)
|
||||
-- Remove leading dot if present
|
||||
if after:sub(1, 1) == "." then
|
||||
after = after:sub(2)
|
||||
end
|
||||
-- Remove trailing dot if present
|
||||
if after:sub(-1) == "." then
|
||||
after = after:sub(1, -2)
|
||||
end
|
||||
return var_pattern, after
|
||||
end
|
||||
|
||||
-- Non-prefixed - extract first identifier as variable
|
||||
local var_name = full_text:match("^([%w_]+)")
|
||||
if var_name and known_vars[var_name] then
|
||||
local after = full_text:sub(#var_name + 2)
|
||||
-- Remove leading dot if present
|
||||
if after:sub(1, 1) == "." then
|
||||
after = after:sub(2)
|
||||
end
|
||||
-- Remove trailing dot if present
|
||||
if after:sub(-1) == "." then
|
||||
after = after:sub(1, -2)
|
||||
end
|
||||
return var_name, after
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- Find the LAST occurrence of a member access pattern (handles "AND var.member" cases)
|
||||
-- First try # prefixed patterns - handle both "#var.member" and "#var.member."
|
||||
local search_pos = 1
|
||||
while search_pos <= #trim_line do
|
||||
local hash_pos = trim_line:find("#", search_pos)
|
||||
if not hash_pos then break end
|
||||
|
||||
-- Find the variable name after #
|
||||
local var_end = hash_pos + 1
|
||||
while var_end <= #trim_line and trim_line:sub(var_end, var_end):match("[%w_]") do
|
||||
var_end = var_end + 1
|
||||
end
|
||||
var_end = var_end - 1
|
||||
|
||||
if var_end > hash_pos then
|
||||
local var_name = trim_line:sub(hash_pos + 1, var_end)
|
||||
-- Check if followed by a dot
|
||||
if var_end < #trim_line and trim_line:sub(var_end + 1, var_end + 1) == "." then
|
||||
-- Extract the member path after the dot
|
||||
local path_start = var_end + 2
|
||||
local path_end = path_start - 1
|
||||
while path_end < #trim_line and trim_line:sub(path_end + 1, path_end + 1):match("[%w_.]") do
|
||||
path_end = path_end + 1
|
||||
end
|
||||
local path = trim_line:sub(path_start, path_end)
|
||||
-- Remove trailing dot if present
|
||||
if path:sub(-1) == "." then
|
||||
path = path:sub(1, -2)
|
||||
end
|
||||
base_var = var_name
|
||||
member_path = path
|
||||
elseif var_end == #trim_line or trim_line:sub(var_end + 1, var_end + 1):match("[%s%)]") then
|
||||
-- Variable at end of line or followed by space/paren
|
||||
base_var = var_name
|
||||
member_path = ""
|
||||
end
|
||||
end
|
||||
search_pos = hash_pos + 1
|
||||
end
|
||||
|
||||
-- Then try non-prefixed patterns - search backwards to find LAST occurrence
|
||||
if not base_var then
|
||||
local search_pos = 1
|
||||
while search_pos <= #trim_line do
|
||||
-- Find word boundaries for potential variable names
|
||||
local word_start, word_end = trim_line:find("([%w_]+)", search_pos)
|
||||
if not word_start then break end
|
||||
|
||||
local var_name = trim_line:sub(word_start, word_end)
|
||||
if known_vars[var_name] then
|
||||
-- Check if followed by a dot
|
||||
if word_end < #trim_line and trim_line:sub(word_end + 1, word_end + 1) == "." then
|
||||
local path_start = word_end + 2
|
||||
local path_end = word_end + 1
|
||||
while path_end < #trim_line and trim_line:sub(path_end + 1, path_end + 1):match("[%w_.]") do
|
||||
path_end = path_end + 1
|
||||
end
|
||||
local path = trim_line:sub(path_start, path_end)
|
||||
if path:sub(-1) == "." then
|
||||
path = path:sub(1, -2)
|
||||
end
|
||||
base_var = var_name
|
||||
member_path = path
|
||||
elseif word_end == #trim_line then
|
||||
base_var = var_name
|
||||
member_path = ""
|
||||
end
|
||||
end
|
||||
search_pos = word_end + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: just var at end of line or var followed by ( or ()
|
||||
if not base_var then
|
||||
local var_name = trim_line:match("([%w_]+)%s*%(%)$")
|
||||
if var_name and known_vars[var_name] then
|
||||
base_var = var_name
|
||||
member_path = ""
|
||||
else
|
||||
var_name = trim_line:match("([%w_]+)%s*%($")
|
||||
if var_name and known_vars[var_name] then
|
||||
base_var = var_name
|
||||
member_path = ""
|
||||
else
|
||||
var_name = trim_line:match("([%w_]+)%s*$")
|
||||
if var_name and known_vars[var_name] then
|
||||
base_var = var_name
|
||||
member_path = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Also check for #varName() pattern (after auto-bracket completion)
|
||||
if not base_var then
|
||||
local hash_var = trim_line:match("#([%w_]+)%s*%(%)$")
|
||||
if hash_var and known_vars[hash_var] then
|
||||
base_var = hash_var
|
||||
member_path = ""
|
||||
end
|
||||
end
|
||||
|
||||
-- Also check for #varName( pattern
|
||||
if not base_var then
|
||||
local hash_var = trim_line:match("#([%w_]+)%s*%($")
|
||||
if hash_var and known_vars[hash_var] then
|
||||
base_var = hash_var
|
||||
member_path = ""
|
||||
end
|
||||
end
|
||||
|
||||
local wrapped_callback = vim.schedule_wrap(function(response)
|
||||
callback({
|
||||
items = response.items,
|
||||
is_incomplete_backward = false,
|
||||
is_incomplete_forward = false,
|
||||
})
|
||||
end)
|
||||
|
||||
vim.schedule(function()
|
||||
local udt_parser = require("scl.udt_parser")
|
||||
|
||||
-- No member access = show local variables, UDT types, and basic types
|
||||
if not base_var then
|
||||
local items = {}
|
||||
|
||||
-- Add local variables from VAR sections
|
||||
local in_var_section = is_in_var_section(bufnr)
|
||||
for _, v in ipairs(variables) do
|
||||
if in_var_section then
|
||||
table.insert(items, {
|
||||
label = v.name,
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Variable,
|
||||
detail = v.type and (v.type .. " - Local variable") or "Local variable",
|
||||
})
|
||||
else
|
||||
table.insert(items, {
|
||||
label = "#" .. v.name,
|
||||
insertText = "#" .. v.name,
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Variable,
|
||||
detail = v.type and (v.type .. " - Local variable") or "Local variable",
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Add UDT types
|
||||
for _, name in ipairs(udt_parser.get_all_udt_names()) do
|
||||
table.insert(items, { label = '"' .. name .. '"', kind = 14 })
|
||||
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 })
|
||||
end
|
||||
wrapped_callback({ items = items })
|
||||
return
|
||||
end
|
||||
|
||||
-- Member completion
|
||||
local var_type = scl_vars.get_variable_type(base_var, bufnr)
|
||||
-- If base_var is not a known variable, show all local variables instead
|
||||
if not var_type then
|
||||
local items = {}
|
||||
local in_var_section = is_in_var_section(bufnr)
|
||||
for _, v in ipairs(variables) do
|
||||
if in_var_section then
|
||||
table.insert(items, {
|
||||
label = v.name,
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Variable,
|
||||
detail = v.type and (v.type .. " - Local variable") or "Local variable",
|
||||
})
|
||||
else
|
||||
table.insert(items, {
|
||||
label = "#" .. v.name,
|
||||
insertText = "#" .. v.name,
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Variable,
|
||||
detail = v.type and (v.type .. " - Local variable") or "Local variable",
|
||||
})
|
||||
end
|
||||
end
|
||||
for _, name in ipairs(udt_parser.get_all_udt_names()) do
|
||||
table.insert(items, { label = '"' .. name .. '"', kind = 14 })
|
||||
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
|
||||
wrapped_callback({ items = items })
|
||||
return
|
||||
end
|
||||
|
||||
var_type = var_type:gsub('"', '')
|
||||
|
||||
local is_udt = udt_parser.is_udt_type(var_type)
|
||||
local is_fb = is_fb_type(var_type)
|
||||
local fb_interface = nil
|
||||
|
||||
if is_fb then
|
||||
fb_interface = get_fb_interface(var_type)
|
||||
end
|
||||
|
||||
if not is_udt and not is_fb then
|
||||
wrapped_callback({ items = {} })
|
||||
return
|
||||
end
|
||||
|
||||
-- Handle FB parameter completion (when user types instanceName() or instanceName(
|
||||
local line_before_cursor = line:sub(1, col)
|
||||
local is_fb_call = line_before_cursor:match("%(#?[%w_]+%s*%(%s*$") ~= nil or
|
||||
line_before_cursor:match("%(#?[%w_]+%s*%)%s*$") ~= nil
|
||||
|
||||
-- Check if there's a ( somewhere after the variable (parameter call mode)
|
||||
local has_param_paren = line:match("%(") ~= nil
|
||||
|
||||
if is_fb and fb_interface and (is_fb_call or (member_path == "" and has_param_paren)) then
|
||||
local items = {}
|
||||
for _, inp in ipairs(fb_interface.inputs or {}) do
|
||||
table.insert(items, {
|
||||
label = inp.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inp.type .. " (Input)",
|
||||
insertText = inp.name .. " := ",
|
||||
})
|
||||
end
|
||||
for _, inout in ipairs(fb_interface.inouts or {}) do
|
||||
table.insert(items, {
|
||||
label = inout.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inout.type .. " (InOut)",
|
||||
insertText = inout.name .. " := ",
|
||||
})
|
||||
end
|
||||
for _, out in ipairs(fb_interface.outputs or {}) do
|
||||
table.insert(items, {
|
||||
label = out.name .. " =>",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = out.type .. " (Output)",
|
||||
insertText = out.name .. " => ",
|
||||
})
|
||||
end
|
||||
wrapped_callback({ items = items })
|
||||
return
|
||||
end
|
||||
|
||||
-- Fallback: if base_var is an FB and we're after BEGIN (in code section), provide parameter completions
|
||||
if is_fb and fb_interface and member_path == "" and is_after_begin(bufnr) then
|
||||
local items = {}
|
||||
for _, inp in ipairs(fb_interface.inputs or {}) do
|
||||
table.insert(items, {
|
||||
label = inp.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inp.type .. " (Input)",
|
||||
insertText = inp.name .. " := ",
|
||||
})
|
||||
end
|
||||
for _, inout in ipairs(fb_interface.inouts or {}) do
|
||||
table.insert(items, {
|
||||
label = inout.name .. " :=",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = inout.type .. " (InOut)",
|
||||
insertText = inout.name .. " := ",
|
||||
})
|
||||
end
|
||||
for _, out in ipairs(fb_interface.outputs or {}) do
|
||||
table.insert(items, {
|
||||
label = out.name .. " =>",
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Property,
|
||||
detail = out.type .. " (Output)",
|
||||
insertText = out.name .. " => ",
|
||||
})
|
||||
end
|
||||
wrapped_callback({ items = items })
|
||||
return
|
||||
end
|
||||
|
||||
-- If it's an FB but we're accessing members with dot, treat as UDT-like completion
|
||||
if is_fb and fb_interface and member_path == "" then
|
||||
local items = {}
|
||||
for _, inp in ipairs(fb_interface.inputs or {}) do
|
||||
table.insert(items, {
|
||||
label = inp.name,
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Field,
|
||||
detail = inp.type .. " (Input)",
|
||||
})
|
||||
end
|
||||
for _, inout in ipairs(fb_interface.inouts or {}) do
|
||||
table.insert(items, {
|
||||
label = inout.name,
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Field,
|
||||
detail = inout.type .. " (InOut)",
|
||||
})
|
||||
end
|
||||
for _, out in ipairs(fb_interface.outputs or {}) do
|
||||
table.insert(items, {
|
||||
label = out.name,
|
||||
kind = vim.lsp.protocol.CompletionItemKind.Field,
|
||||
detail = out.type .. " (Output)",
|
||||
})
|
||||
end
|
||||
wrapped_callback({ items = items })
|
||||
return
|
||||
end
|
||||
|
||||
if not is_udt then
|
||||
wrapped_callback({ items = {} })
|
||||
return
|
||||
end
|
||||
|
||||
-- Resolve nested path - iterate through each part to find the target type
|
||||
local target_type = var_type
|
||||
if member_path and member_path ~= "" then
|
||||
local line_ends_with_dot = trim_line:match("%.$") ~= nil
|
||||
|
||||
local path_parts = {}
|
||||
for part in member_path:gmatch("[^.]+") do
|
||||
table.insert(path_parts, part)
|
||||
end
|
||||
|
||||
local current_type = var_type
|
||||
local resolved_parts = 0
|
||||
|
||||
for i, part in ipairs(path_parts) do
|
||||
local udt = udt_parser.get_udt(current_type)
|
||||
if not udt then
|
||||
break
|
||||
end
|
||||
|
||||
local found = false
|
||||
for _, member in ipairs(udt.members) do
|
||||
if member.name == part then
|
||||
resolved_parts = i
|
||||
if member.is_udt then
|
||||
current_type = member.type
|
||||
found = true
|
||||
else
|
||||
-- Not a UDT - if this is the last part, we found the target type
|
||||
-- Otherwise, we can't continue the path
|
||||
if i < #path_parts then
|
||||
found = false
|
||||
else
|
||||
found = true
|
||||
current_type = member.type
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not found then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if line_ends_with_dot or resolved_parts == #path_parts then
|
||||
target_type = current_type
|
||||
else
|
||||
wrapped_callback({ items = {} })
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local members = udt_parser.get_udt_members(target_type)
|
||||
local items = {}
|
||||
for _, m in ipairs(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
|
||||
|
||||
wrapped_callback({ items = items })
|
||||
end)
|
||||
|
||||
return function() end
|
||||
end
|
||||
|
||||
-- Resolve callback to add # prefix for variables
|
||||
function M:resolve(context, callback)
|
||||
local item = context.item
|
||||
local bufnr = context.bufnr
|
||||
|
||||
-- Check if we should add # prefix
|
||||
local should_prefix = false
|
||||
if item and item.label then
|
||||
-- Only add # for variable kinds
|
||||
local var_kinds = {
|
||||
vim.lsp.protocol.CompletionItemKind.Variable,
|
||||
vim.lsp.protocol.CompletionItemKind.Parameter,
|
||||
}
|
||||
|
||||
local is_var = false
|
||||
for _, kind in ipairs(var_kinds) do
|
||||
if item.kind == kind then
|
||||
is_var = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if is_var then
|
||||
-- Check if we're after BEGIN and not in VAR section
|
||||
if is_after_begin(bufnr) and not is_in_var_section(bufnr) then
|
||||
-- Check if variable is in known variables list
|
||||
local known_vars = get_known_variables(bufnr)
|
||||
local var_name = item.label:gsub("^#", "")
|
||||
if known_vars[var_name] and not item.label:match("^#") then
|
||||
should_prefix = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if should_prefix then
|
||||
item.label = "#" .. item.label
|
||||
item.insertText = "#" .. (item.insertText or item.label)
|
||||
if item.textEdit and item.textEdit.newText then
|
||||
item.textEdit.newText = "#" .. item.textEdit.newText
|
||||
end
|
||||
end
|
||||
|
||||
callback({ item = item })
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user