diff --git a/AGENTS.md b/AGENTS.md index 0f50882..55fad39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ lua test/test_formatter.lua # Run formatter tests lua test/test_plc_json.lua # Run plc_json tests # Reference Project -# Tests use files from: ~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/ +# Tests use files from: ~/Documents/siemens/scl_lang_support_lazyvim_ref_project/ # Override with: SCL_REF_PROJECT=/path/to/ref lua tests/run_tests.lua ``` @@ -152,7 +152,7 @@ The LSP validates data types and provides warnings for unknown types: Use the reference project for testing: ``` -~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/ +~/Documenta/siemens/scl_lang_support_lazyvim_ref_project/ ``` Contains `.scl`, `.db`, `.udt`, and `.xml` files for comprehensive testing of: diff --git a/src/main.lua b/src/main.lua index fd0c0cf..fb7b2b9 100644 --- a/src/main.lua +++ b/src/main.lua @@ -717,6 +717,72 @@ function handlers.textDocument_hover(params) return nil end +--- Detect if cursor is inside a function call's parentheses +--- Scans backwards from cursor to find an unmatched ( and returns the name before it +--- @param line string The current line content +--- @param col number Cursor column (0-indexed LSP position) +--- @return string|nil Function/block name, or nil if not in a call context +local function get_call_context(line, col) + if col <= 1 then return nil end + local before_cursor = line:sub(1, col) + local paren_pos = nil + for i = #before_cursor, 1, -1 do + local c = before_cursor:sub(i, i) + if c == ")" then break end + if c == "(" then paren_pos = i; break end + end + if not paren_pos then return nil end + local before_paren = before_cursor:sub(1, paren_pos - 1) + return before_paren:match("([%w_]+)%s*$") +end + +--- Resolve a function/variable call name to its parameter info +--- Checks variables (for data_type resolution), built-in parameters, and workspace types +--- @param call_name string The name to resolve (e.g., "instTestTimer" or "TON") +--- @param variables table|nil Document variables table +--- @param types table|nil Document types table +--- @return table|nil Parameter definition with inputs and outputs arrays, or nil +local function resolve_call_params(call_name, variables, types) + if not call_name then return nil end + local clean_name = call_name:gsub("^#", "") + local data_type = variables and variables[clean_name] and variables[clean_name].data_type + local resolved_name = data_type or clean_name + local builtin_params = builtin.get_parameters(resolved_name) + if builtin_params then return builtin_params end + local type_info = types and types[resolved_name] + if type_info and type_info.fields then + return { inputs = type_info.fields, outputs = {} } + end + return nil +end + +local function add_param_items(items, params, prefix_filter) + for _, inp in ipairs(params.inputs or {}) do + local label = inp.name .. " := " + if prefix_filter == "" or inp.name:upper():find("^" .. prefix_filter:upper(), 1, true) then + table.insert(items, { + label = label, + kind = 13, + detail = "Parameter (" .. (inp.type or "ANY") .. ")", + insertText = inp.name .. " := ", + insertTextFormat = 1, + }) + end + end + for _, out in ipairs(params.outputs or {}) do + local label = out.name .. " => " + if prefix_filter == "" or out.name:upper():find("^" .. prefix_filter:upper(), 1, true) then + table.insert(items, { + label = label, + kind = 13, + detail = "Output (" .. (out.type or "ANY") .. ")", + insertText = out.name .. " => ", + insertTextFormat = 1, + }) + end + end +end + --- Handle textDocument/completion request --- Provides completion items based on context (VAR section, member access, etc.) --- @param params table Contains textDocument with uri and position @@ -738,7 +804,7 @@ function handlers.textDocument_completion(params) if line_idx <= #lines then local line = lines[line_idx] local col = params.position.character - local trigger = col > 0 and line:sub(col, col) or "" + local trigger = (params.context and params.context.triggerCharacter) or (col > 0 and line:sub(col, col) or "") local line_before = col > 1 and line:sub(1, col - 1) or "" -- Get word prefix being typed (for filtering) @@ -748,29 +814,35 @@ function handlers.textDocument_completion(params) end local word_prefix = line:sub(prefix_start + 1, col - 1) - -- Check if we're in VAR section + -- Check if typing after # prefix (e.g., #myVar) + local has_hash_prefix = line_before:match("#[%w_]*$") + + -- Check if we're in VAR section (detects all VAR variants) local in_var_section = false for i = line_idx - 1, 1, -1 do local prev_line = lines[i]:gsub("^%s+", ""):gsub("%s+$", "") - if prev_line:match("^END_VAR") or prev_line:match("^VAR_TEMP") or prev_line:match("^VAR_IN_OUT") or prev_line:match("^VAR_OUTPUT") or prev_line:match("^VAR_INPUT") then + if prev_line:match("^END_VAR") then break end - if prev_line:match("^VAR%s*$") or prev_line:match("^VAR$") then + if prev_line:match("^VAR") then in_var_section = true break end end - if trigger == "#" then + if trigger == "#" or has_hash_prefix then if doc.variables then + local prefix = has_hash_prefix and line_before:match("#([%w_]*)$") or word_prefix for name, info in pairs(doc.variables) do - table.insert(items, { - label = "#" .. name, - kind = 13, - detail = "Local variable (" .. (info.type or "unknown") .. ")", - insertText = "#" .. name, - insertTextFormat = 1, - }) + if prefix == "" or name:upper():find("^" .. prefix:upper(), 1, true) then + table.insert(items, { + label = "#" .. name, + kind = 13, + detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"), + insertText = "#" .. name, + insertTextFormat = 1, + }) + end end end elseif trigger == "." then @@ -837,7 +909,16 @@ function handlers.textDocument_completion(params) end end elseif trigger == "(" then - if doc.functions then + -- Try get_call_context first (handles case where ( is already in the line) + local call_name = get_call_context(line, col) + -- Fallback: ( may not be in the line yet (client sends completion before didChange) + if not call_name and col > 0 then + call_name = line:sub(1, col):match("([%w_]+)%s*$") + end + local params = call_name and resolve_call_params(call_name, doc.variables, doc.types) + if params then + add_param_items(items, params, "") + elseif doc.functions then for _, func in ipairs(doc.functions) do table.insert(items, { label = func.name, @@ -849,6 +930,10 @@ function handlers.textDocument_completion(params) end end else + -- Check if we're inside a function call (e.g., after instTestTimer() + local call_name = get_call_context(line, col) + local call_params = call_name and resolve_call_params(call_name, doc.variables, doc.types) + -- Check context before cursor local is_var_decl = line_before:match(":%s*$") @@ -881,6 +966,26 @@ function handlers.textDocument_completion(params) add_item(type_name, 7, type_info.kind or "Type") end end + + -- Add declared variables (without # prefix in label, insert with #) + if doc.variables then + for name, info in pairs(doc.variables) do + if word_prefix == "" or name:upper():find("^" .. word_prefix:upper(), 1, true) then + table.insert(items, { + label = "#" .. name, + kind = 13, + detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"), + insertText = "#" .. name, + insertTextFormat = 1, + }) + end + end + end + + -- Add function/block call parameters if inside (...) context + if call_params then + add_param_items(items, call_params, word_prefix) + end else -- General context: show all for type_name, _ in pairs(builtin.DATA_TYPES) do @@ -894,6 +999,26 @@ function handlers.textDocument_completion(params) for func_name, _ in pairs(builtin.FUNCTIONS) do add_item(func_name, 3, "Function") end + + -- Add declared variables in general context too + if doc.variables then + for name, info in pairs(doc.variables) do + if word_prefix == "" or name:upper():find("^" .. word_prefix:upper(), 1, true) then + table.insert(items, { + label = "#" .. name, + kind = 13, + detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"), + insertText = "#" .. name, + insertTextFormat = 1, + }) + end + end + end + + -- Add function/block call parameters if inside (...) context + if call_params then + add_param_items(items, call_params, word_prefix) + end end end end