diff --git a/lua/laravel/navigate.lua b/lua/laravel/navigate.lua index 0c8c351..3ec3ce9 100644 --- a/lua/laravel/navigate.lua +++ b/lua/laravel/navigate.lua @@ -3,6 +3,1065 @@ local M = {} local ui = require('laravel.ui') +-- Treesitter utilities for Laravel navigation +local ts_utils = {} + +-- Check if treesitter is available and get parser +function ts_utils.get_parser() + local has_ts, ts = pcall(require, 'nvim-treesitter.parsers') + if not has_ts then + return nil + end + + local filetype = vim.bo.filetype + if not ts.has_parser(filetype) then + return nil + end + + local parser = ts.get_parser() + if not parser then + return nil + end + + return parser, ts +end + +-- Get the treesitter query for Laravel function calls +function ts_utils.get_laravel_query() + -- Check if we have treesitter query support + if not vim.treesitter.query or not vim.treesitter.query.parse then + return nil + end + + local query_string = [[ + ; Simple function calls - match any function with string arguments + (function_call_expression + (name) @function_name + (arguments + (argument + (string) @string_arg + ) + ) + ) @function_call + + ; Function calls with multiple arguments - capture all strings + (function_call_expression + (name) @function_name + (arguments + (argument + (string) @string_arg + ) + (argument + (string) @string_arg_2 + )+ + ) + ) @function_call_multi + + ; Static method calls (Class::method) + (scoped_call_expression + (name) @scope_name + (name) @method_name + (arguments + (argument + (string) @string_arg + ) + ) + ) @method_call + + ; Static method calls with multiple arguments + (scoped_call_expression + (name) @scope_name + (name) @method_name + (arguments + (argument + (string) @string_arg + ) + (argument + (string) @string_arg_2 + )+ + ) + ) @method_call_multi + + ; Member/method calls + (member_call_expression + (name) @method_name + (arguments + (argument + (string) @string_arg + ) + ) + ) @member_call + + ; Capture any string with content + (string + (string_content) @string_content + ) @string_node + ]] + + local ok, query = pcall(vim.treesitter.query.parse, 'php', query_string) + if not ok then + -- Try to create a minimal working query to test basic functionality + local minimal_ok, minimal_query = pcall(vim.treesitter.query.parse, 'php', '(name) @any_name') + if minimal_ok then + return minimal_query + end + return nil + end + return query +end + +-- Extract Laravel function call information using direct AST traversal (no queries) +function ts_utils.get_laravel_call_at_cursor() + local parser, ts = ts_utils.get_parser() + if not parser then + return nil + end + + local ok, tree = pcall(function() + return parser:parse()[1] + end) + if not ok or not tree then + return nil + end + + local root = tree:root() + if not root then + return nil + end + + local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) + cursor_row = cursor_row - 1 -- Convert to 0-based + + -- Find the node at cursor position and traverse up to find Laravel function calls + local cursor_node = root:descendant_for_range(cursor_row, cursor_col, cursor_row, cursor_col) + if not cursor_node then + return nil + end + + -- First strategy: Look for the most specific Laravel call that contains the cursor + local best_call_info = nil + local best_distance = math.huge + + -- Traverse up the tree to find function calls that contain the cursor + local current_node = cursor_node + local search_depth = 0 + + while current_node and search_depth < 10 do + local call_info = ts_utils.extract_laravel_call_from_node(current_node) + if call_info then + -- Check if this call actually contains the cursor position + if ts_utils.node_contains_cursor(current_node, cursor_row, cursor_col) then + -- Calculate how close this node is to the cursor (smaller nodes = closer) + local ok_range, start_row, start_col, end_row, end_col = pcall(function() + return current_node:range() + end) + + if ok_range then + local node_size = (end_row - start_row) * 1000 + (end_col - start_col) + -- Prefer smaller nodes (more specific) and closer to cursor + local distance = node_size + search_depth + + if distance < best_distance then + best_distance = distance + best_call_info = call_info + end + end + end + end + + current_node = current_node:parent() + search_depth = search_depth + 1 + end + + -- Return the most specific Laravel call that contains the cursor + if best_call_info then + return best_call_info + end + + -- Fallback: Search current line only if no containing call found + local line_node = root:descendant_for_range(cursor_row, 0, cursor_row, 1000) + if line_node then + return ts_utils.search_laravel_calls_in_node_with_cursor_check(line_node, cursor_row, cursor_col) + end + + return nil +end + +-- Extract Laravel call info from a single node using direct AST traversal +function ts_utils.extract_laravel_call_from_node(node) + if not node then + return nil + end + + -- Check if this is a function call expression + if node:type() == 'function_call_expression' then + return ts_utils.parse_function_call_node(node) + end + + -- Check if this is a scoped call expression (Class::method) + if node:type() == 'scoped_call_expression' then + return ts_utils.parse_scoped_call_node(node) + end + + -- Check if this is a member call expression ($obj->method) + if node:type() == 'member_call_expression' then + return ts_utils.parse_member_call_node(node) + end + + -- Check if this is a controller class reference (ControllerName::class) + if node:type() == 'class_constant_access_expression' then + return ts_utils.parse_controller_class_node(node) + end + + -- Check if this is just a name node that might be a controller + if node:type() == 'name' then + return ts_utils.parse_controller_name_node(node) + end + + return nil +end + +-- Search for Laravel function calls within a node +function ts_utils.search_laravel_calls_in_node(node) + if not node then + return nil + end + + -- First check the node itself + local call_info = ts_utils.extract_laravel_call_from_node(node) + if call_info then + return call_info + end + + -- Recursively search children + for i = 0, node:child_count() - 1 do + local child = node:child(i) + if child then + local child_call_info = ts_utils.search_laravel_calls_in_node(child) + if child_call_info then + return child_call_info + end + end + end + + return nil +end + +-- Search for Laravel function calls within a node, prioritizing ones that contain the cursor +function ts_utils.search_laravel_calls_in_node_with_cursor_check(node, cursor_row, cursor_col) + if not node then + return nil + end + + local best_call_info = nil + local best_distance = math.huge + + -- Recursively search all Laravel calls in this node and its children + local function find_all_calls(search_node, depth) + if depth > 10 then + return + end + + local call_info = ts_utils.extract_laravel_call_from_node(search_node) + if call_info then + -- Check if this call contains the cursor + if ts_utils.node_contains_cursor(search_node, cursor_row, cursor_col) then + local ok_range, start_row, start_col, end_row, end_col = pcall(function() + return search_node:range() + end) + + if ok_range then + local node_size = (end_row - start_row) * 1000 + (end_col - start_col) + local distance = node_size + depth + + if distance < best_distance then + best_distance = distance + best_call_info = call_info + end + end + end + end + + -- Recursively search children + for i = 0, search_node:child_count() - 1 do + local child = search_node:child(i) + if child then + find_all_calls(child, depth + 1) + end + end + end + + find_all_calls(node, 0) + return best_call_info +end + +-- Parse a function_call_expression node (e.g., config('app.name')) +function ts_utils.parse_function_call_node(node) + if node:child_count() < 2 then + return nil + end + + -- Child[0] should be the function name + local name_node = node:child(0) + if not name_node or name_node:type() ~= 'name' then + return nil + end + + local ok_name, function_name = pcall(vim.treesitter.get_node_text, name_node, 0) + if not ok_name or not function_name then + return nil + end + + -- Child[1] should be the arguments + local args_node = node:child(1) + if not args_node or args_node:type() ~= 'arguments' then + return nil + end + + local string_args = ts_utils.extract_string_arguments(args_node) + if #string_args == 0 then + return nil + end + + return ts_utils.create_laravel_call_info(function_name, nil, nil, string_args, 'function') +end + +-- Parse a scoped_call_expression node (e.g., Route::get()) +function ts_utils.parse_scoped_call_node(node) + if node:child_count() < 3 then + return nil + end + + -- Find scope name and method name + local scope_name = nil + local method_name = nil + local args_node = nil + + for i = 0, node:child_count() - 1 do + local child = node:child(i) + if child then + if child:type() == 'name' then + if not scope_name then + scope_name = ts_utils.get_node_text_safe(child) + elseif not method_name then + method_name = ts_utils.get_node_text_safe(child) + end + elseif child:type() == 'arguments' then + args_node = child + end + end + end + + if not scope_name or not method_name or not args_node then + return nil + end + + local string_args = ts_utils.extract_string_arguments(args_node) + if #string_args == 0 then + return nil + end + + return ts_utils.create_laravel_call_info(method_name, scope_name, nil, string_args, 'scoped') +end + +-- Parse a member_call_expression node (e.g., $route->name()) +function ts_utils.parse_member_call_node(node) + if node:child_count() < 3 then + return nil + end + + -- Find method name and arguments + local method_name = nil + local args_node = nil + + for i = 0, node:child_count() - 1 do + local child = node:child(i) + if child then + if child:type() == 'name' then + method_name = ts_utils.get_node_text_safe(child) + elseif child:type() == 'arguments' then + args_node = child + end + end + end + + if not method_name or not args_node then + return nil + end + + local string_args = ts_utils.extract_string_arguments(args_node) + if #string_args == 0 then + return nil + end + + return ts_utils.create_laravel_call_info(method_name, nil, method_name, string_args, 'method') +end + +-- Parse a controller class reference (e.g., CompanyProfileController::class) +function ts_utils.parse_controller_class_node(node) + if node:child_count() < 2 then + return nil + end + + -- Look for ControllerName::class pattern + local class_name = nil + local constant_name = nil + + for i = 0, node:child_count() - 1 do + local child = node:child(i) + if child and child:type() == 'name' then + if not class_name then + class_name = ts_utils.get_node_text_safe(child) + else + constant_name = ts_utils.get_node_text_safe(child) + end + end + end + + -- Check if this is a Controller::class pattern + if class_name and constant_name == 'class' and class_name:match('Controller$') then + return { + func = 'controller', + partial = class_name, + call_type = 'class_reference', + function_name = nil, + scope_name = class_name, + method_name = nil, + all_args = { class_name } + } + end + + return nil +end + +-- Parse a controller name node (e.g., just "CompanyProfileController") +function ts_utils.parse_controller_name_node(node) + local name = ts_utils.get_node_text_safe(node) + + -- Check if this looks like a controller name + if name and name:match('Controller$') then + -- Verify it's in a route context by checking parent nodes + local parent = node:parent() + local depth = 0 + while parent and depth < 5 do + local parent_text = ts_utils.get_node_text_safe(parent) + if parent_text and parent_text:match('Route::') then + return { + func = 'controller', + partial = name, + call_type = 'controller_name', + function_name = nil, + scope_name = name, + method_name = nil, + all_args = { name } + } + end + parent = parent:parent() + depth = depth + 1 + end + end + + return nil +end + +-- Extract string arguments from an arguments node +function ts_utils.extract_string_arguments(args_node) + local string_args = {} + + for i = 0, args_node:child_count() - 1 do + local child = args_node:child(i) + if child and child:type() == 'argument' then + -- Look for string inside argument + for j = 0, child:child_count() - 1 do + local arg_child = child:child(j) + if arg_child and arg_child:type() == 'string' then + -- Extract string content + local string_content = ts_utils.extract_string_content(arg_child) + if string_content then + table.insert(string_args, string_content) + end + break + end + end + end + end + + return string_args +end + +-- Extract string content from a string node +function ts_utils.extract_string_content(string_node) + for i = 0, string_node:child_count() - 1 do + local child = string_node:child(i) + if child and child:type() == 'string_content' then + return ts_utils.get_node_text_safe(child) + end + end + + -- Fallback: get the full string and remove quotes + local full_text = ts_utils.get_node_text_safe(string_node) + if full_text then + -- Remove surrounding quotes + local clean_text = full_text:match('^[\'"](.*)[\'"]$') + return clean_text or full_text + end + + return nil +end + +-- Safely get node text +function ts_utils.get_node_text_safe(node) + if not node then + return nil + end + + local ok, text = pcall(vim.treesitter.get_node_text, node, 0) + return ok and text or nil +end + +-- Create Laravel call info from parsed components +function ts_utils.create_laravel_call_info(function_name, scope_name, method_name, string_args, call_type) + if not function_name or #string_args == 0 then + return nil + end + + -- Map Laravel functions to navigation types (same as before) + local laravel_functions = { + -- Navigation helpers + route = 'route', + view = 'view', + config = 'config', + __ = 'trans', + trans = 'trans', + env = 'env', + + -- Asset helpers + asset = 'asset', + secure_asset = 'asset', + mix = 'asset', + + -- URL helpers + action = 'route', + to_route = 'route', + url = 'url', + secure_url = 'url', + + -- Path helpers + app_path = 'path', + base_path = 'path', + config_path = 'path', + database_path = 'path', + lang_path = 'path', + public_path = 'path', + resource_path = 'path', + storage_path = 'path', + + -- Inertia + inertia = 'view', + + -- Other Laravel helpers + policy = 'policy', + broadcast = 'broadcast', + event = 'event', + collect = 'collect', + cache = 'cache', + session = 'session', + request = 'request', + response = 'response', + redirect = 'redirect', + back = 'redirect', + abort = 'abort', + logger = 'logger', + + -- Method calls + name = 'route_name', -- for ->name() calls + render = 'view', -- for Inertia::render() + middleware = 'middleware', + where = 'route_constraint', + } + + local func_type = nil + local target_string = nil + + -- Handle function calls + if call_type == 'function' then + func_type = laravel_functions[function_name] + target_string = string_args[1] + + -- Handle scoped/static method calls + elseif call_type == 'scoped' and scope_name and method_name then + local scope_lower = scope_name:lower() + local method_lower = method_name:lower() + + -- Route static methods + if scope_lower == 'route' then + if method_lower == 'inertia' and #string_args >= 2 then + func_type = 'view' + target_string = string_args[2] -- Second argument is the view name + elseif method_lower:match('^(get|post|put|patch|delete|options|head|any|match|redirect|view|resource|apiresource)$') then + func_type = 'route' + target_string = string_args[1] -- First argument is usually the URI + end + + -- Inertia static methods + elseif scope_lower == 'inertia' and method_lower == 'render' then + func_type = 'view' + target_string = string_args[1] + + -- Config facade + elseif scope_lower == 'config' and method_lower == 'get' then + func_type = 'config' + target_string = string_args[1] + + -- View facade + elseif scope_lower == 'view' and method_lower == 'make' then + func_type = 'view' + target_string = string_args[1] + + -- Other facades + elseif laravel_functions[method_lower] then + func_type = laravel_functions[method_lower] + target_string = string_args[1] + end + + -- Handle method calls (instance or chained) + elseif call_type == 'method' and method_name then + local method_lower = method_name:lower() + + if method_lower == 'name' then + func_type = 'route_name' + target_string = string_args[1] + elseif method_lower == 'view' then + func_type = 'view' + target_string = string_args[1] + elseif laravel_functions[method_lower] then + func_type = laravel_functions[method_lower] + target_string = string_args[1] + end + end + + if func_type and target_string then + return { + func = func_type, + partial = target_string, + call_type = call_type, + function_name = function_name, + scope_name = scope_name, + method_name = method_name, + all_args = string_args + } + end + + return nil +end + +-- Check if a node contains the cursor position +function ts_utils.node_contains_cursor(node, cursor_row, cursor_col) + if not node then + return false + end + + local ok, start_row, start_col, end_row, end_col = pcall(function() + return node:range() + end) + + if not ok then + return false + end + + return cursor_row >= start_row and cursor_row <= end_row and + (cursor_row > start_row or cursor_col >= start_col) and + (cursor_row < end_row or cursor_col <= end_col) +end + +-- Debug version of extract_call_info that shows step-by-step process +function ts_utils.extract_call_info_debug(match, query) + local debug_steps = {} + local function_name = nil + local scope_name = nil + local method_name = nil + local string_args = {} + local call_type = nil + + table.insert(debug_steps, 'Extract Call Info Debug:') + table.insert(debug_steps, '========================') + + for id, node in pairs(match) do + if node == nil then + table.insert(debug_steps, 'Skipping nil node') + goto continue + end + + local capture_name = query.captures[id] + if not capture_name then + table.insert(debug_steps, 'No capture name for node') + goto continue + end + + local ok, text = pcall(vim.treesitter.get_node_text, node, 0) + if not ok or not text then + table.insert(debug_steps, 'Failed to get text for capture: ' .. capture_name) + goto continue + end + + table.insert(debug_steps, 'Processing: ' .. capture_name .. ' = "' .. text .. '"') + + if capture_name == 'function_name' then + function_name = text + call_type = 'function' + table.insert(debug_steps, ' → Set function_name = ' .. text) + elseif capture_name == 'scope_name' then + scope_name = text + call_type = 'scoped' + table.insert(debug_steps, ' → Set scope_name = ' .. text) + elseif capture_name == 'method_name' then + method_name = text + if call_type ~= 'scoped' then + call_type = 'method' + end + table.insert(debug_steps, ' → Set method_name = ' .. text) + elseif capture_name == 'string_arg' or capture_name == 'string_arg_2' then + local clean_string = text + clean_string = clean_string:match('^[\'"](.*)[\'"]$') or clean_string + clean_string = clean_string:match('^"(.*)"$') or clean_string + clean_string = clean_string:match("^'(.*)'$") or clean_string + table.insert(string_args, clean_string) + table.insert(debug_steps, ' → Added string arg: "' .. clean_string .. '"') + elseif capture_name == 'string_content' then + table.insert(string_args, text) + table.insert(debug_steps, ' → Added string content: "' .. text .. '"') + end + + ::continue:: + end + + table.insert(debug_steps, '') + table.insert(debug_steps, 'Results:') + table.insert(debug_steps, '--------') + table.insert(debug_steps, 'call_type: ' .. (call_type or 'nil')) + table.insert(debug_steps, 'function_name: ' .. (function_name or 'nil')) + table.insert(debug_steps, 'scope_name: ' .. (scope_name or 'nil')) + table.insert(debug_steps, 'method_name: ' .. (method_name or 'nil')) + table.insert(debug_steps, 'string_args: ' .. table.concat(string_args, ', ')) + + return debug_steps, { + call_type = call_type, + function_name = function_name, + scope_name = scope_name, + method_name = method_name, + string_args = string_args + } +end + +-- Extract call information from treesitter match +function ts_utils.extract_call_info(match, query) + local function_name = nil + local scope_name = nil + local method_name = nil + local string_args = {} + local call_type = nil + + for id, node in pairs(match) do + -- Skip nil nodes + if node == nil then + goto continue + end + + local capture_name = query.captures[id] + if not capture_name then + goto continue + end + + -- Safely get node text + local ok, text = pcall(vim.treesitter.get_node_text, node, 0) + if not ok or not text then + goto continue + end + + if capture_name == 'function_name' then + function_name = text + call_type = 'function' + elseif capture_name == 'scope_name' then + scope_name = text + call_type = 'scoped' + elseif capture_name == 'method_name' then + method_name = text + if call_type ~= 'scoped' then + call_type = 'method' + end + elseif capture_name == 'string_arg' or capture_name == 'string_arg_2' then + -- Remove quotes from string (handle both single and double quotes) + local clean_string = text + -- Remove outer quotes if present + clean_string = clean_string:match('^[\'"](.*)[\'"]$') or clean_string + -- Handle double quotes with potential escape sequences + clean_string = clean_string:match('^"(.*)"$') or clean_string + -- Handle single quotes + clean_string = clean_string:match("^'(.*)'$") or clean_string + + table.insert(string_args, clean_string) + elseif capture_name == 'string_content' then + -- Direct string content without quotes (PHP grammar structure) + table.insert(string_args, text) + end + + ::continue:: + end + + -- Determine the Laravel function type and return structured info + local laravel_functions = { + -- Navigation helpers + route = 'route', + view = 'view', + config = 'config', + __ = 'trans', + trans = 'trans', + env = 'env', + + -- Asset helpers + asset = 'asset', + secure_asset = 'asset', + mix = 'asset', + + -- URL helpers + action = 'route', + to_route = 'route', + url = 'url', + secure_url = 'url', + + -- Path helpers + app_path = 'path', + base_path = 'path', + config_path = 'path', + database_path = 'path', + lang_path = 'path', + public_path = 'path', + resource_path = 'path', + storage_path = 'path', + + -- Inertia + inertia = 'view', + + -- Other Laravel helpers + policy = 'policy', + broadcast = 'broadcast', + event = 'event', + collect = 'collect', + cache = 'cache', + session = 'session', + request = 'request', + response = 'response', + redirect = 'redirect', + back = 'redirect', + abort = 'abort', + logger = 'logger', + + -- Method calls + name = 'route_name', -- for ->name() calls + render = 'view', -- for Inertia::render() + middleware = 'middleware', + where = 'route_constraint', + } + + local func_type = nil + local target_string = nil + + -- Handle function calls + if call_type == 'function' and function_name then + func_type = laravel_functions[function_name] + target_string = string_args[1] + + -- Handle scoped/static method calls + elseif call_type == 'scoped' and scope_name and method_name then + local scope_lower = scope_name:lower() + local method_lower = method_name:lower() + + -- Route static methods + if scope_lower == 'route' then + if method_lower == 'inertia' and #string_args >= 2 then + func_type = 'view' + target_string = string_args[2] -- Second argument is the view name + elseif method_lower:match('^(get|post|put|patch|delete|options|head|any|match|redirect|view|resource|apiresource)$') then + func_type = 'route' + target_string = string_args[1] -- First argument is usually the URI + end + + -- Inertia static methods + elseif scope_lower == 'inertia' and method_lower == 'render' then + func_type = 'view' + target_string = string_args[1] + + -- Config facade + elseif scope_lower == 'config' and method_lower == 'get' then + func_type = 'config' + target_string = string_args[1] + + -- View facade + elseif scope_lower == 'view' and method_lower == 'make' then + func_type = 'view' + target_string = string_args[1] + + -- Other facades + elseif laravel_functions[method_lower] then + func_type = laravel_functions[method_lower] + target_string = string_args[1] + end + + -- Handle method calls (instance or chained) + elseif call_type == 'method' and method_name then + local method_lower = method_name:lower() + + if method_lower == 'name' then + func_type = 'route_name' + target_string = string_args[1] + elseif method_lower == 'view' then + func_type = 'view' + target_string = string_args[1] + elseif laravel_functions[method_lower] then + func_type = laravel_functions[method_lower] + target_string = string_args[1] + end + end + + if func_type and target_string then + return { + func = func_type, + partial = target_string, + call_type = call_type, + function_name = function_name, + scope_name = scope_name, + method_name = method_name, + all_args = string_args + } + end + + -- Debug: If we couldn't extract info, at least log what we found + -- This helps debug why extraction is failing + if call_type and (function_name or method_name) and #string_args > 0 then + -- We found something but it wasn't recognized as a Laravel function + -- This could help identify missing patterns + end + + return nil +end + +-- Direct AST-based Laravel context detection +function ts_utils.is_laravel_context_ts() + local ok, call_info = pcall(ts_utils.get_laravel_call_at_cursor) + if not ok then + return false + end + return call_info ~= nil +end + +-- Enhanced Laravel string navigation using treesitter +function ts_utils.goto_laravel_string_ts() + local ok, call_info = pcall(ts_utils.get_laravel_call_at_cursor) + if not ok or not call_info then + return false + end + + -- Navigate based on the detected context + if call_info.func == 'route' or call_info.func == 'route_name' then + M.goto_route_definition(call_info.partial) + elseif call_info.func == 'view' then + M.goto_view(call_info.partial) + elseif call_info.func == 'config' then + M.goto_config(call_info.partial) + elseif call_info.func == 'trans' then + M.goto_translation(call_info.partial) + elseif call_info.func == 'env' then + M.goto_env(call_info.partial) + elseif call_info.func == 'asset' then + M.goto_asset(call_info.partial) + elseif call_info.func == 'controller' then + M.goto_controller(call_info.partial) + elseif call_info.func == 'path' then + -- Handle Laravel path helpers - could navigate to directories + return false -- Not implemented yet + elseif call_info.func == 'url' then + -- Handle URL helpers - could show URL info + return false -- Not implemented yet + elseif call_info.func == 'policy' then + -- Could navigate to policy files + M.goto_policy(call_info.partial) + elseif call_info.func == 'middleware' then + -- Could navigate to middleware files + M.goto_middleware(call_info.partial) + elseif call_info.func == 'event' then + -- Could navigate to event files + M.goto_event(call_info.partial) + else + -- Unknown Laravel function + return false + end + + return true +end + +-- Navigate to policy file +function M.goto_policy(policy_name) + if not policy_name or policy_name == '' then + ui.warn('No policy name provided') + return + end + + local root = get_project_root() + if not root then + ui.error('Not in a Laravel project') + return + end + + local policy_path = root .. '/app/Policies/' .. policy_name .. 'Policy.php' + if vim.fn.filereadable(policy_path) == 1 then + vim.cmd('edit ' .. policy_path) + else + ui.warn('Policy file not found: ' .. policy_name .. 'Policy.php') + end +end + +-- Navigate to middleware file +function M.goto_middleware(middleware_name) + if not middleware_name or middleware_name == '' then + ui.warn('No middleware name provided') + return + end + + local root = get_project_root() + if not root then + ui.error('Not in a Laravel project') + return + end + + local middleware_path = root .. '/app/Http/Middleware/' .. middleware_name .. '.php' + if vim.fn.filereadable(middleware_path) == 1 then + vim.cmd('edit ' .. middleware_path) + else + ui.warn('Middleware file not found: ' .. middleware_name .. '.php') + end +end + +-- Navigate to event file +function M.goto_event(event_name) + if not event_name or event_name == '' then + ui.warn('No event name provided') + return + end + + local root = get_project_root() + if not root then + ui.error('Not in a Laravel project') + return + end + + local event_path = root .. '/app/Events/' .. event_name .. '.php' + if vim.fn.filereadable(event_path) == 1 then + vim.cmd('edit ' .. event_path) + else + ui.warn('Event file not found: ' .. event_name .. '.php') + end +end + -- Get Laravel project root local function get_project_root() return _G.laravel_nvim.project_root @@ -324,75 +1383,28 @@ end -- Check if current context is Laravel-specific and should use Laravel navigation function M.is_laravel_navigation_context() + -- Try treesitter first if available - this is now the primary method + if ts_utils.is_laravel_context_ts() then + return true + end + + -- Minimal regex fallback - only for very basic cases when treesitter fails local line = vim.fn.getline('.') - local word = vim.fn.expand('') - -- Check for Laravel helper functions - local laravel_patterns = { - -- Navigation helpers + -- Only check for the most obvious Laravel patterns as fallback + local basic_patterns = { 'route%s*%(', 'view%s*%(', - 'config%s*%(', - '__%s*%(', - 'trans%s*%(', - 'env%s*%(', + 'Route%s*::%s*', 'Inertia%s*::%s*render%s*%(', - 'inertia%s*%(', - - -- URL helpers - 'action%s*%(', - 'asset%s*%(', - 'secure_asset%s*%(', - 'secure_url%s*%(', - 'to_route%s*%(', - 'url%s*%(', - - -- Path helpers - 'app_path%s*%(', - 'base_path%s*%(', - 'config_path%s*%(', - 'database_path%s*%(', - 'lang_path%s*%(', - 'public_path%s*%(', - 'resource_path%s*%(', - 'storage_path%s*%(', - - -- Other common helpers that might reference files - 'mix%s*%(', - 'policy%s*%(', } - for _, pattern in ipairs(laravel_patterns) do + for _, pattern in ipairs(basic_patterns) do if line:match(pattern) then return true end end - -- Check if we're in a quoted string that looks like a Laravel path - local col = vim.fn.col('.') - local before_cursor = line:sub(1, col - 1) - local after_cursor = line:sub(col) - - -- Find if we're inside quotes - local quote_start = before_cursor:find("['\"][^'\"]*$") - local quote_end = after_cursor:find("['\"]") - - if quote_start and quote_end then - local full_string = before_cursor:sub(quote_start + 1) .. after_cursor:sub(1, quote_end - 1) - - -- Check if the string looks like a Laravel pattern - if full_string:match('^[a-z]+%.[a-z]+') or -- route names like 'user.show' - full_string:match('/') or -- view paths like 'users/show' - full_string:match('%.') then -- config keys like 'app.name' - -- Make sure we're in a Laravel function context - for _, pattern in ipairs(laravel_patterns) do - if line:match(pattern) then - return true - end - end - end - end - return false end @@ -409,169 +1421,46 @@ end -- Enhanced Laravel string navigation - detects context and navigates to appropriate file function M.goto_laravel_string() - local col = vim.fn.col('.') - local context = nil - - -- First try single-line extraction (more precise) - local line = vim.fn.getline('.') - - -- Special handling for Route::inertia syntax (view name is second parameter) - local route_inertia_pattern = "Route::inertia%s*%(%s*['\"][^'\"]*['\"]%s*,%s*['\"]([^'\"]+)['\"]" - local route_inertia_match = line:match(route_inertia_pattern) - if route_inertia_match then - context = { func = 'view', partial = route_inertia_match } - -- Early return to prevent other patterns from overriding this - M.goto_view(context.partial) + -- PRIMARY: Try treesitter navigation (this should handle 90%+ of cases) + if ts_utils.goto_laravel_string_ts() then return true end - local function extract_laravel_call(line, func_name) - local escaped_func = func_name:gsub('([%(%)%[%]%*%+%-%?%^%$%%::])', '%%%1') - local patterns = { - escaped_func .. "%s*%(%s*['\"]([^'\"]+)['\"]", -- Basic pattern - escaped_func .. "%s*%(%s*['\"]([^'\"]*)['\"]%s*,", -- With comma after - escaped_func .. "%s*%(%s*['\"]([^'\"]*)['\"]%s*%)", -- With closing paren - } - for _, pattern in ipairs(patterns) do - local match = line:match(pattern) - if match then - return match - end - end - return nil - end + -- MINIMAL FALLBACK: Only try regex for the most basic patterns + local line = vim.fn.getline('.') + local col = vim.fn.col('.') - local laravel_functions = { - -- Navigation helpers - { name = 'route', type = 'route' }, - { name = 'view', type = 'view' }, - { name = 'Inertia::render', type = 'view' }, - { name = 'inertia', type = 'view' }, - { name = 'config', type = 'config' }, - { name = '__', type = 'trans' }, - { name = 'trans', type = 'trans' }, - { name = 'env', type = 'env' }, - -- URL helpers that reference routes - { name = 'action', type = 'route' }, - { name = 'to_route', type = 'route' }, - -- Asset helpers - { name = 'asset', type = 'asset' }, - { name = 'secure_asset', type = 'asset' }, - { name = 'mix', type = 'asset' }, + -- Only handle the most obvious regex patterns as absolute fallback + local basic_extractions = { + { pattern = "route%s*%(%s*['\"]([^'\"]+)['\"]", func = 'route' }, + { pattern = "view%s*%(%s*['\"]([^'\"]+)['\"]", func = 'view' }, + { pattern = "Route::inertia%s*%(%s*['\"][^'\"]*['\"]%s*,%s*['\"]([^'\"]+)['\"]", func = 'view' }, + { pattern = "Inertia::render%s*%(%s*['\"]([^'\"]+)['\"]", func = 'view' } } - -- Try single-line extraction first (more precise) - for _, func in ipairs(laravel_functions) do - local match = extract_laravel_call(line, func.name) + for _, extraction in ipairs(basic_extractions) do + local match = line:match(extraction.pattern) if match then - context = { func = func.type, partial = match } - break - end - end - - -- Fallback to multi-line extraction only if single-line fails - if not context then - local joined_lines = get_surrounding_lines_joined(0, 3) - - local function extract_laravel_call_multiline(text, func_name) - local escaped_func = func_name:gsub('([%(%)%[%]%*%+%-%?%^%$%%::])', '%%%1') - local patterns = { - escaped_func .. "%s*%(%s*['\"]([^'\"]+)['\"]", -- Basic pattern - escaped_func .. "%s*%(%s*['\"]([^'\"]*)['\"]%s*,", -- With comma after - escaped_func .. "%s*%(%s*['\"]([^'\"]*)['\"]%s*%)", -- With closing paren - } - for _, pattern in ipairs(patterns) do - local match = text:match(pattern) - if match then - return match - end - end - return nil - end - - for _, func in ipairs(laravel_functions) do - local match = extract_laravel_call_multiline(joined_lines, func.name) - if match then - context = { func = func.type, partial = match } - break - end - end - end - - if not context then - -- Last resort: check if cursor is on a quoted string and try to guess context - local before_cursor = line:sub(1, col - 1) - local after_cursor = line:sub(col) - - -- Find the quoted string we're in - more robust approach - local quote_char = nil - local quote_start_pos = nil - - -- Look for the opening quote - for i = col - 1, 1, -1 do - local char = line:sub(i, i) - if char == '"' or char == "'" then - quote_char = char - quote_start_pos = i - break - end - end - - if quote_char and quote_start_pos then - -- Find the closing quote - local quote_end_pos = nil - for i = col, #line do - local char = line:sub(i, i) - if char == quote_char then - quote_end_pos = i - break - end - end - - if quote_end_pos then - local full_string = line:sub(quote_start_pos + 1, quote_end_pos - 1) - - -- Try to guess the type based on string content and surrounding context - if (line:match('route%s*%(') or line:match('to_route%s*%(') or line:match('action%s*%(')) and full_string:match('^[a-z]+%.[a-z]+') then - context = { func = 'route', partial = full_string } - elseif (line:match('route%s*%(') or line:match('to_route%s*%(') or line:match('action%s*%(')) then - -- If it doesn't match the dotted pattern but is in a route function, still treat as route - context = { func = 'route', partial = full_string } - elseif (line:match('view%s*%(') or line:match('Inertia%s*::%s*render%s*%(') or line:match('Route%s*::%s*inertia%s*%(')) and (full_string:match('/') or full_string:match('%.')) then - context = { func = 'view', partial = full_string } - elseif line:match('config%s*%(') and full_string:match('%.') then - context = { func = 'config', partial = full_string } - elseif (line:match('__%s*%(') or line:match('trans%s*%(')) then - context = { func = 'trans', partial = full_string } - elseif line:match('env%s*%(') then - context = { func = 'env', partial = full_string } - end + if extraction.func == 'route' then + M.goto_route_definition(match) + elseif extraction.func == 'view' then + M.goto_view(match) end + return true end end - if not context then - return false - end - - -- Navigate based on the detected context - if context.func == 'route' then - M.goto_route_definition(context.partial) - elseif context.func == 'view' then - M.goto_view(context.partial) - elseif context.func == 'config' then - M.goto_config(context.partial) - elseif context.func == 'trans' or context.func == '__' then - M.goto_translation(context.partial) - elseif context.func == 'env' then - M.goto_env(context.partial) - elseif context.func == 'asset' then - M.goto_asset(context.partial) - else - return false - end + -- If we get here, neither treesitter nor basic regex worked + ui.warn('No Laravel navigation pattern detected at cursor position') + return false +end - return true +-- Legacy regex-based navigation (kept for compatibility but minimized) +function M.goto_laravel_string_regex_legacy() + -- This function is now deprecated in favor of treesitter-based navigation + -- It's kept only for extreme edge cases or if treesitter is unavailable + ui.warn('Using legacy regex navigation - treesitter method failed') + return false end -- Navigate to route definition by name @@ -587,6 +1476,8 @@ function M.goto_route_definition(route_name) return end + + local route_files = { root .. '/routes/web.php', root .. '/routes/api.php', @@ -598,26 +1489,51 @@ function M.goto_route_definition(route_name) for _, route_file in ipairs(route_files) do if vim.fn.filereadable(route_file) == 1 then local lines = vim.fn.readfile(route_file) - -- Join lines in windows of 3 to catch multi-line route definitions - local window = 3 + local pattern = '->name%s*%(%s*[\'"]' .. vim.pesc(route_name) .. '[\'"]' + + -- First try: Look for exact line matches for i = 1, #lines do - local chunk = {} - for j = 0, window - 1 do - if lines[i + j] then - table.insert(chunk, lines[i + j]) - end - end - local joined = table.concat(chunk, ' ') - local pattern = '->name%s*%(%s*[\'"]' .. vim.pesc(route_name) .. '[\'"]' - if joined:match(pattern) then + if lines[i]:match(pattern) then vim.cmd('edit ' .. route_file) vim.fn.cursor(i, 1) vim.cmd('normal! zz') - ui.info('Found route: ' .. route_name) found = true break end end + + -- Second try: Join lines in windows of 3 to catch multi-line route definitions + if not found then + local window = 3 + for i = 1, #lines do + local chunk = {} + local chunk_lines = {} + for j = 0, window - 1 do + if lines[i + j] then + table.insert(chunk, lines[i + j]) + table.insert(chunk_lines, i + j) + end + end + local joined = table.concat(chunk, ' ') + + if joined:match(pattern) then + -- Find which specific line in the chunk contains the name + local target_line = i + for k, line_num in ipairs(chunk_lines) do + if lines[line_num]:match('->name') then + target_line = line_num + break + end + end + + vim.cmd('edit ' .. route_file) + vim.fn.cursor(target_line, 1) + vim.cmd('normal! zz') + found = true + break + end + end + end if found then break end end end @@ -666,7 +1582,6 @@ function M.goto_config(config_key) end vim.cmd('normal! zz') - ui.info('Found config: ' .. config_key) else ui.warn('Config file not found: ' .. config_file .. '.php') end @@ -715,7 +1630,6 @@ function M.goto_translation(trans_key) end vim.cmd('normal! zz') - ui.info('Found translation: ' .. trans_key) return end end @@ -755,7 +1669,6 @@ function M.goto_env(env_key) vim.cmd('edit ' .. env_file) vim.fn.cursor(i, 1) vim.cmd('normal! zz') - ui.info('Found environment variable: ' .. env_key .. ' in ' .. vim.fn.fnamemodify(env_file, ':t')) return end end @@ -797,7 +1710,6 @@ function M.goto_asset(asset_path) for _, asset_file in ipairs(asset_locations) do if vim.fn.filereadable(asset_file) == 1 then vim.cmd('edit ' .. asset_file) - ui.info('Found asset: ' .. asset_path) return end end @@ -805,6 +1717,393 @@ function M.goto_asset(asset_path) ui.warn('Asset file not found: ' .. asset_path) end +-- Enhanced debug function to show treesitter parse information and all matches +function M.debug_treesitter_context() + local parser, ts = ts_utils.get_parser() + if not parser then + ui.error('Treesitter not available') + return + end + + local ok, tree = pcall(function() return parser:parse()[1] end) + if not ok or not tree then + ui.error('Failed to parse tree') + return + end + + local root = tree:root() + local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) + cursor_row = cursor_row - 1 + + local debug_info = { + 'Treesitter Debug Information:', + '============================', + 'Cursor Position: Row ' .. cursor_row .. ', Col ' .. cursor_col, + 'Filetype: ' .. vim.bo.filetype, + '', + 'Query Creation Status:', + '--------------------' + } + + local query = ts_utils.get_laravel_query() + if not query then + table.insert(debug_info, 'Failed to create main Laravel query') + else + table.insert(debug_info, 'Main Laravel query created successfully') + end + + table.insert(debug_info, '') + table.insert(debug_info, 'Raw AST at Cursor:') + table.insert(debug_info, '-----------------') + + -- Show the actual AST nodes at cursor position + local cursor_node = root:descendant_for_range(cursor_row, cursor_col, cursor_row, cursor_col) + if cursor_node then + local depth = 0 + local current = cursor_node + while current and depth < 8 do + local node_type = current:type() + local node_text = '' + local ok_text, text = pcall(vim.treesitter.get_node_text, current, 0) + if ok_text and text then + node_text = text:gsub('\n', '\\n'):sub(1, 50) + if #text > 50 then node_text = node_text .. '...' end + end + + local indent = string.rep(' ', depth) + table.insert(debug_info, indent .. '- ' .. node_type .. ': "' .. node_text .. '"') + + current = current:parent() + depth = depth + 1 + end + else + table.insert(debug_info, 'No node found at cursor position') + end + + table.insert(debug_info, '') + table.insert(debug_info, 'All Laravel Matches Found:') + table.insert(debug_info, '-------------------------') + + -- Find all Laravel patterns in the current function/scope + local current_function_node = root:descendant_for_range(cursor_row, 0, cursor_row, 1000) + local all_matches = {} + + -- Debug: Show what node we're searching in + if current_function_node then + table.insert(debug_info, 'Search Node Type: ' .. current_function_node:type()) + local ok_text, search_text = pcall(vim.treesitter.get_node_text, current_function_node, 0) + if ok_text and search_text then + local preview = search_text:gsub('\n', '\\n'):sub(1, 100) + if #search_text > 100 then preview = preview .. '...' end + table.insert(debug_info, 'Search Node Text: "' .. preview .. '"') + end + + -- Test with ultra basic queries + local simple_query_string = [[ + (name) @any_name + ]] + + local simple_ok, simple_query = pcall(vim.treesitter.query.parse, 'php', simple_query_string) + if simple_ok and simple_query then + table.insert(debug_info, 'Simple query created successfully') + table.insert(debug_info, '') + table.insert(debug_info, 'Simple Function Calls Found:') + table.insert(debug_info, '----------------------------') + + local simple_iter_ok, simple_iter = pcall(simple_query.iter_matches, simple_query, current_function_node, 0) + if simple_iter_ok and simple_iter then + local simple_captures = {} + for _, match, _ in simple_iter do + for id, node in pairs(match) do + if node then + local capture_name = simple_query.captures[id] + local ok_text, text = pcall(vim.treesitter.get_node_text, node, 0) + if ok_text and text then + if not simple_captures[capture_name] then + simple_captures[capture_name] = {} + end + table.insert(simple_captures[capture_name], text) + end + end + end + end + + -- Show summary of what we captured + if next(simple_captures) then + for capture_name, texts in pairs(simple_captures) do + table.insert(debug_info, ' ' .. capture_name .. ': ' .. #texts .. ' matches') + for i, text in ipairs(texts) do + if i <= 3 then -- Show first 3 matches + local preview = text:gsub('\n', '\\n'):sub(1, 40) + if #text > 40 then preview = preview .. '...' end + table.insert(debug_info, ' [' .. i .. '] "' .. preview .. '"') + elseif i == 4 then + table.insert(debug_info, ' ... and ' .. (#texts - 3) .. ' more') + break + end + end + end + else + table.insert(debug_info, ' No captures found') + end + else + table.insert(debug_info, ' Error iterating simple query') + end + else + table.insert(debug_info, ' Error creating simple query') + end + + table.insert(debug_info, '') + table.insert(debug_info, 'AST Node Analysis:') + table.insert(debug_info, '------------------') + + -- Let's manually find function calls in the AST and test queries on them + local found_function_calls = {} + local function find_function_calls(node, depth) + if depth > 10 then return end + + if node:type() == 'function_call_expression' then + table.insert(debug_info, 'Found function_call_expression at depth ' .. depth .. ':') + table.insert(found_function_calls, node) -- Store for testing + local child_count = node:child_count() + for i = 0, child_count - 1 do + local child = node:child(i) + if child then + local child_type = child:type() + local child_ok, child_text = pcall(vim.treesitter.get_node_text, child, 0) + if child_ok and child_text then + local preview = child_text:gsub('\n', '\\n'):sub(1, 50) + if #child_text > 50 then preview = preview .. '...' end + table.insert(debug_info, ' Child[' .. i .. '] ' .. child_type .. ': "' .. preview .. '"') + + -- If this child is the function name, let's see its structure + if child_type == 'name' then + table.insert(debug_info, ' ^ This is the function name!') + elseif child_type == 'arguments' then + table.insert(debug_info, ' ^ These are the arguments, exploring...') + local arg_count = child:child_count() + for j = 0, arg_count - 1 do + local arg_child = child:child(j) + if arg_child then + local arg_type = arg_child:type() + local arg_ok, arg_text = pcall(vim.treesitter.get_node_text, arg_child, 0) + if arg_ok and arg_text then + table.insert(debug_info, + ' Arg[' .. j .. '] ' .. arg_type .. ': "' .. arg_text .. '"') + end + end + end + end + end + end + end + end + + -- Recursively search children + for i = 0, node:child_count() - 1 do + local child = node:child(i) + if child then + find_function_calls(child, depth + 1) + end + end + end + + if current_function_node then + find_function_calls(current_function_node, 0) + end + + -- Test query directly on found function calls + if #found_function_calls > 0 and simple_query then + table.insert(debug_info, '') + table.insert(debug_info, 'Direct Query Test on Function Call:') + table.insert(debug_info, '-----------------------------------') + + local function_call_node = found_function_calls[1] + local direct_iter_ok, direct_iter = pcall(simple_query.iter_matches, simple_query, function_call_node, 0) + if direct_iter_ok and direct_iter then + local direct_captures = {} + for _, match, _ in direct_iter do + for id, node in pairs(match) do + if node then + local capture_name = simple_query.captures[id] + local ok_text, text = pcall(vim.treesitter.get_node_text, node, 0) + if ok_text and text then + if not direct_captures[capture_name] then + direct_captures[capture_name] = {} + end + table.insert(direct_captures[capture_name], text) + end + end + end + end + + if next(direct_captures) then + for capture_name, texts in pairs(direct_captures) do + table.insert(debug_info, ' ' .. capture_name .. ': ' .. table.concat(texts, ', ')) + end + else + table.insert(debug_info, ' No direct captures found either') + end + else + table.insert(debug_info, ' Error in direct query iteration') + end + end + + table.insert(debug_info, '') + table.insert(debug_info, 'Laravel Query Results:') + table.insert(debug_info, '--------------------') + + if not query then + table.insert(debug_info, 'Skipping - no Laravel query available') + else + local iter_ok, iter = pcall(query.iter_matches, query, current_function_node, 0) + if iter_ok and iter then + local match_count = 0 + for _, match, _ in iter do + match_count = match_count + 1 + table.insert(debug_info, 'Raw Match #' .. match_count .. ':') + + -- Show raw match info + for id, node in pairs(match) do + if node then + local capture_name = query.captures[id] + local ok_text, text = pcall(vim.treesitter.get_node_text, node, 0) + if ok_text and text then + local node_type = node:type() + table.insert(debug_info, + ' ' .. (capture_name or 'unknown') .. ' (' .. node_type .. '): "' .. text .. '"') + end + end + end + + local debug_steps, extraction_data = ts_utils.extract_call_info_debug(match, query) + + -- Add detailed extraction steps to debug output + for _, step in ipairs(debug_steps) do + table.insert(debug_info, ' ' .. step) + end + + local call_info = ts_utils.extract_call_info(match, query) + table.insert(debug_info, ' Final extract result: ' .. (call_info and 'SUCCESS' or 'FAILED')) + if call_info then + local match_details = { + count = match_count, + call_info = call_info, + ranges = {} + } + + -- Get node ranges for this match + for _, match_node in pairs(match) do + if match_node then + local ok_range, start_row, start_col, end_row, end_col = pcall(function() + return match_node:range() + end) + if ok_range then + table.insert(match_details.ranges, { + start_row = start_row, + start_col = start_col, + end_row = end_row, + end_col = end_col, + contains_cursor = start_row <= cursor_row and cursor_row <= end_row and + (start_row < cursor_row or start_col <= cursor_col) and + (end_row > cursor_row or end_col >= cursor_col) + }) + end + end + end + + table.insert(all_matches, match_details) + else + table.insert(debug_info, ' Failed to extract call info from this match') + end + end + + if match_count == 0 then + table.insert(debug_info, 'No Laravel matches found with main query') + end + else + table.insert(debug_info, 'Error iterating Laravel query: ' .. tostring(iter)) + end + end + else + table.insert(debug_info, 'No search node found for current line') + end + + -- Show parsed match summaries + if #all_matches > 0 then + table.insert(debug_info, '') + table.insert(debug_info, 'Parsed Matches Summary:') + table.insert(debug_info, '======================') + for _, match in ipairs(all_matches) do + table.insert(debug_info, 'Match #' .. match.count .. ':') + table.insert(debug_info, ' Function: ' .. (match.call_info.func or 'nil')) + table.insert(debug_info, ' Target: ' .. (match.call_info.partial or 'nil')) + table.insert(debug_info, ' Type: ' .. (match.call_info.call_type or 'nil')) + + for i, range in ipairs(match.ranges) do + local range_str = string.format(' Range %d: [%d,%d] to [%d,%d]', + i, range.start_row, range.start_col, range.end_row, range.end_col) + if range.contains_cursor then + range_str = range_str .. ' ← CONTAINS CURSOR' + end + table.insert(debug_info, range_str) + end + end + end + + -- Show what was actually selected + table.insert(debug_info, '') + table.insert(debug_info, 'Selected Match:') + table.insert(debug_info, '==============') + + local ok, call_info = pcall(ts_utils.get_laravel_call_at_cursor) + if ok and call_info then + table.insert(debug_info, 'Function Type: ' .. (call_info.func or 'nil')) + table.insert(debug_info, 'Target String: ' .. (call_info.partial or 'nil')) + table.insert(debug_info, 'Call Type: ' .. (call_info.call_type or 'nil')) + table.insert(debug_info, 'Function Name: ' .. (call_info.function_name or 'nil')) + table.insert(debug_info, 'Scope Name: ' .. (call_info.scope_name or 'nil')) + table.insert(debug_info, 'Method Name: ' .. (call_info.method_name or 'nil')) + table.insert(debug_info, 'All Arguments: ' .. table.concat(call_info.all_args or {}, ', ')) + else + table.insert(debug_info, 'No match selected or error occurred') + end + + ui.show_float(debug_info, { title = 'Enhanced Treesitter Debug' }) +end + +-- Test treesitter vs regex parsing +function M.compare_parsing_methods() + local ts_ok, ts_context = pcall(ts_utils.get_laravel_call_at_cursor) + local regex_works = false + + -- Get regex result without treesitter interference + local original_is_laravel_context_ts = ts_utils.is_laravel_context_ts + ts_utils.is_laravel_context_ts = function() return false end + regex_works = M.is_laravel_navigation_context() + ts_utils.is_laravel_context_ts = original_is_laravel_context_ts + + local comparison = { + 'Laravel Navigation Parsing Comparison:', + '=====================================', + '', + 'Treesitter Result:', + ts_ok and ts_context and (' Function: ' .. (ts_context.func or 'nil')) or ' No match found or error', + ts_ok and ts_context and (' Target: ' .. (ts_context.partial or 'nil')) or '', + ts_ok and ts_context and (' Call Type: ' .. (ts_context.call_type or 'nil')) or '', + not ts_ok and (' Error: ' .. tostring(ts_context)) or '', + '', + 'Regex Result:', + regex_works and ' Match found' or ' No match found', + '', + 'Recommendation:', + ts_ok and ts_context and ' Use treesitter navigation' or + (regex_works and ' Use regex fallback' or ' No navigation available'), + } + + ui.show_float(comparison, { title = 'Parsing Methods Comparison' }) +end + -- Navigate to Laravel global function documentation or definition function M.goto_laravel_global(global_func) if not global_func or global_func == '' then @@ -840,7 +2139,6 @@ function M.goto_laravel_global(global_func) local mapping = global_mappings[global_func] if not mapping then - ui.info('Laravel global function: ' .. global_func .. ' - Check Laravel documentation for details') return end @@ -858,16 +2156,11 @@ function M.goto_laravel_global(global_func) vim.cmd('edit ' .. file_path) elseif vim.fn.filereadable(file_path) == 1 then vim.cmd('edit ' .. file_path) - ui.info('Opened ' .. mapping.desc) elseif vim.fn.isdirectory(file_path) == 1 then vim.cmd('edit ' .. file_path) - ui.info('Opened ' .. mapping.desc) else ui.warn('File not found: ' .. file_path) - ui.info('Laravel global function: ' .. global_func .. ' - ' .. mapping.desc) end - else - ui.info('Laravel global function: ' .. global_func .. ' - ' .. mapping.desc) end end diff --git a/plugin/laravel.lua b/plugin/laravel.lua index a7062a5..94b49c6 100644 --- a/plugin/laravel.lua +++ b/plugin/laravel.lua @@ -808,6 +808,27 @@ local function setup_commands() end, { desc = 'Open Laravel Sail app in browser' }) + + -- Debug commands for treesitter navigation + -- vim.api.nvim_create_user_command('LaravelDebugTreesitter', function() + -- if not is_in_laravel_project() then + -- vim.notify('Not in a Laravel project', vim.log.levels.ERROR) + -- return + -- end + -- require('laravel.navigate').debug_treesitter_context() + -- end, { + -- desc = 'Debug treesitter parsing for Laravel navigation' + -- }) + + -- vim.api.nvim_create_user_command('LaravelCompareParsing', function() + -- if not is_in_laravel_project() then + -- vim.notify('Not in a Laravel project', vim.log.levels.ERROR) + -- return + -- end + -- require('laravel.navigate').compare_parsing_methods() + -- end, { + -- desc = 'Compare treesitter vs regex parsing methods' + -- }) end