"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RspadeDefinitionProvider = void 0; const vscode = __importStar(require("vscode")); const path = __importStar(require("path")); const fs = __importStar(require("fs")); const ide_bridge_client_1 = require("./ide_bridge_client"); class RspadeDefinitionProvider { constructor(jqhtml_api) { // Create output channel and IDE bridge client const output_channel = vscode.window.createOutputChannel('RSpade Framework'); this.ide_bridge = new ide_bridge_client_1.IdeBridgeClient(output_channel); this.jqhtml_api = jqhtml_api; } /** * Find the RSpade project root folder (contains system/app/RSpade/) * Works in both single-folder and multi-root workspace modes */ find_rspade_root() { if (!vscode.workspace.workspaceFolders) { return undefined; } // Check each workspace folder for system/app/RSpade/ for (const folder of vscode.workspace.workspaceFolders) { const app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade'); if (fs.existsSync(app_rspade)) { return folder.uri.fsPath; } } return undefined; } show_error_status(message) { // Create status bar item if it doesn't exist if (!this.status_bar_item) { this.status_bar_item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); this.status_bar_item.command = 'workbench.action.output.toggleOutput'; this.status_bar_item.tooltip = 'Click to view RSpade output'; } // Set error message with icon this.status_bar_item.text = `$(error) RSpade: ${message}`; this.status_bar_item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); this.status_bar_item.show(); // Auto-hide after 5 seconds setTimeout(() => { this.clear_status_bar(); }, 5000); } clear_status_bar() { if (this.status_bar_item) { this.status_bar_item.hide(); } } async provideDefinition(document, position, token) { const languageId = document.languageId; const fileName = document.fileName; // Check for Route() pattern first - works in all file types const routeResult = await this.handleRoutePattern(document, position); if (routeResult) { return routeResult; } // Check for href="/" pattern in Blade/Jqhtml files if (fileName.endsWith('.blade.php') || fileName.endsWith('.jqhtml')) { const hrefResult = await this.handleHrefPattern(document, position); if (hrefResult) { return hrefResult; } } // Handle "this.xxx" references in .jqhtml files (highest priority for jqhtml files) if (fileName.endsWith('.jqhtml')) { const thisResult = await this.handleThisReference(document, position); if (thisResult) { return thisResult; } } // Handle jqhtml component tags in .blade.php and .jqhtml files // TEMPORARILY DISABLED FOR .jqhtml FILES: jqhtml extension now provides this feature // Re-enable by uncommenting: || fileName.endsWith('.jqhtml') if (fileName.endsWith('.blade.php') /* || fileName.endsWith('.jqhtml') */) { const componentResult = await this.handleJqhtmlComponent(document, position); if (componentResult) { return componentResult; } } // Handle JavaScript/TypeScript files and .js/.jqhtml files if (['javascript', 'typescript'].includes(languageId) || fileName.endsWith('.js') || fileName.endsWith('.jqhtml')) { const result = await this.handleJavaScriptDefinition(document, position); if (result) { return result; } } // Handle PHP and Blade files (RSX view references and class references) if (['php', 'blade', 'html'].includes(languageId) || fileName.endsWith('.php') || fileName.endsWith('.blade.php')) { const result = await this.handlePhpBladeDefinition(document, position); if (result) { return result; } } // As a fallback, check if the cursor is in a string that is a valid file path return this.handleFilePathInString(document, position); } /** * Handle Route() pattern for both PHP and JavaScript * Detects patterns like: * - Rsx::Route('Controller') (PHP, defaults to 'index') * - Rsx::Route('Controller', 'method') (PHP) * - Rsx.Route('Controller') (JavaScript, defaults to 'index') * - Rsx.Route('Controller', 'method') (JavaScript) */ async handleRoutePattern(document, position) { const line = document.lineAt(position.line).text; // First try to match two-parameter version // Matches: Rsx::Route('Controller', 'method') or Rsx.Route("Controller", "method") const routePatternTwo = /(?:Rsx::Route|Rsx\.Route)\s*\(\s*['"]([A-Z][A-Za-z0-9_]*)['"],\s*['"]([a-z_][a-z0-9_]*)['"].*?\)/; let match = line.match(routePatternTwo); if (match) { const [fullMatch, controller, method] = match; const matchStart = line.indexOf(fullMatch); const matchEnd = matchStart + fullMatch.length; // Check if cursor is within the Route() call if (position.character >= matchStart && position.character <= matchEnd) { // Always go to the method when clicking anywhere in Route() // This takes precedence over individual class name lookups try { const result = await this.queryIdeHelper(controller, method, 'class'); return this.createLocationFromResult(result); } catch (error) { // If method lookup fails, try just the controller try { const result = await this.queryIdeHelper(controller, undefined, 'class'); return this.createLocationFromResult(result); } catch (error2) { console.error('Error querying IDE helper for route:', error); } } } } // Try single-parameter version (defaults to 'index') // Matches: Rsx::Route('Controller') or Rsx.Route("Controller") const routePatternOne = /(?:Rsx::Route|Rsx\.Route)\s*\(\s*['"]([A-Z][A-Za-z0-9_]*)['"].*?\)/; match = line.match(routePatternOne); if (match) { const [fullMatch, controller] = match; const matchStart = line.indexOf(fullMatch); const matchEnd = matchStart + fullMatch.length; // Check if cursor is within the Route() call if (position.character >= matchStart && position.character <= matchEnd) { // Check if this is actually a two-parameter call by looking for a comma if (!fullMatch.includes(',')) { // Single parameter - default to 'index' const method = 'index'; try { const result = await this.queryIdeHelper(controller, method, 'class'); return this.createLocationFromResult(result); } catch (error) { // If method lookup fails, try just the controller try { const result = await this.queryIdeHelper(controller, undefined, 'class'); return this.createLocationFromResult(result); } catch (error2) { console.error('Error querying IDE helper for route:', error); } } } } } return undefined; } /** * Handle href="/" pattern in Blade/Jqhtml files * Detects when cursor is on "/" within href attribute * Resolves to the controller action that handles the root URL */ async handleHrefPattern(document, position) { const line = document.lineAt(position.line).text; // Match href="/" or href='/' const hrefPattern = /href\s*=\s*(['"])\/\1/g; let match; while ((match = hrefPattern.exec(line)) !== null) { const matchStart = match.index + match[0].indexOf('/'); const matchEnd = matchStart + 1; // Just the "/" character // Check if cursor is on the "/" if (position.character >= matchStart && position.character <= matchEnd) { try { // Query IDE bridge to resolve "/" URL to route const result = await this.ide_bridge.queryUrl('/'); if (result && result.found && result.controller && result.method) { // Resolved to controller/method - navigate to it const phpResult = await this.queryIdeHelper(result.controller, result.method, 'class'); return this.createLocationFromResult(phpResult); } } catch (error) { console.error('Error resolving href="/" to route:', error); } } } return undefined; } /** * Handle "this.xxx" references in .jqhtml files * Only handles patterns where cursor is on a word after "this." * Resolves to JavaScript class method if it exists */ async handleThisReference(document, position) { const line = document.lineAt(position.line).text; const fileName = document.fileName; // Check if cursor is on a word after "this." // Get the word at cursor position const wordRange = document.getWordRangeAtPosition(position); if (!wordRange) { return undefined; } const word = document.getText(wordRange); // Check if "this." appears before this word const beforeWord = line.substring(0, wordRange.start.character); if (!beforeWord.endsWith('this.')) { return undefined; } // Get the component name from the file let componentName; const fullText = document.getText(); const defineMatch = fullText.match(/ User_Card const baseName = path.basename(fileName, '.jqhtml'); if (baseName) { // Convert snake_case to PascalCase with underscores componentName = baseName.split('_').map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join('_'); } } if (!componentName) { return undefined; } try { // Try to find the JavaScript class and method // First try jqhtml_class type to find the JS file const result = await this.queryIdeHelper(componentName, word, 'jqhtml_class_method'); if (result && result.found) { return this.createLocationFromResult(result); } } catch (error) { // Method not found, try just the class try { const result = await this.queryIdeHelper(componentName, undefined, 'jqhtml_class'); return this.createLocationFromResult(result); } catch (error2) { console.error('Error querying IDE helper for this reference:', error2); } } return undefined; } /** * Handle jqhtml component tags in .blade.php and .jqhtml files * Detects uppercase HTML-like tags such as: * - * - content * - content */ async handleJqhtmlComponent(document, position) { console.log('[JQHTML Component] Entry point - checking component navigation'); // If JQHTML API not available, skip if (!this.jqhtml_api) { console.log('[JQHTML Component] JQHTML API not available - skipping'); return undefined; } console.log('[JQHTML Component] JQHTML API available'); // 1. Get the word at cursor position (component name pattern) const word_range = document.getWordRangeAtPosition(position, /[A-Z][A-Za-z0-9_]*/); if (!word_range) { console.log('[JQHTML Component] No word range found at cursor position'); return undefined; } const component_name = document.getText(word_range); console.log('[JQHTML Component] Found word at cursor:', component_name); // 2. Verify it's a component reference (starts with uppercase) if (!/^[A-Z]/.test(component_name)) { console.log('[JQHTML Component] Word does not start with uppercase - not a component'); return undefined; } console.log('[JQHTML Component] Word starts with uppercase - valid component name pattern'); // 3. Check if cursor is in a tag context const line = document.lineAt(position.line).text; const before_word = line.substring(0, word_range.start.character); console.log('[JQHTML Component] Line text:', line); console.log('[JQHTML Component] Text before word:', before_word); // Check for opening tags: stringStart && charPosition <= stringEnd) { // Cursor is inside this string break; } inString = false; stringStart = -1; stringEnd = -1; } } } // If we're inside a string, extract the identifier if (stringStart >= 0) { const stringContent = line.substring(stringStart + 1, stringEnd >= 0 ? stringEnd : line.length); // Check what context this string is in const beforeString = line.substring(0, stringStart); // Check if we're in a bundle 'include' array // Look for patterns like 'include' => [ or "include" => [ // We need to check previous lines too for context let inBundleInclude = false; // Check current line for include array if (/['"]include['"]\s*=>\s*\[/.test(line)) { inBundleInclude = true; } else { // Check previous lines for context (up to 10 lines back) for (let i = Math.max(0, position.line - 10); i < position.line; i++) { const prevLine = document.lineAt(i).text; if (/['"]include['"]\s*=>\s*\[/.test(prevLine)) { // Check if we haven't closed the array yet let openBrackets = 0; for (let j = i; j <= position.line; j++) { const checkLine = document.lineAt(j).text; openBrackets += (checkLine.match(/\[/g) || []).length; openBrackets -= (checkLine.match(/\]/g) || []).length; } if (openBrackets > 0) { inBundleInclude = true; break; } } } } // If we're in a bundle include array and the string looks like a bundle alias if (inBundleInclude && /^[a-z0-9]+$/.test(stringContent)) { try { const result = await this.queryIdeHelper(stringContent, undefined, 'bundle_alias'); if (result && result.found) { return this.createLocationFromResult(result); } } catch (error) { console.error('Error querying IDE helper for bundle alias:', error); } } // Check for RSX blade directives or function calls const rsxPatterns = [ /@rsx_extends\s*\(\s*$/, /@rsx_include\s*\(\s*$/, /@rsx_layout\s*\(\s*$/, /@rsx_component\s*\(\s*$/, /rsx_view\s*\(\s*$/, /rsx_include\s*\(\s*$/ ]; let isRsxView = false; for (const pattern of rsxPatterns) { if (pattern.test(beforeString)) { isRsxView = true; break; } } if (isRsxView && stringContent) { // Query as a view const result = await this.queryIdeHelper(stringContent, undefined, 'view'); return this.createLocationFromResult(result); } } // If not in a string, check for class references (like in PHP files) // But skip this if we're inside a Route() call const routePattern = /(?:Rsx::Route|Rsx\.Route)\s*\([^)]*\)/g; let isInRoute = false; let routeMatch; while ((routeMatch = routePattern.exec(line)) !== null) { const matchStart = routeMatch.index; const matchEnd = matchStart + routeMatch[0].length; if (position.character >= matchStart && position.character <= matchEnd) { isInRoute = true; break; } } if (!isInRoute) { const wordRange = document.getWordRangeAtPosition(position, /[A-Z][A-Za-z0-9_]*/); if (wordRange) { const word = document.getText(wordRange); // Check if this looks like an RSX class name if (word.includes('_') && /^[A-Z]/.test(word)) { try { const result = await this.queryIdeHelper(word, undefined, 'class'); return this.createLocationFromResult(result); } catch (error) { console.error('Error querying IDE helper for class:', error); } } } } return undefined; } createLocationFromResult(result) { if (result && result.found) { // Get the RSpade project root const rspade_root = this.find_rspade_root(); if (!rspade_root) { return undefined; } // Construct the full file path const filePath = path.join(rspade_root, result.file); const fileUri = vscode.Uri.file(filePath); // Create a position for the definition const position = new vscode.Position(result.line - 1, 0); // VS Code uses 0-based line numbers // Clear any error status on successful navigation this.clear_status_bar(); return new vscode.Location(fileUri, position); } return undefined; } /** * Handle file paths in strings - allows "Go to Definition" on file path strings * This is a fallback handler that only runs if other definitions aren't found */ async handleFilePathInString(document, position) { const line = document.lineAt(position.line).text; const charPosition = position.character; // Check if we're inside a string literal let inString = false; let stringStart = -1; let stringEnd = -1; let quoteChar = ''; // Find string boundaries around cursor position for (let i = 0; i < line.length; i++) { const char = line[i]; if ((char === '"' || char === "'") && (i === 0 || line[i - 1] !== '\\')) { if (!inString) { inString = true; stringStart = i; quoteChar = char; } else if (char === quoteChar) { stringEnd = i; if (charPosition > stringStart && charPosition <= stringEnd) { // Cursor is inside this string break; } inString = false; stringStart = -1; stringEnd = -1; } } } // If we're not inside a string, return undefined if (stringStart < 0) { return undefined; } // Extract the string content const stringContent = line.substring(stringStart + 1, stringEnd >= 0 ? stringEnd : line.length); // Check if the string looks like a file path (contains forward slashes or dots) if (!stringContent.includes('/') && !stringContent.includes('.')) { return undefined; } // Get the RSpade project root const rspade_root = this.find_rspade_root(); if (!rspade_root) { return undefined; } // Try to resolve the path relative to the workspace const possiblePath = path.join(rspade_root, stringContent); // Check if the file exists try { const stat = fs.statSync(possiblePath); if (stat.isFile()) { // Create a location for the file const fileUri = vscode.Uri.file(possiblePath); const position = new vscode.Position(0, 0); // Go to start of file return new vscode.Location(fileUri, position); } } catch (error) { // File doesn't exist, that's ok - just return undefined } return undefined; } async queryIdeHelper(identifier, methodName, type) { const params = { identifier }; if (methodName) { params.method = methodName; } if (type) { params.type = type; } try { const result = await this.ide_bridge.request('/_ide/service/resolve_class', params); return result; } catch (error) { this.show_error_status('IDE helper request failed'); throw error; } } } exports.RspadeDefinitionProvider = RspadeDefinitionProvider; //# sourceMappingURL=definition_provider.js.map