docs: encourage adding comments; fix nested IF/ELSIF indentation

- Update AGENTS.md to encourage comments instead of forbidding them
- Add docstrings to all public functions in src/ modules
- Add module headers and function comments to lua/scl/ modules
- Fix formatter to properly indent multi-line IF/ELSIF conditions
- Fix FB call detection to not match IF statements with parentheses
This commit is contained in:
2026-02-23 17:40:44 +01:00
parent db69592cc1
commit 37f8d475f0
9 changed files with 301 additions and 18 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ return M
- No trailing whitespace
### Comments
**DO NOT ADD COMMENTS** in code unless explicitly requested by the user.
Add comments to explain non-obvious code sections, complex logic, and public API functions. Keep comments concise and relevant.
### Parser Module API
All parser modules must implement:
+45
View File
@@ -1,5 +1,9 @@
-- TIA Portal built-in instructions for SCL LSP
-- Defines functions, function blocks, data types, and their parameters
local M = {}
-- Built-in functions (return values, no state)
M.FUNCTIONS = {
ADD = true,
SUB = true,
@@ -169,6 +173,7 @@ M.FUNCTIONS = {
DCP_CLIENT = true,
}
-- Built-in function blocks (have state, need instance)
M.FUNCTION_BLOCKS = {
TON = true,
TOF = true,
@@ -226,6 +231,8 @@ M.DATA_TYPES = {
DTL = true,
}
-- Parameter definitions for built-in instructions
-- Format: { inputs = { { name, type }, ... }, outputs = { { name, type }, ... } }
M.PARAMETERS = {
TON = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } },
TOF = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } },
@@ -283,10 +290,48 @@ M.PARAMETERS = {
I_BCD = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } },
}
-- Get parameter info for a built-in instruction
-- @param name string The instruction name
-- @return table|nil Parameter definition with inputs and outputs arrays
function M.get_parameters(name)
return M.PARAMETERS[name:upper()]
end
-- Check if name is a known function block
-- @param name string The name to check
-- @return boolean True if known function block
function M.is_known_function_block(name)
return M.FUNCTION_BLOCKS[name:upper()] or false
end
-- Check if name is a known function
-- @param name string The name to check
-- @return boolean True if known function
function M.is_known_function(name)
return M.FUNCTIONS[name:upper()] or false
end
-- Check if name is a known data type
-- @param name string The name to check
-- @return boolean True if known data type
function M.is_known_data_type(name)
return M.DATA_TYPES[name:upper()] or false
end
-- Check if name is a known instruction (function or function block)
-- @param name string The name to check
-- @return boolean True if known instruction
function M.is_known_instruction(name)
return M.is_known_function(name) or M.is_known_function_block(name) or false
end
-- Check if name is a known type or instruction
-- @param name string The name to check
-- @return boolean True if known type or instruction
function M.is_known_type_or_instruction(name)
return M.is_known_data_type(name) or M.is_known_instruction(name) or false
end
function M.is_known_function(name)
return M.FUNCTIONS[name:upper()] or false
end
+19
View File
@@ -1,4 +1,6 @@
-- SCL Diagnostics - Linter functionality for the LSP server
-- Validates SCL code for undefined variables, unknown types, and syntax errors
local M = {}
-- Get script path for loading parser (same pattern as main.lua)
@@ -18,6 +20,7 @@ package.path = package.path .. ";" .. script_path .. "/?.lua"
local parser = dofile(script_path .. "/parser.lua")
local builtin = dofile(script_path .. "/builtin_instructions.lua")
-- LSP diagnostic severity levels
local DiagnosticSeverity = {
Error = 1,
Warning = 2,
@@ -25,6 +28,10 @@ local DiagnosticSeverity = {
Hint = 4,
}
--- Convert line/character to LSP position format
--- @param line number Zero-based line number
--- @param character number Zero-based character offset
--- @return table LSP Position object
local function position_to_lsp(line, character)
return {
line = line,
@@ -32,6 +39,12 @@ local function position_to_lsp(line, character)
}
end
--- Create LSP Range object from start and end positions
--- @param start_line number Start line (zero-based)
--- @param start_char number Start character (zero-based)
--- @param end_line number End line (zero-based)
--- @param end_char number End character (zero-based)
--- @return table LSP Range object
local function range_to_lsp(start_line, start_char, end_line, end_char)
return {
start = position_to_lsp(start_line, start_char),
@@ -39,6 +52,12 @@ local function range_to_lsp(start_line, start_char, end_line, end_char)
}
end
--- Validate SCL content and generate diagnostics
--- Checks for: undefined variables, invalid # prefix, unknown data types
--- @param content string The SCL source code to validate
--- @param workspace_types table Known types from workspace scanning
--- @param root_dir string Project root directory for additional type lookup
--- @return table Array of LSP Diagnostic objects
function M.validate_diagnostics(content, workspace_types, root_dir)
local diagnostics = {}
local lines = {}
+56 -15
View File
@@ -1,5 +1,6 @@
-- SCL Formatter - Document formatting for the LSP server
-- Stack-based implementation for proper nested structure handling
local M = {}
-- Default patterns for attribute blocks to collapse
@@ -10,10 +11,20 @@ local DEFAULT_COLLAPSE_PATTERNS = {
"^%s*{%s*%w+%s*:=",
}
--- Create LSP Position object
--- @param line number Line number
--- @param character number Character offset
--- @return table Position object
local function position(line, character)
return { line = line, character = character }
end
--- Create LSP Range object
--- @param start_line number Start line
--- @param start_char number Start character
--- @param end_line number End line
--- @param end_char number End character
--- @return table Range object
local function range(start_line, start_char, end_line, end_char)
return {
start = position(start_line, start_char),
@@ -89,6 +100,11 @@ local CONTINUATION_OPERATORS = {
["MOD"] = true,
}
--- Format SCL document content
--- Handles indentation, attribute collapsing, and multiline FB calls
--- @param content string The SCL source code
--- @param options table Formatting options (insertSpaces, tabSize, collapseAttributes)
--- @return table Array of TextEdit objects with range and newText
function M.format_document(content, options)
options = options or {}
local use_spaces = options.insertSpaces or false
@@ -147,6 +163,15 @@ function M.format_document(content, options)
end
local function is_fb_call_start(trimmed)
local first_word = trimmed:match("^(#?[%w_]+)")
if not first_word then return false end
-- Don't treat control structure keywords as FB calls
local control_keywords = { IF = true, ELSIF = true, FOR = true, WHILE = true, CASE = true, WITH = true }
if control_keywords[first_word:upper()] then
return false
end
if not (trimmed:match("^#?%w+") or trimmed:match('^"')) then
return false
end
@@ -341,10 +366,14 @@ function M.format_document(content, options)
-- Check if continuing condition (IF/ELSIF condition before THEN)
if in_condition and not ends_assignment then
local starts_with_operator = is_assignment_continuation(trimmed)
local first_word = trimmed:match("^([%w_]+)")
local is_then = first_word and first_word:upper() == "THEN"
if starts_with_operator or is_then then
local is_else = first_word and first_word:upper() == "ELSE"
local is_elsif = first_word and first_word:upper() == "ELSIF"
local is_ender = trimmed:match("^END_") ~= nil
local starts_new_statement = is_then or is_else or is_elsif or is_ender
if not starts_new_statement then
table.insert(formatted_lines, condition_indent .. trimmed)
if is_then then
in_condition = false
@@ -474,19 +503,20 @@ function M.format_document(content, options)
if SAME_LEVEL[kw] then
local formatted
if kw == "THEN" then
local if_block = nil
for i = #stack, 1, -1 do
if stack[i].type == "IF" then
if_block = stack[i]
break
end
end
if if_block then
formatted = string.rep(indent_char, if_block.indent) .. trimmed
local then_indent
if in_condition and condition_indent ~= "" then
then_indent = condition_indent
else
formatted = get_indent() .. trimmed
local if_block = nil
for i = #stack, 1, -1 do
if stack[i].type == "IF" then
if_block = stack[i]
break
end
end
then_indent = if_block and string.rep(indent_char, if_block.indent) or get_indent()
end
table.insert(formatted_lines, formatted)
table.insert(formatted_lines, then_indent .. trimmed)
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
in_condition = false
elseif kw == "ELSE" or kw == "ELSIF" then
@@ -507,6 +537,11 @@ function M.format_document(content, options)
end
table.insert(formatted_lines, formatted)
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
-- Check if this is a multi-line condition (doesn't end with THEN on same line)
if not trimmed:match("THEN%s*$") then
in_condition = true
condition_indent = string.rep(indent_char, (if_block and if_block.indent or 0) + 1)
end
else
formatted = get_indent() .. trimmed
table.insert(formatted_lines, formatted)
@@ -540,8 +575,12 @@ function M.format_document(content, options)
table.insert(formatted_lines, formatted)
table.insert(stack, { type = kw, indent = current_level })
if kw == "IF" or kw == "ELSIF" then
in_condition = true
condition_indent = string.rep(indent_char, current_level)
-- Check if this is a multi-line condition (doesn't end with THEN on same line)
if not trimmed:match("THEN%s*$") then
in_condition = true
-- Continuation lines should be indented one level deeper than IF/ELSIF
condition_indent = string.rep(indent_char, current_level + 1)
end
end
goto continue
end
@@ -587,6 +626,8 @@ function M.format_document(content, options)
}
end
--- Get default formatting options
--- @return table Default options table
function M.get_formatting_options()
return {
insertSpaces = false,
+40
View File
@@ -1,5 +1,11 @@
-- JSON Encoder/Decoder for LSP server
-- Minimal implementation supporting basic JSON parsing and encoding
local json = {}
--- Check if a table is an array (sequential numeric keys starting from 1)
--- @param t table Table to check
--- @return boolean True if table is an array
local function is_array(t)
local i = 0
for _ in pairs(t) do
@@ -11,6 +17,9 @@ local function is_array(t)
return true
end
--- Escape special characters in string for JSON
--- @param s string String to escape
--- @return string Escaped string
local function escape(s)
s = s:gsub("\\", "\\\\")
s = s:gsub('"', '\\"')
@@ -20,6 +29,9 @@ local function escape(s)
return s
end
--- Encode Lua value to JSON string
--- @param data any Value to encode
--- @return string JSON string
function json.encode(data)
local t = type(data)
if t == "nil" then
@@ -48,6 +60,10 @@ function json.encode(data)
end
end
--- Skip whitespace in string
--- @param s string JSON string
--- @param i number Current position
--- @return number New position after whitespace
local function skip_whitespace(s, i)
while i <= #s and s:sub(i, i):match("%s") do
i = i + 1
@@ -55,6 +71,11 @@ local function skip_whitespace(s, i)
return i
end
--- Parse JSON string literal
--- @param s string JSON string
--- @param i number Current position (at opening quote)
--- @return string|nil Parsed string
--- @return number New position
local function parse_string(s, i)
local result = {}
i = i + 1
@@ -86,6 +107,11 @@ local function parse_string(s, i)
return nil, i
end
--- Parse JSON number
--- @param s string JSON string
--- @param i number Current position
--- @return number Parsed number
--- @return number New position
local function parse_number(s, i)
local start = i
if s:sub(i, i) == "-" then
@@ -114,6 +140,11 @@ end
local parse_value
--- Parse JSON array
--- @param s string JSON string
--- @param i number Current position (at opening bracket)
--- @return table|nil Parsed array
--- @return number New position
local function parse_array(s, i)
local arr = {}
i = i + 1
@@ -136,6 +167,11 @@ local function parse_array(s, i)
return nil, i
end
--- Parse JSON object
--- @param s string JSON string
--- @param i number Current position (at opening brace)
--- @return table|nil Parsed object
--- @return number New position
local function parse_object(s, i)
local obj = {}
i = i + 1
@@ -205,6 +241,10 @@ parse_value = function(s, i)
end
end
--- Decode JSON string to Lua value
--- @param s string JSON string
--- @return any|nil Parsed value
--- @return number|nil Position after parsing or error
function json.decode(s)
if type(s) ~= "string" then
return nil, "invalid input"
+67
View File
@@ -154,6 +154,9 @@ end
local handlers = {}
--- Handle LSP initialize request
--- @param params table Initialization parameters from client
--- @return table Server capabilities and info
function handlers.initialize(params)
return {
capabilities = capabilities,
@@ -161,8 +164,13 @@ function handlers.initialize(params)
}
end
--- Handle initialized notification (no response needed)
--- @param params table Parameters (usually empty)
function handlers.initialized(params) end
--- Handle shutdown request
--- @param params table Parameters (usually empty)
--- @return nil
function handlers.shutdown(params)
return nil
end
@@ -206,10 +214,15 @@ local function find_project_root(start_path)
return nil
end
--- Handle exit notification - terminates the server
--- @param params table Parameters (usually empty)
function handlers.exit(params)
os.exit(0)
end
--- Handle textDocument/didOpen notification
--- Parses document content, extracts symbols, and stores in documents cache
--- @param params table Contains textDocument with uri, text, version
function handlers.textDocument_didOpen(params)
local uri = params.textDocument.uri
local content = params.textDocument.text
@@ -247,6 +260,9 @@ function handlers.textDocument_didOpen(params)
}
end
--- Handle textDocument/didChange notification
--- Re-parses document content after edits
--- @param params table Contains textDocument with uri and contentChanges array
function handlers.textDocument_didChange(params)
local uri = params.textDocument.uri
local content = params.contentChanges[1].text
@@ -285,11 +301,18 @@ function handlers.textDocument_didChange(params)
clear_symbol_cache()
end
--- Handle textDocument/didClose notification
--- Removes document from cache
--- @param params table Contains textDocument with uri
function handlers.textDocument_didClose(params)
documents[params.textDocument.uri] = nil
clear_symbol_cache()
end
--- Handle textDocument/hover request
--- Provides hover information for variables, types, and built-in instructions
--- @param params table Contains textDocument with uri and position
--- @return table|nil Hover result with contents and range, or nil
function handlers.textDocument_hover(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -699,6 +722,10 @@ function handlers.textDocument_hover(params)
return nil
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
--- @return table Completion list with items array
function handlers.textDocument_completion(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -879,6 +906,10 @@ function handlers.textDocument_completion(params)
return { isIncomplete = false, items = items }
end
--- Handle textDocument/definition request
--- Finds definition location for symbol at position
--- @param params table Contains textDocument with uri and position
--- @return table|nil Location with uri and range, or nil
function handlers.textDocument_definition(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -921,6 +952,10 @@ function handlers.textDocument_definition(params)
return nil
end
--- Handle textDocument/declaration request
--- Finds declaration locations for symbol at position across workspace
--- @param params table Contains textDocument with uri and position
--- @return table|nil Array of locations, or nil
function handlers.textDocument_declaration(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -1141,6 +1176,10 @@ function handlers.textDocument_declaration(params)
return nil
end
--- Handle textDocument/references request
--- Finds all references to symbol at position in current document
--- @param params table Contains textDocument with uri and position
--- @return table|nil Array of reference locations, or nil
function handlers.textDocument_references(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -1202,6 +1241,10 @@ function handlers.textDocument_references(params)
return nil
end
--- Handle textDocument/documentSymbol request
--- Returns all symbols in document for outline/navigation
--- @param params table Contains textDocument with uri
--- @return table Array of DocumentSymbol objects
function handlers.textDocument_documentSymbol(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -1286,6 +1329,10 @@ function handlers.textDocument_documentSymbol(params)
return symbols
end
--- Handle textDocument/diagnostic request
--- Validates document and returns diagnostics
--- @param params table Contains textDocument with uri
--- @return table Diagnostic report with items array
function handlers.textDocument_diagnostic(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -1302,6 +1349,10 @@ function handlers.textDocument_diagnostic(params)
return { items = diag_results }
end
--- Handle textDocument/semanticTokens/full request
--- Returns semantic tokens for syntax highlighting (currently disabled)
--- @param params table Contains textDocument with uri
--- @return table Semantic tokens with data array
function handlers.textDocument_semanticTokensFull(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -1399,6 +1450,10 @@ function handlers.textDocument_semanticTokensFull(params)
return { data = tokens }
end
--- Handle textDocument/semanticTokens/range request
--- Returns semantic tokens for a range (delegates to full)
--- @param params table Contains textDocument with uri and range
--- @return table Semantic tokens with data array
function handlers.textDocument_semanticTokensRange(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -1412,6 +1467,10 @@ end
handlers["textDocument_semanticTokens_full"] = handlers.textDocument_semanticTokensFull
handlers["textDocument_semanticTokens_range"] = handlers.textDocument_semanticTokensRange
--- Handle textDocument/inlayHint request
--- Provides inlay hints for variable types in VAR sections
--- @param params table Contains textDocument with uri and range
--- @return table Array of InlayHint objects
function handlers.textDocument_inlayHint(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -1449,6 +1508,10 @@ function handlers.textDocument_inlayHint(params)
return hints
end
--- Handle workspace/symbol request
--- Searches workspace for symbols matching query
--- @param params table Contains query string and optional maxResults
--- @return table Array of SymbolInformation objects
function handlers.workspace_symbol(params)
local query = params.query or ""
local max_results = params.maxResults or 50
@@ -1586,6 +1649,10 @@ function handlers.workspace_symbol(params)
return final_results
end
--- Handle textDocument/formatting request
--- Formats entire document using SCL formatter
--- @param params table Contains textDocument with uri and options
--- @return table|nil Array of TextEdit objects, or nil
function handlers.textDocument_formatting(params)
local uri = params.textDocument.uri
local doc = documents[uri]
+12
View File
@@ -1,6 +1,12 @@
-- SCL Parser utilities for the LSP server
-- Extracts variables, types, and function definitions from SCL source code
local M = {}
--- Extract variables from SCL content
--- @param content string The SCL source code
--- @return table variables Table of variable definitions keyed by name
--- @return table variable_positions Table of variable positions keyed by name
function M.extract_variables(content)
local variables = {}
local variable_positions = {}
@@ -164,6 +170,9 @@ function M.extract_variables(content)
return variables, variable_positions
end
--- Extract type definitions from SCL content
--- @param content string The SCL source code
--- @return table types Table of type definitions keyed by name
function M.extract_types(content)
local types = {}
@@ -285,6 +294,9 @@ function M.extract_types(content)
return types
end
--- Extract function/block definitions from SCL content
--- @param content string The SCL source code
--- @return table functions Array of function definitions with name, kind, start, final
function M.extract_functions(content)
local functions = {}
+33 -1
View File
@@ -1,10 +1,14 @@
-- Load UDT types from external .plc.json files
-- Auto-generate from data_types folder
-- Auto-generate from data_types folder and .scl/.udt/.db files
local M = {}
-- Cache for loaded types: { [root_dir] = { [type_name] = type_info } }
local cached_types = {}
--- Execute shell command and capture output
--- @param cmd string Command to execute
--- @return string|nil Command output, or nil on error
local function run_command(cmd)
local handle = io.popen(cmd)
if not handle then return nil end
@@ -26,6 +30,9 @@ else
script_path = "."
end
--- Load and parse JSON content
--- @param content string JSON string
--- @return table|nil Parsed data, or nil on error
local function load_json(content)
local json = dofile(script_path .. "/json.lua")
local ok, data = pcall(function() return json.decode(content) end)
@@ -33,6 +40,10 @@ local function load_json(content)
return nil
end
--- Scan directory recursively for files matching pattern
--- @param dir string Directory to scan
--- @param pattern string Glob pattern for file names
--- @return table Array of matching file paths
local function scan_files_recursive(dir, pattern)
local files = {}
local result = run_command("find " .. dir .. " -type f -name \"" .. pattern .. "\" 2>/dev/null")
@@ -44,6 +55,9 @@ local function scan_files_recursive(dir, pattern)
return files
end
--- Parse a PLC type definition from JSON structure
--- @param dt table Type definition from JSON
--- @return table|nil Parsed type info, or nil if invalid
local function parse_plc_type(dt)
local type_name = dt.name or dt.Name or dt.baseType or dt.dataTypeName
if not type_name then return nil end
@@ -78,6 +92,9 @@ local function parse_plc_type(dt)
return typ
end
--- Parse SCL type file content (TYPE...END_TYPE, FUNCTION_BLOCK, etc.)
--- @param content string SCL source code
--- @return table types Parsed types keyed by name
local function parse_scl_type_file(content)
local types = {}
local in_type = false
@@ -244,6 +261,9 @@ local function parse_scl_type_file(content)
return types
end
--- Parse DB file content (DATA_BLOCK...END_DATA_BLOCK)
--- @param content string DB source code
--- @return table types Parsed DB types keyed by name
local function parse_db_file(content)
local types = {}
local in_var = false
@@ -288,6 +308,10 @@ local function parse_db_file(content)
return types
end
--- Load all types from workspace directory
--- Scans .plc.json, .scl, .udt, and .db files
--- @param root_dir string Project root directory
--- @return table Types keyed by name
function M.load_types_from_workspace(root_dir)
local types = {}
@@ -375,6 +399,9 @@ function M.load_types_from_workspace(root_dir)
return types
end
--- Get types for a workspace (cached)
--- @param root_dir string Project root directory
--- @return table Types keyed by name
function M.get_types(root_dir)
if not root_dir or root_dir == "" then
root_dir = "workspace"
@@ -389,6 +416,10 @@ function M.get_types(root_dir)
return types
end
--- Generate .plc.json file from workspace types
--- @param root_dir string Project root directory
--- @param output_file string Output file path
--- @return boolean True if file was written successfully
function M.generate_plc_json(root_dir, output_file)
local json = dofile(script_path .. "/json.lua")
@@ -435,6 +466,7 @@ function M.generate_plc_json(root_dir, output_file)
return false
end
--- Clear all cached types
function M.clear_cache()
cached_types = {}
end
+28 -1
View File
@@ -1,10 +1,13 @@
-- Tree-sitter integration for SCL LSP
-- Uses tree-sitter to parse SCL files more accurately
-- Uses tree-sitter to parse SCL files more accurately than regex parsing
local M = {}
local ts = nil
--- Initialize tree-sitter parser
--- Attempts to load vim.treesitter module
--- @return boolean True if tree-sitter is available
function M.init()
local ok, treesitter = pcall(require, "vim.treesitter")
if ok then
@@ -14,6 +17,10 @@ function M.init()
return false
end
--- Parse SCL content using tree-sitter
--- @param content string The SCL source code
--- @return table|nil Parse tree, or nil on error
--- @return string|nil Error message if parsing failed
function M.parse_scl(content)
if not ts then
return nil, "Tree-sitter not available"
@@ -32,6 +39,11 @@ function M.parse_scl(content)
return tree, nil
end
--- Extract variables from tree-sitter parse tree
--- @param tree table Parse tree from parse_scl
--- @param bufnr number Buffer number for text retrieval
--- @return table variables Table of variable definitions
--- @return table variable_positions Table of variable positions
function M.extract_variables_ts(tree, bufnr)
local variables = {}
local variable_positions = {}
@@ -79,6 +91,10 @@ function M.extract_variables_ts(tree, bufnr)
return variables, variable_positions
end
--- Extract function/block definitions from tree-sitter parse tree
--- @param tree table Parse tree from parse_scl
--- @param bufnr number Buffer number for text retrieval
--- @return table functions Array of function definitions
function M.extract_functions_ts(tree, bufnr)
local functions = {}
@@ -109,6 +125,11 @@ function M.extract_functions_ts(tree, bufnr)
return functions
end
--- Get syntax node at position
--- @param tree table Parse tree
--- @param row number Row number (zero-based)
--- @param col number Column number (zero-based)
--- @return table|nil Node at position, or nil if not found
function M.get_node_at_position(tree, row, col)
if not tree then return nil end
@@ -124,6 +145,12 @@ function M.get_node_at_position(tree, row, col)
return nil
end
--- Get variable type at cursor position
--- @param tree table Parse tree
--- @param bufnr number Buffer number
--- @param row number Row number (zero-based)
--- @param col number Column number (zero-based)
--- @return string|nil Variable type, or nil if not found
function M.get_variable_type_at_cursor(tree, bufnr, row, col)
if not tree or not ts then return nil end