From cb0b24d47bdad053ff77881216eaa43cda2cbb9e Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Thu, 19 Feb 2026 14:10:14 +0100 Subject: [PATCH 1/5] feat(lsp): add references provider for goto references Add referencesProvider capability and handler to support LSP references (gr keybinding in LazyVim). Searches all occurrences of a symbol in the current document. --- src/main.lua | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/main.lua b/src/main.lua index 9fb8ecf..1f5bde7 100644 --- a/src/main.lua +++ b/src/main.lua @@ -61,6 +61,7 @@ local capabilities = { resolveProvider = false, }, definitionProvider = true, + referencesProvider = true, documentSymbolProvider = true, diagnosticProvider = { relatedDocuments = {} }, textDocumentSync = 1, @@ -456,6 +457,67 @@ function handlers.textDocument_definition(params) return nil end +function handlers.textDocument_references(params) + local uri = params.textDocument.uri + local doc = documents[uri] + if not doc then + return nil + end + + local lines = {} + for line in doc.content:gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + if params.position.line + 1 > #lines then + return nil + end + + local line = lines[params.position.line + 1] + local col = params.position.character + 1 + + local word_start = col + while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do + word_start = word_start - 1 + end + + local word_end = col + while word_end <= #line and line:sub(word_end, word_end):match("[%w_]") do + word_end = word_end + 1 + end + + local word = line:sub(word_start, word_end - 1):gsub("^#", "") + if not word or word == "" then + return nil + end + + local references = {} + for line_num, line_content in ipairs(lines) do + local search_col = 1 + while search_col <= #line_content do + local match_start, match_end = line_content:find("[%w_]+", search_col) + if not match_start then + break + end + local matched_word = line_content:sub(match_start, match_end) + if matched_word == word then + table.insert(references, { + uri = uri, + range = { + start = { line = line_num - 1, character = match_start - 1 }, + ["end"] = { line = line_num - 1, character = match_end }, + }, + }) + end + search_col = match_end + 1 + end + end + + if #references > 0 then + return references + end + return nil +end + function handlers.textDocument_documentSymbol(params) local uri = params.textDocument.uri local doc = documents[uri] From 577ffa52139cb74e03ba4af0f97430f897ff8e48 Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Thu, 19 Feb 2026 18:01:30 +0100 Subject: [PATCH 2/5] feat(lsp): add goto declaration for FB, DB, and UDT types - Add declaration provider (gD) that finds definitions for: - Functions in .scl files - Function Blocks (FB) in .scl files - Data Blocks (DB) in .scl and .db files - User-Defined Types (UDT) in .scl and .udt files - Implement project root detection for cross-directory searches - Add duplicate location filtering to prevent multiple results - Load UDT types from .udt files in plc_json - Update documentation with goto declaration feature --- AGENTS.md | 11 ++ src/main.lua | 266 ++++++++++++++++++++++++++++++++++++++++++++++- src/parser.lua | 14 +-- src/plc_json.lua | 15 +++ 4 files changed, 297 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d0c9551..1fbea2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -235,6 +235,17 @@ Rules are checked in order; first match wins. | `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer | | `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks | +## LSP Features + +### Goto Declaration (`gD`) +The LSP supports goto declaration functionality for: +- **Functions** - Jump to function definition in `.scl` files +- **Function Blocks (FB)** - Jump to FB definition in `.scl` files +- **Data Blocks (DB)** - Jump to DB definition in `.scl` and `.db` files +- **User-Defined Types (UDT)** - Jump to type definition in `.scl` and `.udt` files + +Usage: Place cursor on a variable or type name and press `gD` (or use LSP client command). + ## Keybindings (SCL/UDT files) ### Attribute Block Toggle diff --git a/src/main.lua b/src/main.lua index 1f5bde7..9853157 100644 --- a/src/main.lua +++ b/src/main.lua @@ -62,6 +62,7 @@ local capabilities = { }, definitionProvider = true, referencesProvider = true, + declarationProvider = true, documentSymbolProvider = true, diagnosticProvider = { relatedDocuments = {} }, textDocumentSync = 1, @@ -99,7 +100,7 @@ local function scan_scl_files(root_dir) if not root_dir or root_dir == "" then return files end - local result = run_command("find " .. root_dir .. ' -type f -name "*.scl" 2>/dev/null') + local result = run_command('find "' .. root_dir .. '" -type f -name "*.scl" 2>/dev/null') if result then for f in result:gmatch("[^\n]+") do if f and f ~= "" then @@ -165,6 +166,45 @@ function handlers.shutdown(params) return nil end +local project_markers = { + "Program blocks", + "PLC data types", + "PLC tags", + "program blocks", + "plc data types", + "plc tags", + ".git", +} + +local function find_project_root(start_path) + if not start_path or start_path == "" then + return nil + end + local current = start_path + local depth = 0 + while current and current ~= "/" and current ~= "" and depth < 20 do + local handle = io.popen('ls -a "' .. current .. '" 2>/dev/null') + if handle then + local entries = {} + for line in handle:lines() do + table.insert(entries, line) + end + handle:close() + local found_marker = false + for _, marker in ipairs(project_markers) do + for _, entry in ipairs(entries) do + if entry == marker then + return current + end + end + end + end + current = current:match("(.*)/") or "" + depth = depth + 1 + end + return nil +end + function handlers.exit(params) os.exit(0) end @@ -173,7 +213,8 @@ function handlers.textDocument_didOpen(params) local uri = params.textDocument.uri local content = params.textDocument.text local path = uri_to_path(uri) - local root_dir = path:match("(.*/)") or "." + local file_dir = path:match("(.*/)") or "." + local root_dir = find_project_root(file_dir) or file_dir local variables, var_positions = parser.extract_variables(content) local functions = parser.extract_functions(content) @@ -457,6 +498,226 @@ function handlers.textDocument_definition(params) return nil end +function handlers.textDocument_declaration(params) + local uri = params.textDocument.uri + local doc = documents[uri] + if not doc then + return nil + end + + local lines = {} + for line in doc.content:gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + if params.position.line + 1 > #lines then + return nil + end + + local line = lines[params.position.line + 1] + local col = params.position.character + 1 + + local word_start = col + while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do + word_start = word_start - 1 + end + + local word_end = col + while word_end <= #line and line:sub(word_end, word_end):match("[%w_]") do + word_end = word_end + 1 + end + + local word = line:sub(word_start, word_end - 1):gsub("^#", "") + if not word or word == "" then + return nil + end + + local function make_location(doc_uri, doc_line, doc_col, name_len) + name_len = name_len or 0 + return { + uri = doc_uri, + range = { + start = { line = doc_line, character = doc_col }, + ["end"] = { line = doc_line, character = doc_col + name_len }, + }, + } + end + + local locations = {} + local seen_locations = {} + local search_names = { word } + + local function add_location(loc) + local key = loc.uri .. ":" .. tostring(loc.range.start.line) .. ":" .. tostring(loc.range.start.character) + if not seen_locations[key] then + seen_locations[key] = true + table.insert(locations, loc) + end + end + + if doc.variables and doc.variables[word] then + local var_type = doc.variables[word].data_type + if var_type then + var_type = var_type:gsub("%s*;.*$", ""):gsub("%s*:=.*$", ""):gsub("^%s+", ""):gsub("%s+$", "") + local type_name = var_type:match("([%w_]+)") + if type_name and type_name ~= word then + table.insert(search_names, type_name) + end + end + end + + for check_uri, check_doc in pairs(documents) do + if check_doc.functions then + for _, func in ipairs(check_doc.functions) do + for _, search_name in ipairs(search_names) do + if func.name == search_name then + add_location(make_location(check_uri, func.start, 0, #search_name)) + end + end + end + end + end + + local root_dirs = {} + for _, d in pairs(documents) do + if d.root_dir and d.root_dir ~= "" then + root_dirs[d.root_dir] = true + end + end + + for root_dir, _ in pairs(root_dirs) do + local scl_files = scan_scl_files(root_dir) + for _, filepath in ipairs(scl_files) do + local cached = workspace_symbol_cache[":file:" .. filepath] + if not cached then + local variables, functions, types = parse_scl_file(filepath) + cached = { functions = functions, types = types } + workspace_symbol_cache[":file:" .. filepath] = cached + end + + if cached.functions then + for _, func in ipairs(cached.functions) do + for _, search_name in ipairs(search_names) do + if func.name == search_name then + local file_uri = path_to_uri(filepath) + add_location(make_location(file_uri, func.start, 0, #search_name)) + end + end + end + end + + local handle = io.open(filepath, "r") + if handle then + local content = handle:read("*a") + handle:close() + for _, search_name in ipairs(search_names) do + local fb_pattern = 'FUNCTION_BLOCK%s+"' .. search_name .. '"' + local fb_plain = "^FUNCTION_BLOCK%s+" .. search_name .. "$" + if content:match(fb_pattern) or content:match(fb_plain) then + for line in content:gmatch("([^\n]*)\n") do + if line:match(fb_pattern) or line:match(fb_plain) then + local file_uri = path_to_uri(filepath) + add_location(make_location(file_uri, 0, 0, #search_name)) + break + end + end + end + + local db_pattern = 'DATA_BLOCK%s+"' .. search_name .. '"' + local db_plain = "^DATA_BLOCK%s+" .. search_name .. "$" + if content:match(db_pattern) or content:match(db_plain) then + for line in content:gmatch("([^\n]*)\n") do + if line:match(db_pattern) or line:match(db_plain) then + local file_uri = path_to_uri(filepath) + add_location(make_location(file_uri, 0, 0, #search_name)) + break + end + end + end + end + end + + if cached.types then + for type_name, type_info in pairs(cached.types) do + for _, search_name in ipairs(search_names) do + if type_name == search_name then + local file_uri = path_to_uri(filepath) + local start_line = type_info.start_line or 0 + add_location(make_location(file_uri, start_line, 0, #search_name)) + end + end + end + end + end + end + + for root_dir, _ in pairs(root_dirs) do + local udt_result = run_command('find "' .. root_dir .. '" -type f -name "*.udt" 2>/dev/null') + if udt_result then + for filepath in udt_result:gmatch("[^\n]+") do + if filepath and filepath ~= "" then + local handle = io.open(filepath, "r") + if handle then + local file_content = handle:read("*a") + handle:close() + for _, search_name in ipairs(search_names) do + local type_line = file_content:match('TYPE%s+"' .. search_name .. '"') + if not type_line then + type_line = file_content:match("TYPE%s+" .. search_name) + end + if type_line then + local line_num = 1 + for line in file_content:gmatch("([^\n]*)\n?") do + if line:match('TYPE%s+"' .. search_name .. '"') or line:match("TYPE%s+" .. search_name) then + local file_uri = path_to_uri(filepath) + add_location(make_location(file_uri, line_num - 1, 0, #search_name)) + break + end + line_num = line_num + 1 + end + end + end + end + end + end + end + + local db_result = run_command('find "' .. root_dir .. '" -type f -name "*.db" 2>/dev/null') + if db_result then + for filepath in db_result:gmatch("[^\n]+") do + if filepath and filepath ~= "" then + local handle = io.open(filepath, "r") + if handle then + local file_content = handle:read("*a") + handle:close() + for _, search_name in ipairs(search_names) do + local db_line = file_content:match('DATA_BLOCK%s+"' .. search_name .. '"') + if not db_line then + db_line = file_content:match("DATA_BLOCK%s+" .. search_name) + end + if db_line then + local line_num = 1 + for line in file_content:gmatch("([^\n]*)\n?") do + if line:match('DATA_BLOCK%s+"' .. search_name .. '"') or line:match("DATA_BLOCK%s+" .. search_name) then + local file_uri = path_to_uri(filepath) + add_location(make_location(file_uri, line_num - 1, 0, #search_name)) + break + end + line_num = line_num + 1 + end + end + end + end + end + end + end + end + + if #locations > 0 then + return locations + end + return nil +end + function handlers.textDocument_references(params) local uri = params.textDocument.uri local doc = documents[uri] @@ -923,6 +1184,7 @@ local function handle_message(message) if message.method == "$/cancelRequest" then return nil end + -- Convert method name from "textDocument/didOpen" to "textDocument_didOpen" local method_name = message.method:gsub("/", "_") local handler = handlers[method_name] diff --git a/src/parser.lua b/src/parser.lua index 936f4d5..06bb44c 100644 --- a/src/parser.lua +++ b/src/parser.lua @@ -17,22 +17,22 @@ function M.extract_variables(content) local function process_line(line, line_num) local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "") - -- Detect block start - local fb_name = trimmed:match('FUNCTION_BLOCK%s+"([^"]+)"') + -- Detect block start (with or without quotes) + local fb_name = trimmed:match('FUNCTION_BLOCK%s+"([^"]+)"') or trimmed:match("^FUNCTION_BLOCK%s+([%w_]+)") if fb_name then current_block = { name = fb_name, kind = "function_block" } in_var_section = false return end - local ob_name = trimmed:match('ORGANIZATION_BLOCK%s+"([^"]+)"') + local ob_name = trimmed:match('ORGANIZATION_BLOCK%s+"([^"]+)"') or trimmed:match("^ORGANIZATION_BLOCK%s+([%w_]+)") if ob_name then current_block = { name = ob_name, kind = "organization_block" } in_var_section = false return end - local fn_name = trimmed:match('FUNCTION%s+"([^"]+)"') + local fn_name = trimmed:match('FUNCTION%s+"([^"]+)"') or trimmed:match("^FUNCTION%s+([%w_]+)") if fn_name then current_block = { name = fn_name, kind = "function" } in_var_section = false @@ -239,7 +239,7 @@ function M.extract_functions(content) end for i, line in ipairs(lines) do - local func_name = line:match('FUNCTION%s+"([^"]+)"') + local func_name = line:match('FUNCTION%s+"([^"]+)"') or line:match("^FUNCTION%s+([%w_]+)") if func_name then local end_line = find_block_end(i, "END_FUNCTION", { "END_FUNCTION" }) table.insert(functions, { @@ -250,7 +250,7 @@ function M.extract_functions(content) }) end - local fb_name = line:match('FUNCTION_BLOCK%s+"([^"]+)"') + local fb_name = line:match('FUNCTION_BLOCK%s+"([^"]+)"') or line:match("^FUNCTION_BLOCK%s+([%w_]+)") if fb_name then local end_line = find_block_end(i, "END_FUNCTION_BLOCK", { "END_FUNCTION_BLOCK" }) table.insert(functions, { @@ -261,7 +261,7 @@ function M.extract_functions(content) }) end - local ob_name = line:match('ORGANIZATION_BLOCK%s+"([^"]+)"') + local ob_name = line:match('ORGANIZATION_BLOCK%s+"([^"]+)"') or line:match("^ORGANIZATION_BLOCK%s+([%w_]+)") if ob_name then local end_line = find_block_end(i, "END_ORGANIZATION_BLOCK", { "END_ORGANIZATION_BLOCK" }) table.insert(functions, { diff --git a/src/plc_json.lua b/src/plc_json.lua index 433f41a..e4833db 100644 --- a/src/plc_json.lua +++ b/src/plc_json.lua @@ -198,6 +198,21 @@ function M.load_types_from_workspace(root_dir) end end + -- Also scan for .udt files + local udt_files = scan_files_recursive(root_dir, "*.udt") + for _, filepath in ipairs(udt_files) do + local content = run_command("cat " .. filepath:gsub(" ", "\\ ")) + if content then + local udt_types = parse_scl_type_file(content) + for name, typ in pairs(udt_types) do + typ.source = "udt_file" + if not types[name] then + types[name] = typ + end + end + end + end + return types end From 9b393a65f0e0fd4232e7e4f7c4d39ed08976c49d Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Thu, 19 Feb 2026 18:07:04 +0100 Subject: [PATCH 3/5] docs: update README with goto declaration feature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18e59e0..46ee012 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP) |---------|-------------| | **Hover** | Press `K` to see variable type and struct fields | | **Completion** | `#` for variables, `.` for struct fields, `(` for functions | -| **Go to Definition** | `gd` jump to variable declaration | +| **Go to Definition** | `gD` jump to function, FB, DB, or UDT definition | | **Document Symbols** | Shows functions, variables, types | | **Workspace Symbols** | Search across all `.scl` files with fuzzy matching | | **Semantic Tokens** | Keywords, variables highlighted | From 1f9c99272cbeb3b4272a8a9b579a43a22a17989b Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Fri, 20 Feb 2026 08:59:54 +0100 Subject: [PATCH 4/5] fix(parser): add support for .udt and .db file parsing and goto declaration - extract_variables: Parse STRUCT fields in TYPE sections for .udt files - extract_types: Handle quoted type names and multiple type definitions - declaration handler: Extract quoted type names for goto declaration - Add filetype detection for .udt and .db files in nvim config --- src/main.lua | 2 +- src/parser.lua | 139 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 95 insertions(+), 46 deletions(-) diff --git a/src/main.lua b/src/main.lua index 9853157..886f6f5 100644 --- a/src/main.lua +++ b/src/main.lua @@ -558,7 +558,7 @@ function handlers.textDocument_declaration(params) local var_type = doc.variables[word].data_type if var_type then var_type = var_type:gsub("%s*;.*$", ""):gsub("%s*:=.*$", ""):gsub("^%s+", ""):gsub("%s+$", "") - local type_name = var_type:match("([%w_]+)") + local type_name = var_type:match('"([^"]+)"') or var_type:match("([%w_]+)") if type_name and type_name ~= word then table.insert(search_names, type_name) end diff --git a/src/parser.lua b/src/parser.lua index 06bb44c..95ec23e 100644 --- a/src/parser.lua +++ b/src/parser.lua @@ -11,6 +11,7 @@ function M.extract_variables(content) end local in_var_section = false + local in_struct_section = false local var_type = "unknown" local current_block = nil @@ -22,6 +23,7 @@ function M.extract_variables(content) if fb_name then current_block = { name = fb_name, kind = "function_block" } in_var_section = false + in_struct_section = false return end @@ -29,6 +31,7 @@ function M.extract_variables(content) if ob_name then current_block = { name = ob_name, kind = "organization_block" } in_var_section = false + in_struct_section = false return end @@ -36,6 +39,7 @@ function M.extract_variables(content) if fn_name then current_block = { name = fn_name, kind = "function" } in_var_section = false + in_struct_section = false return end @@ -44,6 +48,19 @@ function M.extract_variables(content) trimmed:match("^END_FUNCTION") then current_block = nil in_var_section = false + in_struct_section = false + return + end + + -- Handle TYPE sections (for .udt files with STRUCT) + if trimmed:match("^TYPE") and (trimmed:match("^TYPE%s") or trimmed:match('^TYPE"') or trimmed == "TYPE") then + in_struct_section = true + var_type = "STRUCT_FIELD" + return + end + + if trimmed:match("^END_TYPE") then + in_struct_section = false return end @@ -56,6 +73,7 @@ function M.extract_variables(content) trimmed:match("^VAR[_ ]*RETAIN%s*$") or trimmed:match("^VAR[_ ]*NON_RETAIN%s*$") then in_var_section = true + in_struct_section = false if trimmed:match("INPUT") then var_type = "VAR_INPUT" elseif trimmed:match("OUTPUT") then var_type = "VAR_OUTPUT" elseif trimmed:match("IN_OUT") then var_type = "VAR_IN_OUT" @@ -72,7 +90,18 @@ function M.extract_variables(content) return end - if not in_var_section then return end + if trimmed:match("^STRUCT%s*$") then + in_struct_section = true + var_type = "STRUCT_FIELD" + return + end + + if trimmed:match("^END_STRUCT%s*$") then + in_struct_section = false + return + end + + if not in_var_section and not in_struct_section then return end if trimmed:match("^%(%*") or trimmed:match("^%s*//") then return @@ -96,7 +125,7 @@ function M.extract_variables(content) brace_depth = brace_depth - 1 elseif c == ":" and brace_depth == 0 then type_start = i - break -- Only take the first : outside braces + break end end local var_data_type = "unknown" @@ -141,70 +170,90 @@ function M.extract_types(content) local in_type_section = false local current_type = nil local brace_depth = 0 + local in_struct = false + local pending_type_name = nil + + local function extract_type_name(after_colon) + if not after_colon then return nil end + local quoted = after_colon:match('^%s*"([^"]+)"') + if quoted then return quoted end + return after_colon:match("^%s*(%w+)") + end for i, line in ipairs(lines) do local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "") - if trimmed:match("^TYPE$") or trimmed:match("^TYPE%s+:=") then + local is_type_line = trimmed:match("^TYPE") and (trimmed:match("^TYPE%s") or trimmed:match('^TYPE"') or trimmed == "TYPE") + + if is_type_line then in_type_section = true + in_struct = false brace_depth = 0 + pending_type_name = trimmed:match('^TYPE%s+"([^"]+)') or trimmed:match("^TYPE%s+(%w+)") elseif in_type_section and trimmed:match("^END_TYPE") then in_type_section = false + in_struct = false current_type = nil + pending_type_name = nil elseif in_type_section then - local type_name = trimmed:match("^(%w+):") - if type_name then - local struct_start = trimmed:find("{") - local array_match = trimmed:find("ARRAY") - - if struct_start then + local type_name_with_struct = trimmed:match("^(%w+)%s+.-:%s+STRUCT") + if type_name_with_struct then + current_type = { + name = type_name_with_struct, + kind = "struct", + fields = {}, + start_line = i - 1, + } + types[type_name_with_struct] = current_type + in_struct = true + pending_type_name = nil + elseif trimmed:match("^STRUCT%s*$") then + if pending_type_name then current_type = { - name = type_name, + name = pending_type_name, kind = "struct", fields = {}, start_line = i - 1, } - types[type_name] = current_type - elseif array_match then - local of_type = trimmed:match("OF%s+(%w+)") - if of_type then - current_type = { - name = type_name, - kind = "array", - element_type = of_type, - start_line = i - 1, - } - types[type_name] = current_type - end - else - local field_type = trimmed:match(":%s*(%w+)") - if field_type then - current_type = { - name = type_name, - kind = "alias", - base_type = field_type, - start_line = i - 1, - } - types[type_name] = current_type - end + types[pending_type_name] = current_type + pending_type_name = nil end - end - - if current_type and current_type.kind == "struct" then - for open in line:gmatch("{") do brace_depth = brace_depth + 1 end - for close in line:gmatch("}") do brace_depth = brace_depth - 1 end - - local field_name = line:match("^%s*(%w+):") - if field_name and field_name ~= current_type.name then - local field_type = line:match(":%s*(%w+)") + in_struct = true + elseif trimmed:match("^END_STRUCT%s*$") then + in_struct = false + elseif in_struct and current_type and current_type.fields then + local field_name = line:match("^%s*(%w+)%s*:%s*") + if field_name then + local after_colon = line:match(":%s*(.+)") + local field_type = after_colon and extract_type_name(after_colon) table.insert(current_type.fields, { name = field_name, type = field_type or "unknown", }) end - - if brace_depth == 0 then - current_type = nil + elseif not in_struct and not pending_type_name then + local array_match = trimmed:match("^(%w+)%s+:%s+ARRAY") + if array_match then + local of_type = trimmed:match("OF%s+(%w+)") + current_type = { + name = array_match, + kind = "array", + element_type = of_type, + start_line = i - 1, + } + types[array_match] = current_type + else + local alias_match = trimmed:match("^(%w+)%s+:%s+(%w+)") + if alias_match then + local base_type = extract_type_name(trimmed:match(":%s+(.+)$")) + current_type = { + name = alias_match, + kind = "alias", + base_type = base_type, + start_line = i - 1, + } + types[alias_match] = current_type + end end end end From 40748bffd0276f8db94e68336cd9a72e5d7db0fc Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Fri, 20 Feb 2026 09:01:06 +0100 Subject: [PATCH 5/5] docs: add filetype detection section to AGENTS.md --- AGENTS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 1fbea2f..8e6c0d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,18 @@ scl_lsp/ └── grammar.js # Tree-sitter grammar ``` +## Filetype Detection + +The LSP requires the `scl` filetype to be set. Add to your Neovim config (e.g., `ftdetect/scl.vim`): + +```vim +au BufRead,BufNewFile *.scl set filetype=scl +au BufRead,BufNewFile *.udt set filetype=scl +au BufRead,BufNewFile *.db set filetype=scl +``` + +This enables LSP features (completion, goto definition, hover, etc.) for all SCL-related file types. + ## Code Style Guidelines ### Module Pattern