Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | // src/providers/RouteDefinitionProvider.ts import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import type { CacheManager } from '../cache/CacheManager'; // ─── Route Definition Provider ──────────────────────────────────────────── /** * Allows Ctrl+Click / F12 on route names and controller strings to * navigate directly to the relevant PHP file and method. * * Handles: * - route('user.index') → resolves to UserController::index() * - 'UserController@index' → resolves to controller file + method line * - [UserController::class, 'show'] → resolves to class + method */ export class RouteDefinitionProvider implements vscode.DefinitionProvider { constructor( private readonly cache: CacheManager, private readonly workspaceRoot: string ) {} async provideDefinition( document: vscode.TextDocument, position: vscode.Position ): Promise<vscode.Location | vscode.Location[] | undefined> { const wordRange = document.getWordRangeAtPosition(position, /['"][^'"]+['"]/); if (!wordRange) { return undefined; } const word = document.getText(wordRange).replace(/['"]/g, ''); // ── Strategy 1: Route name → controller ──────────────────────────── const routeLocation = await this.resolveRouteName(word); if (routeLocation) { return routeLocation; } // ── Strategy 2: Controller@Method string ─────────────────────────── if (word.includes('@')) { return this.resolveControllerAtMethod(word); } // ── Strategy 3: View name → blade file ──────────────────────────── const viewLocation = this.resolveViewName(word); if (viewLocation) { return viewLocation; } return undefined; } // ─── Route Name → Controller Method ────────────────────────────────── private async resolveRouteName(routeName: string): Promise<vscode.Location | undefined> { const index = this.cache.getIndex(); const route = index.routes.get(routeName); if (!route) { return undefined; } if (route.action === 'Closure' || !route.controllerClass) { return undefined; } return this.resolveControllerAtMethod( `${route.controllerClass}@${route.controllerMethod ?? 'index'}` ); } // ─── Controller@Method → File Location ─────────────────────────────── private async resolveControllerAtMethod( actionString: string ): Promise<vscode.Location | undefined> { const [className, methodName] = actionString.split('@'); if (!className) { return undefined; } const index = this.cache.getIndex(); // First try fully qualified lookup in symbol table let phpClass = index.classes.get(className); // Fallback: search by simple class name if (!phpClass) { for (const [, cls] of index.classes) { if (cls.name === className || cls.fullyQualifiedName.endsWith(`\\${className}`)) { phpClass = cls; break; } } } if (!phpClass) { // Last resort: try to find file by convention const guessedPath = this.guessControllerPath(className); if (guessedPath && fs.existsSync(guessedPath)) { const uri = vscode.Uri.file(guessedPath); return new vscode.Location(uri, new vscode.Position(0, 0)); } return undefined; } const uri = vscode.Uri.file(phpClass.filePath); if (!methodName) { return new vscode.Location(uri, new vscode.Position(phpClass.lineNumber - 1, 0)); } // Find the method's line number const method = phpClass.methods.find((m) => m.name === methodName); const line = method ? method.lineNumber - 1 : phpClass.lineNumber - 1; return new vscode.Location(uri, new vscode.Position(Math.max(0, line), 0)); } // ─── View Name → Blade File ─────────────────────────────────────────── private resolveViewName(viewName: string): vscode.Location | undefined { const index = this.cache.getIndex(); const filePath = index.views.get(viewName); if (filePath && fs.existsSync(filePath)) { return new vscode.Location( vscode.Uri.file(filePath), new vscode.Position(0, 0) ); } // Try convention: "auth.login" → resources/views/auth/login.blade.php const conventionPath = path.join( this.workspaceRoot, 'resources', 'views', ...viewName.split('.').map((p) => p) ) + '.blade.php'; if (fs.existsSync(conventionPath)) { return new vscode.Location( vscode.Uri.file(conventionPath), new vscode.Position(0, 0) ); } return undefined; } // ─── Helpers ────────────────────────────────────────────────────────── private guessControllerPath(className: string): string | undefined { // Strip namespace const simple = className.split('\\').pop() ?? className; if (!simple) { return undefined; } const candidates = [ path.join(this.workspaceRoot, 'app', 'Http', 'Controllers', `${simple}.php`), path.join(this.workspaceRoot, 'app', 'Http', 'Controllers', 'Api', `${simple}.php`), path.join(this.workspaceRoot, 'app', 'Http', 'Controllers', 'Admin', `${simple}.php`), ]; return candidates.find((c) => fs.existsSync(c)); } } |