- Fix path extraction pattern (missing closing quote, extra capture group) - Add Array[..] of "UdtName" detection for nested member resolution - Remove vim.schedule_wrap from DB member callback (causes blink.cmp timeout) - Add built-in instruction parameter fallback (IEC_TIMER IN/PT/Q/ET) - Ensure DB cache is populated on demand in completion source - Make try_register() succeed without cmp.add_source_provider API - docs: add blink.cmp source config steps to installation guide
1217 lines
42 KiB
Lua
1217 lines
42 KiB
Lua
-- 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 (including space for := assignments)
|
|
if char_at == "." or char_at == "(" or char_at == "#" or char_at == '"' or char_at == " " or char_at == ":" or
|
|
prev_char == "." or prev_char == "(" or prev_char == "#" or prev_char == '"' or prev_char == " " or prev_char == ":" then
|
|
return true
|
|
end
|
|
|
|
-- Also trigger when typing a word character if we're in VAR section (type declaration)
|
|
if char_at:match("[%w_]") or prev_char:match("[%w_]") then
|
|
-- Check if we're in VAR section
|
|
local in_var = false
|
|
local lines = vim.api.nvim_buf_get_lines(context.bufnr, 0, row, false)
|
|
for i = 1, #lines do
|
|
local l = lines[i]:gsub("^%s+", ""):gsub("%s+$", "")
|
|
if l:match("^END_VAR") then
|
|
in_var = false
|
|
elseif l:match("^VAR%s*$") or l:match("^VAR$") or l:match("^VAR_INPUT") or l:match("^VAR_OUTPUT") or l:match("^VAR_IN_OUT") or l:match("^VAR_TEMP") then
|
|
in_var = true
|
|
end
|
|
end
|
|
|
|
-- Also check current line - if it has ":" followed by space or word, we're declaring a type
|
|
local line_before = col > 1 and line:sub(1, col - 1) or ""
|
|
if in_var and (line_before:match(":%s*$") or line_before:match(":%s+$")) then
|
|
return true
|
|
end
|
|
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("tia.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("tia.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("tia.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("tia.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("tia.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
|
|
|
|
-- Check for Global DB completion (quoted DB names like "HmiData")
|
|
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
|
|
|
-- Find the "active portion" of the line - after the last operator if present
|
|
-- Operators: :=, =>, =, <>, >=, <=, >, <, AND, OR, NOT
|
|
-- This ensures we match DB patterns nearest to the cursor, not the first in line
|
|
local active_portion = line_before_cursor
|
|
|
|
local function find_last_operator_pos(text)
|
|
local last_pos = 0
|
|
local operators = {":=", "=>", "<>", ">=", "<=", "=", ">", "<"}
|
|
for _, op in ipairs(operators) do
|
|
local search_start = 1
|
|
while true do
|
|
local pos = text:find(op, search_start, true)
|
|
if not pos then break end
|
|
if pos > last_pos then
|
|
last_pos = pos + #op
|
|
end
|
|
search_start = pos + 1
|
|
end
|
|
end
|
|
-- Also check for AND/OR/NOT keywords (case insensitive)
|
|
local keywords = {"%sAND%s", "%sOR%s", "%sNOT%s"}
|
|
for _, kw in ipairs(keywords) do
|
|
local search_start = 1
|
|
while true do
|
|
local pos = text:upper():find(kw, search_start)
|
|
if not pos then break end
|
|
if pos > last_pos then
|
|
last_pos = pos + #kw - 1
|
|
end
|
|
search_start = pos + 1
|
|
end
|
|
end
|
|
return last_pos
|
|
end
|
|
|
|
local last_op_pos = find_last_operator_pos(line_before_cursor)
|
|
if last_op_pos > 0 then
|
|
active_portion = line_before_cursor:sub(last_op_pos):gsub("^%s+", "")
|
|
end
|
|
|
|
-- Check for "DBName" pattern (quoted DB name access with dot) - in active portion only
|
|
local db_member_pattern = active_portion:match('"([%w_]+)"%.([%w_.]*)')
|
|
|
|
-- Check if we're typing a quoted DB name (after " but before closing ") - in active portion
|
|
local is_in_db_name = active_portion:match('"[%w_]*$') ~= nil
|
|
|
|
-- Check for partial DB name like "HmiData (unclosed quote) - in active portion
|
|
local partial_db_match = active_portion:match('"([%w_]+)$')
|
|
local is_partial_db = partial_db_match ~= nil and active_portion:match('"[^"]*$') ~= nil
|
|
|
|
-- Check if after a colon followed by quote (type declaration context) - use full trimmed line
|
|
local is_after_colon_quote = trimmed:match(':%s*"$') ~= nil
|
|
|
|
-- Check if cursor is right after a closed quoted DB name (before dot) - in active portion
|
|
local after_closed_db = active_portion:match('"([%w_]+)"%s*%.') ~= nil
|
|
|
|
if db_member_pattern or after_closed_db then
|
|
local db_parser = require("tia.db_parser")
|
|
local db_name = active_portion:match('"([%w_]+)"')
|
|
|
|
-- Ensure cache is populated
|
|
if db_name and db_parser.get_cache_count() == 0 then
|
|
local ws = require("tia.workspace_types")
|
|
ws.scan_project_dbs(bufnr)
|
|
end
|
|
|
|
if db_name then
|
|
local db = db_parser.get_db(db_name)
|
|
if db then
|
|
local path = active_portion: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
|
|
local nested_type = nil
|
|
if found_member.is_udt then
|
|
nested_type = found_member.type
|
|
else
|
|
-- Check for array of UDT: Array[..] of "TypeName"
|
|
local array_udt = found_member.raw_type:match('Array%b[]%s*of%s*"([^"]+)"')
|
|
if array_udt then
|
|
nested_type = array_udt
|
|
end
|
|
end
|
|
if nested_type then
|
|
local udt_parser = require("tia.udt_parser")
|
|
current_members = udt_parser.get_udt_members(nested_type)
|
|
current_type = nested_type
|
|
else
|
|
-- This is a basic type, can't continue
|
|
if i < #path_parts then
|
|
found = false
|
|
break
|
|
end
|
|
current_type = found_member.type
|
|
current_members = {}
|
|
end
|
|
else
|
|
found = false
|
|
break
|
|
end
|
|
end
|
|
|
|
if not found then
|
|
callback({ items = {}, is_incomplete_backward = false, is_incomplete_forward = false })
|
|
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
|
|
callback({ items = {}, is_incomplete_backward = false, is_incomplete_forward = false })
|
|
return
|
|
end
|
|
|
|
-- Get members to show (either from DB or from resolved UDT type)
|
|
if current_type then
|
|
local udt_parser = require("tia.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
|
|
callback({
|
|
items = items,
|
|
is_incomplete_backward = false,
|
|
is_incomplete_forward = false,
|
|
})
|
|
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("tia.db_parser")
|
|
|
|
-- If cache is empty, try to scan workspace for DBs
|
|
if db_parser.get_cache_count() == 0 then
|
|
local ws = require("tia.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("tia.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("tia.udt_parser")
|
|
local db_parser = require("tia.db_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 Global DB names (without quotes - will be wrapped in resolve)
|
|
local db_parser = require("tia.db_parser")
|
|
if db_parser.get_cache_count() == 0 then
|
|
local ws = require("tia.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 - expanded list
|
|
local basic_types = {
|
|
-- Elementary types
|
|
"BOOL", "BYTE", "WORD", "DWORD", "LWORD",
|
|
"CHAR", "WCHAR", "STRING", "WSTRING",
|
|
"INT", "DINT", "LINT", "USINT", "SINT", "UINT", "UDINT", "ULINT",
|
|
"REAL", "LREAL",
|
|
"TIME", "DATE", "TOD", "TIME_OF_DAY", "DATE_AND_TIME", "DT", "S5TIME", "LTIME", "DTL",
|
|
-- Timer and counter types
|
|
"IEC_TIMER", "TON_TIME", "TOF_TIME", "TONR_TIME", "TP_TIME",
|
|
"IEC_TON", "IEC_TOF", "IEC_TP", "IEC_COUNTER",
|
|
"CTU", "CTD", "CTUD",
|
|
}
|
|
for _, t in ipairs(basic_types) do
|
|
table.insert(items, { label = t, kind = 19 })
|
|
end
|
|
|
|
-- Add built-in function blocks
|
|
local builtin_fbs = {
|
|
-- Timers and counters
|
|
"TON", "TOF", "TP", "TONR",
|
|
"CTU", "CTD", "CTUD",
|
|
"IEC_TIMER", "IEC_TON", "IEC_TOF", "IEC_TP",
|
|
"IEC_COUNTER", "IEC_CTU", "IEC_CTD", "IEC_CTUD",
|
|
-- Bit logic
|
|
"R_TRIG", "F_TRIG", "SR", "RS",
|
|
-- Other
|
|
"SCHEDULE", "BPM", "LOG", "DATALOG",
|
|
}
|
|
for _, fb in ipairs(builtin_fbs) do
|
|
table.insert(items, { label = fb, kind = 6, detail = "Function block" })
|
|
end
|
|
|
|
-- Add built-in functions
|
|
local builtin_fcs = {
|
|
-- Math
|
|
"ADD", "SUB", "MUL", "DIV", "MOD", "ABS", "NEG", "SQR", "SQRT",
|
|
"EXP", "LN", "LOG", "SIN", "COS", "TAN", "ASIN", "ACOS", "ATAN",
|
|
"MIN", "MAX", "LIMIT", "CALCULATE",
|
|
-- Conversion
|
|
"CONVERT", "ROUND", "TRUNC", "CEIL", "FLOOR", "SCALE_X", "NORM_X",
|
|
"BCD_I", "I_BCD", "DI_BCD", "DI_R", "I_DI", "R_DI", "R_DB",
|
|
-- Move/String
|
|
"MOVE", "FILL", "SWAP", "PEEK", "POKE",
|
|
"LEN", "CONCAT", "LEFT", "RIGHT", "MID", "INSERT", "DELETE", "REPLACE", "FIND",
|
|
-- Bit logic
|
|
"AND", "OR", "XOR", "NOT", "SHL", "SHR", "ROL", "ROR", "DECO", "ENCO",
|
|
-- Time/Date
|
|
"T_CONV", "T_ADD", "T_SUB", "T_DIFF", "T_COMBINE",
|
|
"DT_DATE", "DT_TOD", "DATE_TO_DTL", "DTL_TO_DATE", "TOD_TO_DTL", "DTL_TO_TOD",
|
|
"TimeToTicks", "TicksToTime",
|
|
-- Error handling
|
|
"GET_ERROR", "GET_ERR_ID", "RESET_ERR", "GET_DIAG", "DPRINT",
|
|
-- Communication
|
|
"RDREC", "WRREC", "RALRM",
|
|
"TSEND", "TRCV", "TCON", "TDISCON", "TSEND_C", "TRCV_C",
|
|
"TUSEND", "TURCV", "TCONNECT", "TDISCONNECT",
|
|
"MODBUSPN", "MODBUSCP",
|
|
}
|
|
for _, fc in ipairs(builtin_fcs) do
|
|
table.insert(items, { label = fc, kind = 3, detail = "Function" })
|
|
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
|
|
-- 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
|
|
table.insert(items, { label = t, kind = 19 })
|
|
end
|
|
-- Add more basic types
|
|
local more_types = { "BOOL", "DINT", "WORD", "DWORD", "LWORD", "CHAR", "WSTRING", "UDINT", "LINT", "ULINT", "USINT", "SINT", "DATE", "TOD", "DTL", "LTIME", "S5TIME", "TIME_OF_DAY", "DATE_AND_TIME" }
|
|
for _, t in ipairs(more_types) do
|
|
table.insert(items, { label = t, kind = 19 })
|
|
end
|
|
-- Add built-in FBs
|
|
local builtin_fbs = {
|
|
"TON", "TOF", "TP", "TONR",
|
|
"CTU", "CTD", "CTUD",
|
|
"IEC_TIMER", "IEC_TON", "IEC_TOF", "IEC_TP",
|
|
"IEC_COUNTER", "IEC_CTU", "IEC_CTD", "IEC_CTUD",
|
|
"R_TRIG", "F_TRIG", "SR", "RS",
|
|
"SCHEDULE", "BPM", "LOG", "DATALOG",
|
|
}
|
|
for _, fb in ipairs(builtin_fbs) do
|
|
table.insert(items, { label = fb, kind = 6, detail = "Function block" })
|
|
end
|
|
-- Add built-in functions
|
|
local builtin_fcs = {
|
|
"ADD", "SUB", "MUL", "DIV", "MOD", "ABS", "NEG", "SQR", "SQRT",
|
|
"EXP", "LN", "LOG", "SIN", "COS", "TAN", "ASIN", "ACOS", "ATAN",
|
|
"MIN", "MAX", "LIMIT", "CALCULATE",
|
|
"CONVERT", "ROUND", "TRUNC", "CEIL", "FLOOR", "SCALE_X", "NORM_X",
|
|
"BCD_I", "I_BCD", "DI_BCD", "DI_R", "I_DI", "R_DI", "R_DB",
|
|
"MOVE", "FILL", "SWAP", "PEEK", "POKE",
|
|
"LEN", "CONCAT", "LEFT", "RIGHT", "MID", "INSERT", "DELETE", "REPLACE", "FIND",
|
|
"AND", "OR", "XOR", "NOT", "SHL", "SHR", "ROL", "ROR", "DECO", "ENCO",
|
|
"T_CONV", "T_ADD", "T_SUB", "T_DIFF", "T_COMBINE",
|
|
"DT_DATE", "DT_TOD", "DATE_TO_DTL", "DTL_TO_DATE", "TOD_TO_DTL", "DTL_TO_TOD",
|
|
"GET_ERROR", "GET_ERR_ID", "RESET_ERR", "GET_DIAG", "DPRINT",
|
|
"RDREC", "WRREC", "RALRM",
|
|
"TSEND", "TRCV", "TCON", "TDISCON", "TSEND_C", "TRCV_C",
|
|
"TUSEND", "TURCV", "MODBUSPN", "MODBUSCP",
|
|
}
|
|
for _, fc in ipairs(builtin_fcs) do
|
|
table.insert(items, { label = fc, kind = 3, detail = "Function" })
|
|
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
|
|
-- Check built-in instructions (IEC_TIMER, TON, etc.)
|
|
local builtin = require("tia.builtin_instructions")
|
|
local builtin_params = builtin.get_parameters(var_type)
|
|
if builtin_params then
|
|
local items = {}
|
|
for _, inp in ipairs(builtin_params.inputs or {}) do
|
|
table.insert(items, {
|
|
label = inp.name,
|
|
kind = vim.lsp.protocol.CompletionItemKind.Field,
|
|
detail = (inp.type or "ANY") .. " (Input)",
|
|
})
|
|
end
|
|
for _, out in ipairs(builtin_params.outputs or {}) do
|
|
table.insert(items, {
|
|
label = out.name,
|
|
kind = vim.lsp.protocol.CompletionItemKind.Field,
|
|
detail = (out.type or "ANY") .. " (Output)",
|
|
})
|
|
end
|
|
wrapped_callback({ items = items })
|
|
return
|
|
end
|
|
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
|