Fix unimplemented login route with # prefix

Fix IDE service routing and path normalization
Refactor IDE services and add session rotation

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-22 15:59:42 +00:00
parent fe2ef1b35b
commit e678b987c2
39 changed files with 2028 additions and 522 deletions

View File

@@ -27,6 +27,7 @@ exports.AutoRenameProvider = 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");
/**
* Provides automatic file renaming based on RSX naming conventions
*
@@ -43,6 +44,7 @@ class AutoRenameProvider {
this.config_enabled = false;
this.workspace_root = '';
this.is_checking = false;
this.ide_bridge_client = null;
this.init();
}
find_rspade_root() {
@@ -327,9 +329,24 @@ class AutoRenameProvider {
return class_name.toLowerCase() + '.php';
}
async get_suggested_js_filename(file_path, class_name, content) {
// Check if this extends Jqhtml_Component
const is_jqhtml = content.includes('extends Jqhtml_Component') ||
content.match(/extends\s+[A-Za-z0-9_]+\s+extends Jqhtml_Component/);
// Check if this extends Jqhtml_Component (directly or via inheritance)
let is_jqhtml = content.includes('extends Jqhtml_Component');
// If not directly extending, check via API
if (!is_jqhtml && content.includes('extends ')) {
// Initialize IDE bridge client if needed
if (!this.ide_bridge_client) {
const output_channel = vscode.window.createOutputChannel('RSpade Auto Rename');
this.ide_bridge_client = new ide_bridge_client_1.IdeBridgeClient(output_channel);
}
try {
const response = await this.ide_bridge_client.js_is_subclass_of(class_name, 'Jqhtml_Component');
is_jqhtml = response.is_subclass || false;
}
catch (error) {
// If API call fails, fall back to direct check only
console.log('[AutoRename] JS - Failed to check inheritance:', error);
}
}
console.log('[AutoRename] JS - Is Jqhtml_Component:', is_jqhtml);
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,77 @@
"use strict";
/**
* RSpade Definition Provider - "Go to Definition" for RSX Classes, Routes, and Components
*
* RESOLUTION TYPE PRIORITY MATRIX
* ================================
*
* This provider determines what to navigate to when you click "Go to Definition" on various
* identifiers across different file types. The resolution logic uses CSV type lists sent to
* the server endpoint `/_ide/service/resolve_class?type=X,Y,Z` which tries each type in order.
*
* FILE TYPE HANDLERS & RESOLUTION RULES:
*
* 1. ROUTE PATTERNS (all files)
* Pattern: Rsx::Route('Controller') or Rsx.Route('Controller', 'method')
* Type: 'php_class'
* Reason: Routes always point to PHP controllers (server-side)
*
* 2. HREF PATTERNS (Blade, jqhtml)
* Pattern: href="/"
* Type: 'php_class'
* Reason: Resolves URL to controller, always PHP
*
* 3. JQHTML EXTENDS ATTRIBUTE (jqhtml only)
* Pattern: <Define:My_Component extends="DataGrid_Abstract">
* Type: 'jqhtml_class,js_class'
* Reason: Component inheritance - try jqhtml component first, then JS class
*
* 4. JQHTML $xxx ATTRIBUTES (jqhtml only)
* Pattern: $data_source=Frontend_Controller.fetch_data
* Type: 'js_class,php_class'
* Reason: Try JS class first (for components), then PHP (for controllers/models)
*
* Pattern: $handler=this.on_click
* Type: 'jqhtml_class_method'
* Special: Resolves to current component's method
*
* 5. THIS REFERENCES (jqhtml only)
* Pattern: <%= this.data.users %>
* Type: 'jqhtml_class_method'
* Reason: Always current component's method/property
*
* 6. JAVASCRIPT CLASS REFERENCES (JS, jqhtml)
* Pattern: class My_Component extends DataGrid_Abstract
* Pattern: User_Controller.fetch_all()
* Type: 'js_class,php_class'
* Reason: Try JS first (component inheritance), then PHP (controllers/models)
* Note: JS stub files (auto-generated from PHP) are client-side only, not in manifest
*
* 7. PHP CLASS REFERENCES (PHP, Blade)
* Pattern: class Contacts_DataGrid extends DataGrid_Abstract
* Pattern: use Rsx\Lib\DataGrid_QueryBuilder;
* Type: 'php_class'
* Reason: In PHP files, class references are always PHP (not JavaScript)
*
* 8. BUNDLE ALIASES (PHP only)
* Pattern: 'include' => ['jqhtml', 'frontend']
* Type: 'bundle_alias'
* Reason: Resolves to bundle class definition
*
* 9. VIEW REFERENCES (PHP, Blade)
* Pattern: @rsx_extends('frontend.layout')
* Pattern: rsx_view('frontend.dashboard')
* Type: 'view'
* Reason: Resolves to Blade view template files
*
* METHOD RESOLUTION:
* When a pattern includes a method (e.g., Controller.method), the server attempts to find
* the specific method in the class. If the method isn't found but the class is, it returns
* the class location as a fallback.
*
* IMPORTANT: The server endpoint supports CSV type lists for priority ordering.
* Example: type='php_class,js_class' tries PHP first, then JavaScript.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -88,8 +161,19 @@ class RspadeDefinitionProvider {
return hrefResult;
}
}
// Handle "this.xxx" references in .jqhtml files (highest priority for jqhtml files)
// Handle jqhtml-specific patterns
if (fileName.endsWith('.jqhtml')) {
// Check for extends="ClassName" attribute
const extendsResult = await this.handleJqhtmlExtends(document, position);
if (extendsResult) {
return extendsResult;
}
// Check for $xxx=... attributes (must come before handleThisReference)
const attrResult = await this.handleJqhtmlAttribute(document, position);
if (attrResult) {
return attrResult;
}
// Handle "this.xxx" references in template expressions
const thisResult = await this.handleThisReference(document, position);
if (thisResult) {
return thisResult;
@@ -132,6 +216,9 @@ class RspadeDefinitionProvider {
* - Rsx::Route('Controller', 'method') (PHP)
* - Rsx.Route('Controller') (JavaScript, defaults to 'index')
* - Rsx.Route('Controller', 'method') (JavaScript)
*
* Resolution: Routes always point to PHP controllers (server-side)
* Type: 'php_class'
*/
async handleRoutePattern(document, position) {
const line = document.lineAt(position.line).text;
@@ -148,13 +235,13 @@ class RspadeDefinitionProvider {
// 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');
const result = await this.queryIdeHelper(controller, method, 'php_class');
return this.createLocationFromResult(result);
}
catch (error) {
// If method lookup fails, try just the controller
try {
const result = await this.queryIdeHelper(controller, undefined, 'class');
const result = await this.queryIdeHelper(controller, undefined, 'php_class');
return this.createLocationFromResult(result);
}
catch (error2) {
@@ -178,13 +265,13 @@ class RspadeDefinitionProvider {
// Single parameter - default to 'index'
const method = 'index';
try {
const result = await this.queryIdeHelper(controller, method, 'class');
const result = await this.queryIdeHelper(controller, method, 'php_class');
return this.createLocationFromResult(result);
}
catch (error) {
// If method lookup fails, try just the controller
try {
const result = await this.queryIdeHelper(controller, undefined, 'class');
const result = await this.queryIdeHelper(controller, undefined, 'php_class');
return this.createLocationFromResult(result);
}
catch (error2) {
@@ -215,8 +302,8 @@ class RspadeDefinitionProvider {
// 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');
// Resolved to controller/method - navigate to it (always PHP)
const phpResult = await this.queryIdeHelper(result.controller, result.method, 'php_class');
return this.createLocationFromResult(phpResult);
}
}
@@ -227,6 +314,106 @@ class RspadeDefinitionProvider {
}
return undefined;
}
/**
* Handle jqhtml extends="" attribute
* Detects patterns like:
* - <Define:My_Component extends="DataGrid_Abstract">
*
* Resolution: Try jqhtml component first, then JS class
* Type: 'jqhtml_class,js_class'
*/
async handleJqhtmlExtends(document, position) {
const line = document.lineAt(position.line).text;
// Match extends="ClassName" or extends='ClassName'
const extendsPattern = /extends\s*=\s*(['"])([A-Z][A-Za-z0-9_]*)\1/g;
let match;
while ((match = extendsPattern.exec(line)) !== null) {
const className = match[2];
const classStart = match.index + match[0].indexOf(className);
const classEnd = classStart + className.length;
// Check if cursor is on the class name
if (position.character >= classStart && position.character < classEnd) {
try {
// Try jqhtml component first, then JS class
const result = await this.queryIdeHelper(className, undefined, 'jqhtml_class,js_class');
return this.createLocationFromResult(result);
}
catch (error) {
console.error('Error resolving jqhtml extends:', error);
}
}
}
return undefined;
}
/**
* Handle jqhtml $xxx=... attributes
* Detects patterns like:
* - $data_source=Frontend_Controller.fetch_data
* - $on_click=this.handle_click
*
* Resolution logic:
* - If starts with "this.", resolve to current component's jqhtml class methods
* - Otherwise, resolve like JS class references: 'js_class,php_class'
*/
async handleJqhtmlAttribute(document, position) {
const line = document.lineAt(position.line).text;
// Match $attribute=Value or $attribute=this.method or $attribute=Class.method
// Pattern: $word=(this.)?(Word)(.word)?
const attrPattern = /\$[a-z_][a-z0-9_]*\s*=\s*(this\.)?([A-Z][A-Za-z0-9_]*)(?:\.([a-z_][a-z0-9_]*))?/gi;
let match;
while ((match = attrPattern.exec(line)) !== null) {
const hasThis = !!match[1]; // "this." prefix
const className = match[2];
const methodName = match[3]; // Optional method after dot
const classStart = match.index + match[0].indexOf(className);
const classEnd = classStart + className.length;
// Check if cursor is on the class name
if (position.character >= classStart && position.character < classEnd) {
if (hasThis) {
// this.method - resolve to current component's methods
// 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
const fileName = document.fileName;
const baseName = fileName.split('/').pop()?.replace('.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 {
// The className here is actually the method name after "this."
// We need to use the component name as the identifier
const result = await this.queryIdeHelper(componentName, className.toLowerCase(), 'jqhtml_class_method');
return this.createLocationFromResult(result);
}
catch (error) {
console.error('Error resolving jqhtml this reference:', error);
}
}
else {
// Class.method or Class - resolve like JS class references
try {
const result = await this.queryIdeHelper(className, methodName, 'js_class,php_class');
return this.createLocationFromResult(result);
}
catch (error) {
console.error('Error resolving jqhtml attribute class:', error);
}
}
}
}
return undefined;
}
/**
* Handle "this.xxx" references in .jqhtml files
* Only handles patterns where cursor is on a word after "this."
@@ -340,6 +527,19 @@ class RspadeDefinitionProvider {
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);
}
/**
* Handle JavaScript class references in .js and .jqhtml files
* Detects patterns like:
* - class My_Component extends DataGrid_Abstract
* - User_Controller.fetch_all()
* - await Product_Model.fetch(123)
*
* Resolution: Try JS classes first (for component inheritance), then PHP classes (for controllers/models)
* Type: 'js_class,php_class'
*
* Note: JS stub files (auto-generated from PHP) are client-side only and not in the manifest,
* so there's no conflict - the server will correctly return PHP classes when they exist.
*/
async handleJavaScriptDefinition(document, position) {
// Get the word at the current position
const wordRange = document.getWordRangeAtPosition(position, /[A-Z][A-Za-z0-9_]*/);
@@ -361,8 +561,9 @@ class RspadeDefinitionProvider {
method_name = methodMatch[1];
}
// Query the IDE helper endpoint
// Try JS classes first (component inheritance), then PHP (controllers/models)
try {
const result = await this.queryIdeHelper(word, method_name, 'class');
const result = await this.queryIdeHelper(word, method_name, 'js_class,php_class');
return this.createLocationFromResult(result);
}
catch (error) {
@@ -488,7 +689,9 @@ class RspadeDefinitionProvider {
// 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');
// When resolving from PHP files, only look for PHP classes
// This prevents jumping to JavaScript files when clicking on PHP class references
const result = await this.queryIdeHelper(word, undefined, 'php_class');
return this.createLocationFromResult(result);
}
catch (error) {

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
* Centralized client for communicating with RSpade framework IDE helper endpoints.
*
* AUTO-DISCOVERY SYSTEM:
* 1. Server creates storage/rsx-ide-bridge/domain.txt on first web request
* 1. Server creates system/storage/rsx-ide-bridge/domain.txt on first web request
* 2. Client reads domain.txt to discover server URL
* 3. Falls back to VS Code setting: rspade.serverUrl
* 4. Auto-retries with refreshed URL on connection failure
@@ -128,6 +128,44 @@ class IdeBridgeClient {
async queryUrl(url) {
return this.request('/_ide/service/resolve_url', { url }, 'GET');
}
/**
* Check if a JavaScript class extends another class (anywhere in the inheritance chain)
*
* @param subclass The potential subclass name
* @param superclass The potential superclass name
* @returns Promise with { is_subclass: boolean }
*/
async js_is_subclass_of(subclass, superclass) {
return this.request('/_ide/service/js_is_subclass_of', { subclass, superclass }, 'GET');
}
/**
* Check if a PHP class extends another class (anywhere in the inheritance chain)
*
* @param subclass The potential subclass name
* @param superclass The potential superclass name
* @returns Promise with { is_subclass: boolean }
*/
async php_is_subclass_of(subclass, superclass) {
return this.request('/_ide/service/php_is_subclass_of', { subclass, superclass }, 'GET');
}
/**
* Trigger incremental manifest build
*
* Calls Manifest::init() on the server to update the manifest cache.
* Does NOT clear the manifest, just performs incremental update of changed files.
*
* @returns Promise with { success: boolean }
*/
async manifest_build() {
try {
return await this.request('/_ide/service/manifest_build', {}, 'GET');
}
catch (error) {
// Log to console but don't throw - errors are silent to user
console.warn('[IdeBridge] Manifest build failed:', error.message);
return { success: false };
}
}
async make_request_with_retry(endpoint, data, method, retry_count) {
if (retry_count > 0) {
this.output_channel.appendLine(`\n--- RETRY ATTEMPT ${retry_count} ---`);
@@ -146,9 +184,14 @@ class IdeBridgeClient {
// Only retry once
if (retry_count === 0) {
const error_msg = error.message || '';
// Session expired or signature invalid - recreate session
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.output_channel.appendLine('Session/signature error, recreating session...');
// Authentication failure - recreate session
// Handles: "Session not found", "Invalid signature", "Authentication required"
// or any HTTP 401 response
if (error_msg.includes('Session not found') ||
error_msg.includes('Invalid signature') ||
error_msg.includes('Authentication required') ||
error_msg.includes('HTTP 401')) {
this.output_channel.appendLine('Authentication failed, recreating session...');
this.auth_data = null;
return this.make_request_with_retry(endpoint, data, method, retry_count + 1);
}
@@ -327,7 +370,7 @@ class IdeBridgeClient {
this.show_detailed_error();
throw new Error('RSpade project root not found');
}
const domain_file = path.join(rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
const domain_file = path.join(rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
this.output_channel.appendLine(`Checking for domain file: ${domain_file}`);
if (await exists(domain_file)) {
const domain = (await read_file(domain_file, 'utf8')).trim();
@@ -338,7 +381,7 @@ class IdeBridgeClient {
// domain.txt doesn't exist yet - schedule retry
this.schedule_retry();
this.show_detailed_error();
throw new Error('RSpade: storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
throw new Error('RSpade: system/storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
}
async negotiate_protocol(url_or_hostname) {
// Parse the input to extract hostname
@@ -427,7 +470,7 @@ class IdeBridgeClient {
if (!rspade_root) {
return;
}
const domain_file = path.join(rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
const domain_file = path.join(rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
const domain_dir = path.dirname(domain_file);
// Watch the directory (file might not exist yet)
if (fs.existsSync(domain_dir)) {
@@ -469,10 +512,11 @@ class IdeBridgeClient {
return undefined;
}
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first
// Try new structure first - check for system/app/RSpade
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
if (fs.existsSync(system_app_rspade)) {
return path.join(folder.uri.fsPath, 'system');
// Return project root (not system directory)
return folder.uri.fsPath;
}
// Fall back to legacy structure
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
@@ -507,7 +551,7 @@ class IdeBridgeClient {
this.output_channel.appendLine('\nThe extension needs to know your development server URL.');
this.output_channel.appendLine('\nPlease do ONE of the following:\n');
this.output_channel.appendLine('1. Load your site in a web browser');
this.output_channel.appendLine(' This will auto-create: storage/rsx-ide-bridge/domain.txt\n');
this.output_channel.appendLine(' This will auto-create: system/storage/rsx-ide-bridge/domain.txt\n');
this.output_channel.appendLine('2. Set VS Code setting: rspade.serverUrl');
this.output_channel.appendLine(' File → Preferences → Settings → Search "rspade"');
this.output_channel.appendLine(' Set to your development URL (e.g., https://myapp.test)\n');

File diff suppressed because one or more lines are too long

View File

@@ -56,20 +56,21 @@ const LIFECYCLE_DOCS = {
on_destroy: 'Component destruction phase - cleanup resources. Called before component is removed. MUST be synchronous.',
};
/**
* Cache for lineage lookups
* Cache for subclass checks
*/
const lineage_cache = new Map();
const subclass_cache = new Map();
/**
* IDE Bridge client instance (shared across all providers)
*/
let ide_bridge_client = null;
/**
* Get JavaScript class lineage from backend via IDE bridge
* Check if a JavaScript class extends another class (anywhere in inheritance chain)
*/
async function get_js_lineage(class_name) {
async function is_subclass_of_jqhtml_component(class_name) {
const cache_key = `${class_name}:Jqhtml_Component`;
// Check cache first
if (lineage_cache.has(class_name)) {
return lineage_cache.get(class_name);
if (subclass_cache.has(cache_key)) {
return subclass_cache.get(cache_key);
}
// Initialize IDE bridge client if needed
if (!ide_bridge_client) {
@@ -77,15 +78,15 @@ async function get_js_lineage(class_name) {
ide_bridge_client = new ide_bridge_client_1.IdeBridgeClient(output_channel);
}
try {
const response = await ide_bridge_client.request('/_ide/service/js_lineage', { class: class_name });
const lineage = response.lineage || [];
const response = await ide_bridge_client.js_is_subclass_of(class_name, 'Jqhtml_Component');
const is_subclass = response.is_subclass || false;
// Cache the result
lineage_cache.set(class_name, lineage);
return lineage;
subclass_cache.set(cache_key, is_subclass);
return is_subclass;
}
catch (error) {
// Re-throw error to fail loud - no silent fallbacks
throw new Error(`Failed to get JS lineage for ${class_name}: ${error.message}`);
throw new Error(`Failed to check if ${class_name} extends Jqhtml_Component: ${error.message}`);
}
}
/**
@@ -169,16 +170,14 @@ class JqhtmlLifecycleSemanticTokensProvider {
// Check if directly extends Jqhtml_Component
const is_jqhtml = directly_extends_jqhtml(text);
console.log(`[JQHTML] Directly extends Jqhtml_Component: ${is_jqhtml}`);
// If not directly extending, check lineage
// If not directly extending, check inheritance chain
let extends_jqhtml = is_jqhtml;
if (!is_jqhtml && has_extends_clause(text)) {
const class_name = extract_class_name(text);
console.log(`[JQHTML] Checking lineage for class: ${class_name}`);
console.log(`[JQHTML] Checking inheritance for class: ${class_name}`);
if (class_name) {
const lineage = await get_js_lineage(class_name);
console.log(`[JQHTML] Lineage: ${JSON.stringify(lineage)}`);
extends_jqhtml = lineage.includes('Jqhtml_Component');
console.log(`[JQHTML] Extends Jqhtml_Component via lineage: ${extends_jqhtml}`);
extends_jqhtml = await is_subclass_of_jqhtml_component(class_name);
console.log(`[JQHTML] Extends Jqhtml_Component via inheritance: ${extends_jqhtml}`);
}
}
// Highlight lifecycle methods (only if extends Jqhtml_Component)
@@ -271,8 +270,7 @@ class JqhtmlLifecycleHoverProvider {
if (!is_jqhtml) {
const class_name = extract_class_name(text);
if (class_name) {
const lineage = await get_js_lineage(class_name);
extends_jqhtml = lineage.includes('Jqhtml_Component');
extends_jqhtml = await is_subclass_of_jqhtml_component(class_name);
}
}
if (!extends_jqhtml) {
@@ -346,8 +344,7 @@ class JqhtmlLifecycleDiagnosticProvider {
if (!is_jqhtml) {
const class_name = extract_class_name(text);
if (class_name) {
const lineage = await get_js_lineage(class_name);
extends_jqhtml = lineage.includes('Jqhtml_Component');
extends_jqhtml = await is_subclass_of_jqhtml_component(class_name);
}
}
this.document_cache.set(cache_key, extends_jqhtml);

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"name": "rspade-framework",
"displayName": "RSpade Framework Support",
"description": "VS Code extension for RSpade framework with code folding, formatting, and namespace management",
"version": "0.1.182",
"version": "0.1.186",
"publisher": "rspade",
"engines": {
"vscode": "^1.74.0"

View File

@@ -1,6 +1,7 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { IdeBridgeClient } from './ide_bridge_client';
/**
* Provides automatic file renaming based on RSX naming conventions
@@ -17,6 +18,7 @@ export class AutoRenameProvider {
private config_enabled: boolean = false;
private workspace_root: string = '';
private is_checking = false;
private ide_bridge_client: IdeBridgeClient | null = null;
constructor() {
this.init();
@@ -354,9 +356,25 @@ export class AutoRenameProvider {
}
private async get_suggested_js_filename(file_path: string, class_name: string, content: string): Promise<string> {
// Check if this extends Jqhtml_Component
const is_jqhtml = content.includes('extends Jqhtml_Component') ||
content.match(/extends\s+[A-Za-z0-9_]+\s+extends Jqhtml_Component/);
// Check if this extends Jqhtml_Component (directly or via inheritance)
let is_jqhtml = content.includes('extends Jqhtml_Component');
// If not directly extending, check via API
if (!is_jqhtml && content.includes('extends ')) {
// Initialize IDE bridge client if needed
if (!this.ide_bridge_client) {
const output_channel = vscode.window.createOutputChannel('RSpade Auto Rename');
this.ide_bridge_client = new IdeBridgeClient(output_channel);
}
try {
const response = await this.ide_bridge_client.js_is_subclass_of(class_name, 'Jqhtml_Component');
is_jqhtml = response.is_subclass || false;
} catch (error) {
// If API call fails, fall back to direct check only
console.log('[AutoRename] JS - Failed to check inheritance:', error);
}
}
console.log('[AutoRename] JS - Is Jqhtml_Component:', is_jqhtml);

View File

@@ -1,3 +1,77 @@
/**
* RSpade Definition Provider - "Go to Definition" for RSX Classes, Routes, and Components
*
* RESOLUTION TYPE PRIORITY MATRIX
* ================================
*
* This provider determines what to navigate to when you click "Go to Definition" on various
* identifiers across different file types. The resolution logic uses CSV type lists sent to
* the server endpoint `/_ide/service/resolve_class?type=X,Y,Z` which tries each type in order.
*
* FILE TYPE HANDLERS & RESOLUTION RULES:
*
* 1. ROUTE PATTERNS (all files)
* Pattern: Rsx::Route('Controller') or Rsx.Route('Controller', 'method')
* Type: 'php_class'
* Reason: Routes always point to PHP controllers (server-side)
*
* 2. HREF PATTERNS (Blade, jqhtml)
* Pattern: href="/"
* Type: 'php_class'
* Reason: Resolves URL to controller, always PHP
*
* 3. JQHTML EXTENDS ATTRIBUTE (jqhtml only)
* Pattern: <Define:My_Component extends="DataGrid_Abstract">
* Type: 'jqhtml_class,js_class'
* Reason: Component inheritance - try jqhtml component first, then JS class
*
* 4. JQHTML $xxx ATTRIBUTES (jqhtml only)
* Pattern: $data_source=Frontend_Controller.fetch_data
* Type: 'js_class,php_class'
* Reason: Try JS class first (for components), then PHP (for controllers/models)
*
* Pattern: $handler=this.on_click
* Type: 'jqhtml_class_method'
* Special: Resolves to current component's method
*
* 5. THIS REFERENCES (jqhtml only)
* Pattern: <%= this.data.users %>
* Type: 'jqhtml_class_method'
* Reason: Always current component's method/property
*
* 6. JAVASCRIPT CLASS REFERENCES (JS, jqhtml)
* Pattern: class My_Component extends DataGrid_Abstract
* Pattern: User_Controller.fetch_all()
* Type: 'js_class,php_class'
* Reason: Try JS first (component inheritance), then PHP (controllers/models)
* Note: JS stub files (auto-generated from PHP) are client-side only, not in manifest
*
* 7. PHP CLASS REFERENCES (PHP, Blade)
* Pattern: class Contacts_DataGrid extends DataGrid_Abstract
* Pattern: use Rsx\Lib\DataGrid_QueryBuilder;
* Type: 'php_class'
* Reason: In PHP files, class references are always PHP (not JavaScript)
*
* 8. BUNDLE ALIASES (PHP only)
* Pattern: 'include' => ['jqhtml', 'frontend']
* Type: 'bundle_alias'
* Reason: Resolves to bundle class definition
*
* 9. VIEW REFERENCES (PHP, Blade)
* Pattern: @rsx_extends('frontend.layout')
* Pattern: rsx_view('frontend.dashboard')
* Type: 'view'
* Reason: Resolves to Blade view template files
*
* METHOD RESOLUTION:
* When a pattern includes a method (e.g., Controller.method), the server attempts to find
* the specific method in the class. If the method isn't found but the class is, it returns
* the class location as a fallback.
*
* IMPORTANT: The server endpoint supports CSV type lists for priority ordering.
* Example: type='php_class,js_class' tries PHP first, then JavaScript.
*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
@@ -94,8 +168,21 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
}
}
// Handle "this.xxx" references in .jqhtml files (highest priority for jqhtml files)
// Handle jqhtml-specific patterns
if (fileName.endsWith('.jqhtml')) {
// Check for extends="ClassName" attribute
const extendsResult = await this.handleJqhtmlExtends(document, position);
if (extendsResult) {
return extendsResult;
}
// Check for $xxx=... attributes (must come before handleThisReference)
const attrResult = await this.handleJqhtmlAttribute(document, position);
if (attrResult) {
return attrResult;
}
// Handle "this.xxx" references in template expressions
const thisResult = await this.handleThisReference(document, position);
if (thisResult) {
return thisResult;
@@ -143,6 +230,9 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
* - Rsx::Route('Controller', 'method') (PHP)
* - Rsx.Route('Controller') (JavaScript, defaults to 'index')
* - Rsx.Route('Controller', 'method') (JavaScript)
*
* Resolution: Routes always point to PHP controllers (server-side)
* Type: 'php_class'
*/
private async handleRoutePattern(
document: vscode.TextDocument,
@@ -165,12 +255,12 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
// 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');
const result = await this.queryIdeHelper(controller, method, 'php_class');
return this.createLocationFromResult(result);
} catch (error) {
// If method lookup fails, try just the controller
try {
const result = await this.queryIdeHelper(controller, undefined, 'class');
const result = await this.queryIdeHelper(controller, undefined, 'php_class');
return this.createLocationFromResult(result);
} catch (error2) {
console.error('Error querying IDE helper for route:', error);
@@ -196,12 +286,12 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
// Single parameter - default to 'index'
const method = 'index';
try {
const result = await this.queryIdeHelper(controller, method, 'class');
const result = await this.queryIdeHelper(controller, method, 'php_class');
return this.createLocationFromResult(result);
} catch (error) {
// If method lookup fails, try just the controller
try {
const result = await this.queryIdeHelper(controller, undefined, 'class');
const result = await this.queryIdeHelper(controller, undefined, 'php_class');
return this.createLocationFromResult(result);
} catch (error2) {
console.error('Error querying IDE helper for route:', error);
@@ -240,8 +330,8 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
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');
// Resolved to controller/method - navigate to it (always PHP)
const phpResult = await this.queryIdeHelper(result.controller, result.method, 'php_class');
return this.createLocationFromResult(phpResult);
}
} catch (error) {
@@ -253,6 +343,123 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
return undefined;
}
/**
* Handle jqhtml extends="" attribute
* Detects patterns like:
* - <Define:My_Component extends="DataGrid_Abstract">
*
* Resolution: Try jqhtml component first, then JS class
* Type: 'jqhtml_class,js_class'
*/
private async handleJqhtmlExtends(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
const line = document.lineAt(position.line).text;
// Match extends="ClassName" or extends='ClassName'
const extendsPattern = /extends\s*=\s*(['"])([A-Z][A-Za-z0-9_]*)\1/g;
let match;
while ((match = extendsPattern.exec(line)) !== null) {
const className = match[2];
const classStart = match.index + match[0].indexOf(className);
const classEnd = classStart + className.length;
// Check if cursor is on the class name
if (position.character >= classStart && position.character < classEnd) {
try {
// Try jqhtml component first, then JS class
const result = await this.queryIdeHelper(className, undefined, 'jqhtml_class,js_class');
return this.createLocationFromResult(result);
} catch (error) {
console.error('Error resolving jqhtml extends:', error);
}
}
}
return undefined;
}
/**
* Handle jqhtml $xxx=... attributes
* Detects patterns like:
* - $data_source=Frontend_Controller.fetch_data
* - $on_click=this.handle_click
*
* Resolution logic:
* - If starts with "this.", resolve to current component's jqhtml class methods
* - Otherwise, resolve like JS class references: 'js_class,php_class'
*/
private async handleJqhtmlAttribute(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
const line = document.lineAt(position.line).text;
// Match $attribute=Value or $attribute=this.method or $attribute=Class.method
// Pattern: $word=(this.)?(Word)(.word)?
const attrPattern = /\$[a-z_][a-z0-9_]*\s*=\s*(this\.)?([A-Z][A-Za-z0-9_]*)(?:\.([a-z_][a-z0-9_]*))?/gi;
let match;
while ((match = attrPattern.exec(line)) !== null) {
const hasThis = !!match[1]; // "this." prefix
const className = match[2];
const methodName = match[3]; // Optional method after dot
const classStart = match.index + match[0].indexOf(className);
const classEnd = classStart + className.length;
// Check if cursor is on the class name
if (position.character >= classStart && position.character < classEnd) {
if (hasThis) {
// this.method - resolve to current component's methods
// Get the component name from the file
let componentName: string | undefined;
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
const fileName = document.fileName;
const baseName = fileName.split('/').pop()?.replace('.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 {
// The className here is actually the method name after "this."
// We need to use the component name as the identifier
const result = await this.queryIdeHelper(componentName, className.toLowerCase(), 'jqhtml_class_method');
return this.createLocationFromResult(result);
} catch (error) {
console.error('Error resolving jqhtml this reference:', error);
}
} else {
// Class.method or Class - resolve like JS class references
try {
const result = await this.queryIdeHelper(className, methodName, 'js_class,php_class');
return this.createLocationFromResult(result);
} catch (error) {
console.error('Error resolving jqhtml attribute class:', error);
}
}
}
}
return undefined;
}
/**
* Handle "this.xxx" references in .jqhtml files
* Only handles patterns where cursor is on a word after "this."
@@ -391,6 +598,19 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
return new vscode.Location(component_def.uri, component_def.position);
}
/**
* Handle JavaScript class references in .js and .jqhtml files
* Detects patterns like:
* - class My_Component extends DataGrid_Abstract
* - User_Controller.fetch_all()
* - await Product_Model.fetch(123)
*
* Resolution: Try JS classes first (for component inheritance), then PHP classes (for controllers/models)
* Type: 'js_class,php_class'
*
* Note: JS stub files (auto-generated from PHP) are client-side only and not in the manifest,
* so there's no conflict - the server will correctly return PHP classes when they exist.
*/
private async handleJavaScriptDefinition(
document: vscode.TextDocument,
position: vscode.Position
@@ -420,8 +640,9 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
}
// Query the IDE helper endpoint
// Try JS classes first (component inheritance), then PHP (controllers/models)
try {
const result = await this.queryIdeHelper(word, method_name, 'class');
const result = await this.queryIdeHelper(word, method_name, 'js_class,php_class');
return this.createLocationFromResult(result);
} catch (error) {
console.error('Error querying IDE helper:', error);
@@ -563,7 +784,9 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
// 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');
// When resolving from PHP files, only look for PHP classes
// This prevents jumping to JavaScript files when clicking on PHP class references
const result = await this.queryIdeHelper(word, undefined, 'php_class');
return this.createLocationFromResult(result);
} catch (error) {
console.error('Error querying IDE helper for class:', error);

View File

@@ -4,7 +4,7 @@
* Centralized client for communicating with RSpade framework IDE helper endpoints.
*
* AUTO-DISCOVERY SYSTEM:
* 1. Server creates storage/rsx-ide-bridge/domain.txt on first web request
* 1. Server creates system/storage/rsx-ide-bridge/domain.txt on first web request
* 2. Client reads domain.txt to discover server URL
* 3. Falls back to VS Code setting: rspade.serverUrl
* 4. Auto-retries with refreshed URL on connection failure
@@ -119,6 +119,46 @@ export class IdeBridgeClient {
return this.request('/_ide/service/resolve_url', { url }, 'GET');
}
/**
* Check if a JavaScript class extends another class (anywhere in the inheritance chain)
*
* @param subclass The potential subclass name
* @param superclass The potential superclass name
* @returns Promise with { is_subclass: boolean }
*/
public async js_is_subclass_of(subclass: string, superclass: string): Promise<{ is_subclass: boolean }> {
return this.request('/_ide/service/js_is_subclass_of', { subclass, superclass }, 'GET');
}
/**
* Check if a PHP class extends another class (anywhere in the inheritance chain)
*
* @param subclass The potential subclass name
* @param superclass The potential superclass name
* @returns Promise with { is_subclass: boolean }
*/
public async php_is_subclass_of(subclass: string, superclass: string): Promise<{ is_subclass: boolean }> {
return this.request('/_ide/service/php_is_subclass_of', { subclass, superclass }, 'GET');
}
/**
* Trigger incremental manifest build
*
* Calls Manifest::init() on the server to update the manifest cache.
* Does NOT clear the manifest, just performs incremental update of changed files.
*
* @returns Promise with { success: boolean }
*/
public async manifest_build(): Promise<{ success: boolean }> {
try {
return await this.request('/_ide/service/manifest_build', {}, 'GET');
} catch (error: any) {
// Log to console but don't throw - errors are silent to user
console.warn('[IdeBridge] Manifest build failed:', error.message);
return { success: false };
}
}
private async make_request_with_retry(
endpoint: string,
data: any,
@@ -145,9 +185,14 @@ export class IdeBridgeClient {
if (retry_count === 0) {
const error_msg = error.message || '';
// Session expired or signature invalid - recreate session
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.output_channel.appendLine('Session/signature error, recreating session...');
// Authentication failure - recreate session
// Handles: "Session not found", "Invalid signature", "Authentication required"
// or any HTTP 401 response
if (error_msg.includes('Session not found') ||
error_msg.includes('Invalid signature') ||
error_msg.includes('Authentication required') ||
error_msg.includes('HTTP 401')) {
this.output_channel.appendLine('Authentication failed, recreating session...');
this.auth_data = null;
return this.make_request_with_retry(endpoint, data, method, retry_count + 1);
}
@@ -367,7 +412,7 @@ export class IdeBridgeClient {
throw new Error('RSpade project root not found');
}
const domain_file = path.join(rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
const domain_file = path.join(rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
this.output_channel.appendLine(`Checking for domain file: ${domain_file}`);
if (await exists(domain_file)) {
@@ -380,7 +425,7 @@ export class IdeBridgeClient {
// domain.txt doesn't exist yet - schedule retry
this.schedule_retry();
this.show_detailed_error();
throw new Error('RSpade: storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
throw new Error('RSpade: system/storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
}
private async negotiate_protocol(url_or_hostname: string): Promise<string> {
@@ -483,7 +528,7 @@ export class IdeBridgeClient {
return;
}
const domain_file = path.join(rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
const domain_file = path.join(rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
const domain_dir = path.dirname(domain_file);
// Watch the directory (file might not exist yet)
@@ -531,10 +576,11 @@ export class IdeBridgeClient {
}
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first
// Try new structure first - check for system/app/RSpade
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
if (fs.existsSync(system_app_rspade)) {
return path.join(folder.uri.fsPath, 'system');
// Return project root (not system directory)
return folder.uri.fsPath;
}
// Fall back to legacy structure
@@ -576,7 +622,7 @@ export class IdeBridgeClient {
this.output_channel.appendLine('\nThe extension needs to know your development server URL.');
this.output_channel.appendLine('\nPlease do ONE of the following:\n');
this.output_channel.appendLine('1. Load your site in a web browser');
this.output_channel.appendLine(' This will auto-create: storage/rsx-ide-bridge/domain.txt\n');
this.output_channel.appendLine(' This will auto-create: system/storage/rsx-ide-bridge/domain.txt\n');
this.output_channel.appendLine('2. Set VS Code setting: rspade.serverUrl');
this.output_channel.appendLine(' File → Preferences → Settings → Search "rspade"');
this.output_channel.appendLine(' Set to your development URL (e.g., https://myapp.test)\n');

View File

@@ -34,9 +34,9 @@ const LIFECYCLE_DOCS: { [key: string]: string } = {
};
/**
* Cache for lineage lookups
* Cache for subclass checks
*/
const lineage_cache = new Map<string, string[]>();
const subclass_cache = new Map<string, boolean>();
/**
* IDE Bridge client instance (shared across all providers)
@@ -44,12 +44,14 @@ const lineage_cache = new Map<string, string[]>();
let ide_bridge_client: IdeBridgeClient | null = null;
/**
* Get JavaScript class lineage from backend via IDE bridge
* Check if a JavaScript class extends another class (anywhere in inheritance chain)
*/
async function get_js_lineage(class_name: string): Promise<string[]> {
async function is_subclass_of_jqhtml_component(class_name: string): Promise<boolean> {
const cache_key = `${class_name}:Jqhtml_Component`;
// Check cache first
if (lineage_cache.has(class_name)) {
return lineage_cache.get(class_name)!;
if (subclass_cache.has(cache_key)) {
return subclass_cache.get(cache_key)!;
}
// Initialize IDE bridge client if needed
@@ -59,16 +61,16 @@ async function get_js_lineage(class_name: string): Promise<string[]> {
}
try {
const response = await ide_bridge_client.request('/_ide/service/js_lineage', { class: class_name });
const lineage = response.lineage || [];
const response = await ide_bridge_client.js_is_subclass_of(class_name, 'Jqhtml_Component');
const is_subclass = response.is_subclass || false;
// Cache the result
lineage_cache.set(class_name, lineage);
subclass_cache.set(cache_key, is_subclass);
return lineage;
return is_subclass;
} catch (error: any) {
// Re-throw error to fail loud - no silent fallbacks
throw new Error(`Failed to get JS lineage for ${class_name}: ${error.message}`);
throw new Error(`Failed to check if ${class_name} extends Jqhtml_Component: ${error.message}`);
}
}
@@ -164,17 +166,15 @@ export class JqhtmlLifecycleSemanticTokensProvider implements vscode.DocumentSem
const is_jqhtml = directly_extends_jqhtml(text);
console.log(`[JQHTML] Directly extends Jqhtml_Component: ${is_jqhtml}`);
// If not directly extending, check lineage
// If not directly extending, check inheritance chain
let extends_jqhtml = is_jqhtml;
if (!is_jqhtml && has_extends_clause(text)) {
const class_name = extract_class_name(text);
console.log(`[JQHTML] Checking lineage for class: ${class_name}`);
console.log(`[JQHTML] Checking inheritance for class: ${class_name}`);
if (class_name) {
const lineage = await get_js_lineage(class_name);
console.log(`[JQHTML] Lineage: ${JSON.stringify(lineage)}`);
extends_jqhtml = lineage.includes('Jqhtml_Component');
console.log(`[JQHTML] Extends Jqhtml_Component via lineage: ${extends_jqhtml}`);
extends_jqhtml = await is_subclass_of_jqhtml_component(class_name);
console.log(`[JQHTML] Extends Jqhtml_Component via inheritance: ${extends_jqhtml}`);
}
}
@@ -286,8 +286,7 @@ export class JqhtmlLifecycleHoverProvider implements vscode.HoverProvider {
if (!is_jqhtml) {
const class_name = extract_class_name(text);
if (class_name) {
const lineage = await get_js_lineage(class_name);
extends_jqhtml = lineage.includes('Jqhtml_Component');
extends_jqhtml = await is_subclass_of_jqhtml_component(class_name);
}
}
@@ -385,8 +384,7 @@ export class JqhtmlLifecycleDiagnosticProvider {
if (!is_jqhtml) {
const class_name = extract_class_name(text);
if (class_name) {
const lineage = await get_js_lineage(class_name);
extends_jqhtml = lineage.includes('Jqhtml_Component');
extends_jqhtml = await is_subclass_of_jqhtml_component(class_name);
}
}