Add config() Go to Definition support to VS Code extension

Always include params in window.rsxapp to reduce state variations
Add request params to window.rsxapp global
Enhance module creation commands with clear nomenclature guidance
Add module/submodule/feature nomenclature clarification to docs

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-30 19:13:57 +00:00
parent 8c8fb8e902
commit ac082bce2a
15 changed files with 546 additions and 16 deletions

View File

@@ -13,7 +13,7 @@ class Module_Create_Command extends Command
*
* @var string
*/
protected $signature = 'rsx:app:module:create
protected $signature = 'rsx:app:module:create
{name : Module name (lowercase with underscores)}';
/**
@@ -21,7 +21,7 @@ class Module_Create_Command extends Command
*
* @var string
*/
protected $description = 'Create a new RSX application module with default index feature';
protected $description = 'Create a new module (top-level section with shared layout). Example: frontend, admin';
/**
* Execute the console command.

View File

@@ -12,7 +12,7 @@ class Module_Feature_Create_Command extends Command
*
* @var string
*/
protected $signature = 'rsx:app:module:feature:create
protected $signature = 'rsx:app:module:feature:create
{module : Module name (must exist)}
{feature : Feature name (lowercase with underscores)}';
@@ -21,7 +21,7 @@ class Module_Feature_Create_Command extends Command
*
* @var string
*/
protected $description = 'Create a new feature within an RSX module';
protected $description = 'Create a feature (CRUD page group) within a module. Example: clients, tasks';
/**
* Execute the console command.
@@ -47,7 +47,14 @@ class Module_Feature_Create_Command extends Command
// Check if module exists
$module_path = base_path("rsx/app/{$module_name}");
if (!is_dir($module_path)) {
$this->error("Module '{$module_name}' does not exist. Create it first with: php artisan rsx:app:module:create {$module_name}");
$this->error("Module '{$module_name}' does not exist.");
$this->line('');
$this->line('NOMENCLATURE:');
$this->line(' Module = Top-level section with shared layout (e.g., frontend, admin)');
$this->line(' Feature = CRUD page group within a module (e.g., clients, tasks)');
$this->line('');
$this->line('Create the module first:');
$this->info(" php artisan rsx:app:module:create {$module_name}");
return 1;
}

View File

@@ -21,7 +21,7 @@ class Submodule_Create_Command extends Command
*
* @var string
*/
protected $description = 'Create a new submodule within an RSX module with embedded layout and default index feature';
protected $description = 'Create a submodule (page group with own layout within a module). Example: settings within frontend';
/**
* Execute the console command.
@@ -47,7 +47,14 @@ class Submodule_Create_Command extends Command
// Check if module exists
$module_path = base_path("rsx/app/{$module_name}");
if (!is_dir($module_path)) {
$this->error("Module '{$module_name}' does not exist. Create it first with: php artisan rsx:app:module:create {$module_name}");
$this->error("Module '{$module_name}' does not exist.");
$this->line('');
$this->line('NOMENCLATURE:');
$this->line(' Module = Top-level section with shared layout (e.g., frontend, admin)');
$this->line(' Submodule = Page group with own layout within a module (e.g., settings within frontend)');
$this->line('');
$this->line('Create the module first:');
$this->info(" php artisan rsx:app:module:create {$module_name}");
return 1;
}

View File

@@ -22,7 +22,7 @@ class Submodule_Feature_Create_Command extends Command
*
* @var string
*/
protected $description = 'Create a new feature within an RSX submodule';
protected $description = 'Create a feature (CRUD page group) within a submodule';
/**
* Execute the console command.
@@ -54,14 +54,24 @@ class Submodule_Feature_Create_Command extends Command
// Check if module exists
$module_path = base_path("rsx/app/{$module_name}");
if (!is_dir($module_path)) {
$this->error("Module '{$module_name}' does not exist. Create it first with: php artisan rsx:app:module:create {$module_name}");
$this->error("Module '{$module_name}' does not exist.");
$this->line('');
$this->line('Create the module first:');
$this->info(" php artisan rsx:app:module:create {$module_name}");
return 1;
}
// Check if submodule exists
$submodule_path = "{$module_path}/{$submodule_name}";
if (!is_dir($submodule_path)) {
$this->error("Submodule '{$submodule_name}' does not exist. Create it first with: php artisan rsx:app:submodule:create {$module_name} {$submodule_name}");
$this->error("Submodule '{$submodule_name}' does not exist in module '{$module_name}'.");
$this->line('');
$this->line('NOMENCLATURE:');
$this->line(' Submodule = Page group with own layout within a module');
$this->line(' Feature = CRUD page group within a submodule');
$this->line('');
$this->line('Create the submodule first:');
$this->info(" php artisan rsx:app:submodule:create {$module_name} {$submodule_name}");
return 1;
}

View File

@@ -23,7 +23,7 @@ class Submodule_Subfeature_Create_Command extends Command
*
* @var string
*/
protected $description = 'Create a new subfeature within an RSX submodule feature';
protected $description = 'Create a subfeature (individual CRUD operation like edit, view) within a feature';
/**
* Execute the console command.

View File

@@ -267,6 +267,10 @@ abstract class Rsx_Bundle_Abstract
$rsxapp_data['is_auth'] = Session::is_logged_in();
$rsxapp_data['ajax_disable_batching'] = config('rsx.development.ajax_disable_batching', false);
// Add current params (always set to reduce state variations)
$current_params = \App\RSpade\Core\Rsx::get_current_params();
$rsxapp_data['params'] = $current_params ?? [];
// Add user, site, and csrf data from session
$rsxapp_data['user'] = Session::get_user();
$rsxapp_data['site'] = Session::get_site();

View File

@@ -246,7 +246,7 @@ class Dispatcher
Debugger::console_debug('DISPATCH', 'Matched route to ' . $handler_class . '::' . $handler_method . ' params: ' . json_encode($params));
// Set current controller and action in Rsx for tracking
\App\RSpade\Core\Rsx::_set_current_controller_action($handler_class, $handler_method);
\App\RSpade\Core\Rsx::_set_current_controller_action($handler_class, $handler_method, $params);
// Load and validate handler class
static::__load_handler_class($handler_class);
@@ -574,7 +574,7 @@ class Dispatcher
}
// Set current controller and action for tracking
Rsx::_set_current_controller_action($class_name, $method_name);
Rsx::_set_current_controller_action($class_name, $method_name, $params);
// Check if this is a controller (all methods are static)
if (static::__is_controller($class_name)) {

View File

@@ -35,13 +35,20 @@ class Rsx
*/
protected static $current_action = null;
/**
* Current request params
* @var array|null
*/
protected static $current_params = null;
/**
* Set the current controller and action being executed
*
* @param string $controller_class The controller class name
* @param string $action_method The action method name
* @param array $params Optional request params to store
*/
public static function _set_current_controller_action($controller_class, $action_method)
public static function _set_current_controller_action($controller_class, $action_method, array $params = [])
{
// Extract just the class name without namespace
$parts = explode('\\', $controller_class);
@@ -49,6 +56,7 @@ class Rsx
static::$current_controller = $class_name;
static::$current_action = $action_method;
static::$current_params = $params;
}
/**
@@ -71,6 +79,16 @@ class Rsx
return static::$current_action;
}
/**
* Get the current request params
*
* @return array|null The current request params or null if not set
*/
public static function get_current_params()
{
return static::$current_params;
}
/**
* Clear the current controller and action tracking
*/
@@ -78,6 +96,7 @@ class Rsx
{
static::$current_controller = null;
static::$current_action = null;
static::$current_params = null;
}
/**

View File

@@ -0,0 +1,234 @@
MODULE_ORGANIZATION(3) RSX Framework Manual MODULE_ORGANIZATION(3)
NAME
Module Organization - RSX application structure and nomenclature
SYNOPSIS
php artisan rsx:app:module:create <name>
php artisan rsx:app:module:feature:create <module> <feature>
php artisan rsx:app:submodule:create <module> <submodule>
php artisan rsx:app:submodule:feature:create <module> <submodule> <feature>
php artisan rsx:app:submodule:subfeature:create <module> <submodule> <feature> <subfeature>
DESCRIPTION
RSX applications organize code into a hierarchical structure:
Module > Submodule > Feature > Subfeature
This structure enforces clear separation of concerns and predictable
file organization for both developers and AI assistants.
NOMENCLATURE
Module
Top-level section with shared layout and common UI elements.
Examples: frontend, admin, api
Rule: If pages share a layout, they belong in the same module.
Created with: rsx:app:module:create <name>
Structure:
rsx/app/frontend/
frontend_bundle.php
frontend_layout.blade.php
frontend_index_controller.php
frontend_index.blade.php
...
Submodule
Group of pages within a module with its own embedded layout.
Examples: settings within frontend, reports within admin
Rule: If pages within a module need their own layout and common
navigation, create a submodule.
Created with: rsx:app:submodule:create <module> <submodule>
Structure:
rsx/app/frontend/settings/
frontend_settings_layout.blade.php
frontend_settings.scss
frontend_settings_index_controller.php
...
Feature
CRUD page group for a specific entity or concept.
Examples: clients, tasks, users, projects
Rule: If pages perform CRUD operations on a single entity type,
they are a feature group.
Created with:
rsx:app:module:feature:create <module> <feature>
rsx:app:submodule:feature:create <module> <submodule> <feature>
Structure:
rsx/app/frontend/clients/
frontend_clients_controller.php
frontend_clients.blade.php
frontend_clients.js
frontend_clients.scss
Subfeature
Individual CRUD operation within a feature.
Examples: edit, view, delete, export
Rule: Each distinct operation on an entity is a subfeature.
Created with:
rsx:app:submodule:subfeature:create <module> <submodule> <feature> <subfeature>
Structure:
rsx/app/frontend/settings/profile/edit/
frontend_settings_profile_edit_controller.php
frontend_settings_profile_edit.blade.php
frontend_settings_profile_edit.js
frontend_settings_profile_edit.scss
EXAMPLES
Basic Module with Feature
Structure: frontend/clients/edit
Hierarchy: module, feature, subfeature
Commands:
php artisan rsx:app:module:create frontend
php artisan rsx:app:module:feature:create frontend clients
(edit subfeature created automatically with clients feature)
Module with Submodule
Structure: frontend/settings/profile/edit
Hierarchy: module, submodule, feature, subfeature
Commands:
php artisan rsx:app:module:create frontend
php artisan rsx:app:submodule:create frontend settings
php artisan rsx:app:submodule:feature:create frontend settings profile
php artisan rsx:app:submodule:subfeature:create frontend settings profile edit
Simple Admin Module
Structure: admin/sites
Hierarchy: module, feature
Commands:
php artisan rsx:app:module:create admin
php artisan rsx:app:module:feature:create admin sites
DIRECTORY STRUCTURE
Module Level
rsx/app/{module}/
{module}_bundle.php - Asset bundle definition
{module}_layout.blade.php - Shared layout for module
{module}_index_controller.php
{module}_index.blade.php
{module}_index.js
{module}_index.scss
Feature Level
rsx/app/{module}/{feature}/
{module}_{feature}_controller.php
{module}_{feature}.blade.php
{module}_{feature}.js
{module}_{feature}.scss
Submodule Level
rsx/app/{module}/{submodule}/
{module}_{submodule}_layout.blade.php - Embedded layout
{module}_{submodule}.scss
{module}_{submodule}_index_controller.php
...
Subfeature Level
rsx/app/{module}/{submodule}/{feature}/{subfeature}/
{module}_{submodule}_{feature}_{subfeature}_controller.php
{module}_{submodule}_{feature}_{subfeature}.blade.php
{module}_{submodule}_{feature}_{subfeature}.js
{module}_{submodule}_{feature}_{subfeature}.scss
ROUTING
Routes are automatically generated from the hierarchy:
Module index: /{module}
Module feature: /{module}/{feature}
Submodule index: /{module}/{submodule}
Submodule feature: /{module}/{submodule}/{feature}
Submodule subfeature: /{module}/{submodule}/{feature}/{subfeature}
Controllers use #[Route] attributes to define routes:
#[Route('/{module}/{feature}', name: 'module.feature.index')]
public static function index(Request $request, array $params = [])
DECISION GUIDE
When creating new pages, ask:
1. Does it share a layout with existing pages?
YES -> Add to existing module
NO -> Create new module
2. Does it need its own layout within a module?
YES -> Create submodule
NO -> Create feature in module
3. Is it a CRUD operation group?
YES -> Create feature
NO -> May need submodule or different organization
4. Is it a single CRUD operation?
YES -> Create subfeature
NO -> Create feature with multiple subfeatures
COMMON MISTAKES
Creating Module Instead of Feature
WRONG: php artisan rsx:app:module:create tasks
RIGHT: php artisan rsx:app:module:feature:create frontend tasks
Reason: tasks shares the frontend layout, so it's a feature not a module.
Creating Feature Before Module
WRONG: php artisan rsx:app:module:feature:create tasks edit
RIGHT: php artisan rsx:app:module:create frontend
php artisan rsx:app:module:feature:create frontend tasks
Error: "Module 'tasks' does not exist"
Solution: Create the module first, then add features to it.
Confusing Submodule with Feature
Submodule: Has embedded layout, groups related features
Feature: CRUD page group, uses parent layout
Use submodule for: Settings, Reports, Admin sections
Use feature for: Clients, Tasks, Users, Projects
NAMING CONVENTIONS
All names use lowercase with underscores only: [a-z_]+
Valid: frontend, client_portal, user_management
Invalid: Frontend, client-portal, userManagement, User_Management
FILE NAMING
All files follow predictable patterns:
Controller: {prefix}_controller.php
View: {prefix}.blade.php
JavaScript: {prefix}.js
SCSS: {prefix}.scss
Layout: {prefix}_layout.blade.php
Bundle: {prefix}_bundle.php
Where prefix is: {module}_{submodule}_{feature}_{subfeature}
(omitting parts not applicable)
SEE ALSO
rsx_architecture(3), routing(3), bundle_api(3), controller(3)

View File

@@ -154,6 +154,15 @@ class RspadeDefinitionProvider {
if (routeResult) {
return routeResult;
}
// Check for config() pattern - works in PHP and Blade files
if (['php', 'blade', 'html'].includes(languageId) ||
fileName.endsWith('.php') ||
fileName.endsWith('.blade.php')) {
const configResult = await this.handleConfigPattern(document, position);
if (configResult) {
return configResult;
}
}
// Check for href="/" pattern in Blade/Jqhtml files
if (fileName.endsWith('.blade.php') || fileName.endsWith('.jqhtml')) {
const hrefResult = await this.handleHrefPattern(document, position);
@@ -283,6 +292,101 @@ class RspadeDefinitionProvider {
}
return undefined;
}
/**
* Handle config() pattern in PHP and Blade files
* Detects patterns like:
* - config('rsx.default_user.email')
* - config("app.name")
*
* Searches in both system/config/ and rsx/resource/config/
* (rsx/resource/config/ takes precedence)
*/
async handleConfigPattern(document, position) {
const line = document.lineAt(position.line).text;
// Match config('key.path') or config("key.path")
const configPattern = /config\s*\(\s*(['"])([a-zA-Z0-9_.]+)\1\s*\)/g;
let match;
while ((match = configPattern.exec(line)) !== null) {
const fullMatch = match[0];
const configKey = match[2]; // e.g., "rsx.default_user.email"
const keyStart = match.index + match[0].indexOf(configKey);
const keyEnd = keyStart + configKey.length;
// Check if cursor is on the config key
if (position.character >= keyStart && position.character < keyEnd) {
// Parse the config key
const keyParts = configKey.split('.');
const configFile = keyParts[0]; // e.g., "rsx"
const nestedPath = keyParts.slice(1); // e.g., ["default_user", "email"]
// Get the RSpade project root
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
return undefined;
}
// Search for config file (prioritize rsx/resource/config/)
const rsxConfigPath = path.join(rspade_root, 'rsx', 'resource', 'config', `${configFile}.php`);
const systemConfigPath = path.join(rspade_root, 'system', 'config', `${configFile}.php`);
let configFilePath;
// Check rsx/resource/config first
if (fs.existsSync(rsxConfigPath)) {
configFilePath = rsxConfigPath;
}
else if (fs.existsSync(systemConfigPath)) {
configFilePath = systemConfigPath;
}
if (!configFilePath) {
return undefined;
}
// Read the config file
try {
const configContent = fs.readFileSync(configFilePath, 'utf8');
// Find the line containing the nested key
const location = this.findConfigKeyInFile(configContent, nestedPath, configFilePath);
if (location) {
this.clear_status_bar();
return location;
}
// If we can't find the specific key, just return the file
const fileUri = vscode.Uri.file(configFilePath);
const filePosition = new vscode.Position(0, 0);
this.clear_status_bar();
return new vscode.Location(fileUri, filePosition);
}
catch (error) {
console.error('Error reading config file:', error);
return undefined;
}
}
}
return undefined;
}
/**
* Find a nested config key in a PHP config file
* Returns the location of the key definition if found
*/
findConfigKeyInFile(content, nestedPath, filePath) {
if (nestedPath.length === 0) {
// No nested path, return start of file
const fileUri = vscode.Uri.file(filePath);
return new vscode.Location(fileUri, new vscode.Position(0, 0));
}
// Split content into lines
const lines = content.split('\n');
// Search for the key in the file
// For nested keys like ["default_user", "email"], we need to find:
// 1. First, find 'default_user' => [
// 2. Then find 'email' => value
// Simple approach: search for the last key in quotes
const targetKey = nestedPath[nestedPath.length - 1];
const keyPattern = new RegExp(`['"]${targetKey}['"]\\s*=>`, 'i');
for (let i = 0; i < lines.length; i++) {
if (keyPattern.test(lines[i])) {
const fileUri = vscode.Uri.file(filePath);
const position = new vscode.Position(i, 0);
return new vscode.Location(fileUri, position);
}
}
return undefined;
}
/**
* Handle href="/" pattern in Blade/Jqhtml files
* Detects when cursor is on "/" within href attribute

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.212",
"version": "0.1.214",
"publisher": "rspade",
"engines": {
"vscode": "^1.74.0"

Binary file not shown.

View File

@@ -160,6 +160,16 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
return routeResult;
}
// Check for config() pattern - works in PHP and Blade files
if (['php', 'blade', 'html'].includes(languageId) ||
fileName.endsWith('.php') ||
fileName.endsWith('.blade.php')) {
const configResult = await this.handleConfigPattern(document, position);
if (configResult) {
return configResult;
}
}
// Check for href="/" pattern in Blade/Jqhtml files
if (fileName.endsWith('.blade.php') || fileName.endsWith('.jqhtml')) {
const hrefResult = await this.handleHrefPattern(document, position);
@@ -304,6 +314,126 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
return undefined;
}
/**
* Handle config() pattern in PHP and Blade files
* Detects patterns like:
* - config('rsx.default_user.email')
* - config("app.name")
*
* Searches in both system/config/ and rsx/resource/config/
* (rsx/resource/config/ takes precedence)
*/
private async handleConfigPattern(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
const line = document.lineAt(position.line).text;
// Match config('key.path') or config("key.path")
const configPattern = /config\s*\(\s*(['"])([a-zA-Z0-9_.]+)\1\s*\)/g;
let match;
while ((match = configPattern.exec(line)) !== null) {
const fullMatch = match[0];
const configKey = match[2]; // e.g., "rsx.default_user.email"
const keyStart = match.index + match[0].indexOf(configKey);
const keyEnd = keyStart + configKey.length;
// Check if cursor is on the config key
if (position.character >= keyStart && position.character < keyEnd) {
// Parse the config key
const keyParts = configKey.split('.');
const configFile = keyParts[0]; // e.g., "rsx"
const nestedPath = keyParts.slice(1); // e.g., ["default_user", "email"]
// Get the RSpade project root
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
return undefined;
}
// Search for config file (prioritize rsx/resource/config/)
const rsxConfigPath = path.join(rspade_root, 'rsx', 'resource', 'config', `${configFile}.php`);
const systemConfigPath = path.join(rspade_root, 'system', 'config', `${configFile}.php`);
let configFilePath: string | undefined;
// Check rsx/resource/config first
if (fs.existsSync(rsxConfigPath)) {
configFilePath = rsxConfigPath;
} else if (fs.existsSync(systemConfigPath)) {
configFilePath = systemConfigPath;
}
if (!configFilePath) {
return undefined;
}
// Read the config file
try {
const configContent = fs.readFileSync(configFilePath, 'utf8');
// Find the line containing the nested key
const location = this.findConfigKeyInFile(configContent, nestedPath, configFilePath);
if (location) {
this.clear_status_bar();
return location;
}
// If we can't find the specific key, just return the file
const fileUri = vscode.Uri.file(configFilePath);
const filePosition = new vscode.Position(0, 0);
this.clear_status_bar();
return new vscode.Location(fileUri, filePosition);
} catch (error) {
console.error('Error reading config file:', error);
return undefined;
}
}
}
return undefined;
}
/**
* Find a nested config key in a PHP config file
* Returns the location of the key definition if found
*/
private findConfigKeyInFile(
content: string,
nestedPath: string[],
filePath: string
): vscode.Location | undefined {
if (nestedPath.length === 0) {
// No nested path, return start of file
const fileUri = vscode.Uri.file(filePath);
return new vscode.Location(fileUri, new vscode.Position(0, 0));
}
// Split content into lines
const lines = content.split('\n');
// Search for the key in the file
// For nested keys like ["default_user", "email"], we need to find:
// 1. First, find 'default_user' => [
// 2. Then find 'email' => value
// Simple approach: search for the last key in quotes
const targetKey = nestedPath[nestedPath.length - 1];
const keyPattern = new RegExp(`['"]${targetKey}['"]\\s*=>`, 'i');
for (let i = 0; i < lines.length; i++) {
if (keyPattern.test(lines[i])) {
const fileUri = vscode.Uri.file(filePath);
const position = new vscode.Position(i, 0);
return new vscode.Location(fileUri, position);
}
}
return undefined;
}
/**
* Handle href="/" pattern in Blade/Jqhtml files
* Detects when cursor is on "/" within href attribute