Files
rspade_system/app/RSpade/resource/vscode_extension/out/definition_provider.js
root 37a6183eb4 Fix code quality violations and add VS Code extension features
Fix VS Code extension storage paths for new directory structure
Fix jqhtml compiled files missing from bundle
Fix bundle babel transformation and add rsxrealpath() function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 00:43:05 +00:00

604 lines
27 KiB
JavaScript

"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(/<Define:([A-Z][A-Za-z0-9_]*)/);
if (defineMatch) {
componentName = defineMatch[1];
}
else {
// If no Define tag, try to get component name from filename
// e.g., user_card.jqhtml -> 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:
* - <User_Card ... />
* - <User_Card ...>content</User_Card>
* - <Foo>content</Foo>
*/
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: <ComponentName or closing tags: </ComponentName
const is_in_tag_context = before_word.match(/<\s*$/) !== null ||
before_word.match(/<\/\s*$/) !== null;
console.log('[JQHTML Component] Is in tag context:', is_in_tag_context);
if (!is_in_tag_context) {
console.log('[JQHTML Component] Not in tag context - skipping');
return undefined;
}
// 4. Look up component using JQHTML API
console.log('[JQHTML Component] Calling JQHTML API findComponent for:', component_name);
const component_def = this.jqhtml_api.findComponent(component_name);
console.log('[JQHTML Component] JQHTML API result:', component_def);
if (!component_def) {
console.log('[JQHTML Component] Component not found in JQHTML index');
return undefined;
}
// 5. Return the location
console.log('[JQHTML Component] Returning location:', component_def.uri.fsPath, 'at position', component_def.position);
return new vscode.Location(component_def.uri, component_def.position);
}
async handleJavaScriptDefinition(document, position) {
// Get the word at the current position
const wordRange = document.getWordRangeAtPosition(position, /[A-Z][A-Za-z0-9_]*/);
if (!wordRange) {
return undefined;
}
const word = document.getText(wordRange);
// Check if this looks like an RSX class name (contains underscore and starts with capital)
if (!word.includes('_') || !/^[A-Z]/.test(word)) {
return undefined;
}
// Check if we're on a method call (look for a dot and method name after the class)
let method_name;
const line = document.lineAt(position.line).text;
const wordEnd = wordRange.end.character;
// Look for pattern like "ClassName.methodName"
const methodMatch = line.substring(wordEnd).match(/^\.([a-z_][a-z0-9_]*)/i);
if (methodMatch) {
method_name = methodMatch[1];
}
// Query the IDE helper endpoint
try {
const result = await this.queryIdeHelper(word, method_name, 'class');
return this.createLocationFromResult(result);
}
catch (error) {
console.error('Error querying IDE helper:', error);
}
return undefined;
}
async handlePhpBladeDefinition(document, position) {
const line = document.lineAt(position.line).text;
const charPosition = position.character;
// Pattern 1: @rsx_extends('View_Name') or @rsx_include('View_Name')
// Pattern 2: rsx_view('View_Name')
// Pattern 3: Class references like Demo_Controller
// 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 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