Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "variable",
"format": ["snake_case", "UPPER_CASE"],
"leadingUnderscore": "allow"
},
{
"selector": "function",
"format": ["snake_case"],
"leadingUnderscore": "allow"
},
{
"selector": "class",
"format": ["PascalCase"]
}
],
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/semi": ["warn", "always"],
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn"
}
}

View File

@@ -0,0 +1,12 @@
.vscode/**
.vscode-test/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
node_modules/**
out/test/**

View File

@@ -0,0 +1,99 @@
# RSpade VS Code Extension
This directory contains the Visual Studio Code extension for the RSpade framework, providing enhanced development experience with automatic code folding, namespace management, and integrated formatting.
## Purpose
The RSpade extension enhances VS Code for RSpade framework development by:
- Automatically folding LLMDIRECTIVE blocks to reduce visual clutter
- Providing visual indicators and warnings for auto-generated RSX:USE sections
- Updating PHP namespaces automatically when files are moved or renamed
- Integrating PHP formatting directly into VS Code (replacing RunOnSave)
- Auto-renaming files to match RSX naming conventions (optional, configurable)
## Architecture
The extension is built with TypeScript and follows VS Code extension best practices:
### Core Components
1. **extension.ts** - Main entry point that activates providers and registers commands
2. **folding_provider.ts** - Implements automatic folding for LLMDIRECTIVE blocks
3. **decoration_provider.ts** - Adds visual indicators to RSX:USE sections
4. **file_watcher.ts** - Monitors file moves/renames and triggers namespace updates
5. **formatting_provider.ts** - Integrates with the main formatter for PHP formatting
6. **auto_rename_provider.ts** - Automatically renames files to match RSX naming conventions
7. **config.ts** - Centralized configuration management
### Key Features
1. **Code Folding**
- Automatically identifies and folds LLMDIRECTIVE blocks
- Uses VS Code's FoldingRangeProvider API
- Can be toggled via command or settings
2. **Protected Region Indicators**
- Highlights RSX:USE sections with yellow background
- Shows warning icons and hover messages
- Alerts users when editing auto-generated code
3. **Smart File Handling**
- Detects file moves and renames
- Automatically updates PHP namespaces for RSX files
- Works with drag-and-drop in VS Code explorer
4. **Integrated Formatting**
- Docker-first formatting via `/_idehelper/codeformat` endpoint
- Falls back to local PHP execution if Docker unavailable
- Uses the main formatter at `./bin/rsx-format`
- Graceful degradation - warns but allows save if formatting fails
- Works even when Laravel is broken (bypasses framework)
5. **Auto-Rename Files** (Optional - disabled by default)
- Automatically renames files in `./rsx` to match RSX naming conventions on save
- Supports PHP classes, JavaScript classes, .blade.php files with @rsx_id, and .jqhtml components
- Only renames if the correct filename doesn't already exist
- Respects short-name conventions (directory-based prefixes)
- Files containing `@FILENAME-CONVENTION-EXCEPTION` are skipped
- Enable via `config/rsx.php`: `'development' => ['auto_rename_files' => true]`
**Naming Conventions Applied:**
- **PHP classes in rsx/**: Lowercase, optional short name
- **JS classes in rsx/**: Lowercase, snake_case for Jqhtml_Component subclasses
- **.blade.php with @rsx_id**: Lowercase, optional short name
- **.jqhtml components**: snake_case (lowercase with underscores)
**Short Names:** If class is `Foo_Bar_Baz_Bom` in directory `./rsx/app/foo/bar/`, filename can be `baz_bom.php` instead of `foo_bar_baz_bom.php`
## Development
### Setup
```bash
npm install
npm run compile
```
### Testing
Press F5 in VS Code to launch a development instance with the extension loaded.
### Building
```bash
npm install -g vsce
vsce package
```
## Configuration
Users can configure the extension through VS Code settings:
- `rspade.enableCodeFolding` - Toggle LLMDIRECTIVE folding
- `rspade.enableReadOnlyRegions` - Toggle RSX:USE visual indicators
- `rspade.enableFormatOnMove` - Toggle automatic namespace updates
## Integration
The extension integrates with the existing RSpade formatting infrastructure:
- Format-on-save handled by RunOnSave extension configured in `.vscode/settings.json`
- Uses the main formatter at `./bin/rsx-format`
- PHP formatting via `./bin/formatters/php-formatter`
- JSON formatting via `./bin/formatters/json-formatter`
- Maintains compatibility with existing VS Code tasks and settings

View File

@@ -0,0 +1,101 @@
# Go to Definition - JavaScript to PHP Navigation
## Overview
The RSpade VS Code extension now supports "Go to Definition" navigation from JavaScript class references to their corresponding PHP implementations. This feature is particularly useful when working with RSX's Internal API system, where JavaScript code calls PHP methods through auto-generated stubs.
## How It Works
When you right-click on an identifier and select "Go to Definition", the extension will:
### For JavaScript Class References
1. Detect if the identifier looks like an RSX class (contains underscore, starts with capital letter)
2. Query the IDE helper endpoint (`/_idehelper`) to resolve the PHP file location
3. If a method is being called (e.g., `Demo_Index_Controller.hello_world()`), navigate directly to that method
4. Open the PHP file at the exact line where the class or method is defined
### For Bundle Aliases in PHP
1. Detect if you're in a bundle's `'include'` array
2. Check if the string is a bundle alias (lowercase single word like 'bootstrap5', 'jquery')
3. Query the IDE helper to resolve the alias to its bundle class
4. Open the bundle PHP file at the class definition
## Technical Implementation
### IDE Helper Endpoint
The endpoint `/_idehelper` (implemented in `/app/RSpade/Ide/Helper/Ide_Helper_Controller.php`) accepts:
- `class` parameter: The PHP class name to resolve
- `method` parameter (optional): The specific method to navigate to
Returns JSON with:
- `found`: Whether the class was found
- `file`: Relative path to the PHP file
- `line`: Line number where the class/method is defined
- `metadata`: Additional information about the class
### VS Code Extension
The `RspadeDefinitionProvider` (in `/app/RSpade/Extension/src/definition_provider.ts`):
- Implements VS Code's `DefinitionProvider` interface
- Registers for JavaScript and TypeScript files
- Makes HTTPS requests to the IDE helper endpoint
- Returns a `Location` object pointing to the PHP file
## Usage Examples
### JavaScript to PHP Navigation
```javascript
// In rsx/app/demo/demo_index.js
console.log(await Demo_Index_Controller.hello_world());
// ^^^^^^^^^^^^^^^^^^^^^ Right-click here
// Select "Go to Definition"
// Opens demo_index_controller.php at line 48
```
### Bundle Alias Navigation
```php
// In rsx/app/frontend/frontend_bundle.php
public static function define(): array
{
return [
'include' => [
'bootstrap5', // <- Right-click here, Go to Definition
// Opens Bootstrap5_Bundle.php
'jquery', // <- Opens Jquery_Bundle.php
'lodash', // <- Opens Lodash_Bundle.php
],
];
}
```
## Configuration
The base URL for the IDE helper can be configured in VS Code settings:
```json
{
"rspade.baseUrl": "https://rspade.claude.dev.hanson.xyz"
}
```
## Security
- The IDE helper endpoint is only available in development mode
- Production environments return a 403 Forbidden error
- No sensitive information is exposed through this endpoint
## Installation
After building the extension with `./build.sh`, install the generated `.vsix` file:
1. In VS Code: Extensions → ... → Install from VSIX
2. Or via command line: `code --install-extension rspade-framework.vsix`
## Limitations
- Only works for RSX class names (containing underscores, starting with capital letter)
- Requires the RSpade manifest to be up-to-date
- The PHP file must exist and be indexed in the manifest

View File

@@ -0,0 +1,158 @@
# JQHTML Component Highlighting in RSpade VS Code Extension
## Version 0.1.39 - Added JQHTML Component Support
The RSpade VS Code Extension now includes syntax highlighting for JQHTML components in Blade PHP files.
## What Gets Highlighted
### 1. Uppercase Component Tags
Any tag starting with an uppercase letter is treated as a JQHTML component and highlighted distinctively:
```blade
{{-- These get highlighted as JQHTML components --}}
<User_Card name="John" email="john@example.com" />
<Counter_Widget title="My Counter" initial_value="0" />
<DataGrid_Component rows="data" />
<MyCustomWidget>Content here</MyCustomWidget>
{{-- Regular HTML tags remain standard --}}
<div>
<span>
<button>
```
### 2. JQHTML Slots
Slot tags for components with multiple content areas:
```blade
{{-- Slot tags are highlighted specially --}}
<DataGrid>
<#header>Name | Email | Status</#header>
<#row><%= row.name %> | <%= row.email %></#row>
<#empty>No data found</#empty>
</DataGrid>
```
### 3. Blade Directives for JQHTML
The `@jqhtml` directive and `Jqhtml::component()` calls:
```blade
{{-- Blade directive --}}
@jqhtml('User_Card', ['name' => 'John'])
{{-- PHP helper --}}
{!! Jqhtml::component('User_Card', ['name' => 'John']) !!}
```
## Color Scheme
The extension uses the TextMate scope `entity.name.class.component.jqhtml` which provides:
| Theme | Typical Component Color |
|-------|-------------------------|
| Dark+ (VS Code default) | Cyan (#4EC9B0) |
| Light+ (VS Code default) | Teal (#267F99) |
| Monokai | Green (#A6E22E) |
| Dracula | Cyan (#8BE9FD) |
| Solarized Dark | Cyan (#2AA198) |
| Nord | Teal (#88C0D0) |
## How It Works
The extension injects TextMate grammar rules into PHP and Blade files to:
1. **Identify Component Tags**: Patterns match tags starting with uppercase letters
2. **Highlight Component Names**: Apply distinctive coloring to component names
3. **Preserve Blade Syntax**: Handle Blade expressions within component attributes
4. **Support Slots**: Recognize and highlight `<#slotname>` syntax
## Supported Patterns
### Basic Components
```blade
<User_Card name="Alice" />
<Counter_Widget initial_value="5" />
```
### Components with Content
```blade
<Container theme="dark">
<h2>Dashboard</h2>
<p>Content here</p>
</Container>
```
### Components with Blade Expressions
```blade
<User_Card
name="{{ $user->name }}"
email="{{ $user->email }}"
:data="@json($userData)"
/>
```
### Components with Conditional Attributes
```blade
<DataTable
@if($sortable)
sortable="true"
@endif
:items="@json($items)"
/>
```
### Nested Components
```blade
<Dashboard>
@foreach($users as $user)
<User_Card :user="$user" />
@endforeach
</Dashboard>
```
## Installation
The JQHTML highlighting is included in RSpade VS Code Extension version 0.1.39 and above.
To install or update:
```bash
# From the extension directory
./build.sh
# Then in VS Code
# Press Ctrl+Shift+X → ... → Install from VSIX
# Select: rspade-framework.vsix
```
## Technical Details
### Grammar File Location
`/app/RSpade/resource/vscode_extension/syntaxes/blade-jqhtml.tmLanguage.json`
### Injection Points
The grammar is injected into:
- `text.html.php.blade` - Laravel Blade files
- `text.html.php` - Standard PHP files with HTML
### Pattern Matching
- Component opening tags: `(<)([A-Z][\\w_]*)(?=\\s|>)`
- Component closing tags: `(</)([A-Z][\\w_]*)(>)`
- Slot tags: `(<#)(\\w+)(>)` and `(</#)(\\w+)(>)`
## Troubleshooting
If highlighting doesn't appear:
1. Ensure you have version 0.1.39+ of the RSpade extension
2. Reload VS Code window (Ctrl+R / Cmd+R)
3. Check that file is recognized as PHP/Blade
4. Verify your color theme supports the scope
## Future Enhancements
Potential improvements for future versions:
- IntelliSense for component names
- Auto-completion for component attributes
- Go-to-definition for component classes
- Hover documentation for components
- Component validation and error checking

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 RSpade Framework Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,113 @@
# RSpade VS Code Extension
## Overview
The RSpade VS Code extension provides development tools and enhancements specifically for RSpade framework projects.
## Features
### RSX:USE Section Management
- Automatically highlights RSX:USE sections in gray
- Shows info icons to indicate auto-generated code
- Prevents accidental editing of managed sections
### Smart File Operations
- Automatically updates PHP namespaces when files are moved
- Maintains RSX class references during refactoring
- Updates use statements as needed
### PHP Formatting Integration
- Uses the main formatter at `./bin/rsx-format`
- Handles PHP and JSON file formatting
- Preserves file modification times to prevent VS Code conflicts
- Formats on save via RunOnSave extension (configured in settings.json)
### LLMDIRECTIVE Folding
- Automatically collapses LLMDIRECTIVE comment blocks
- Reduces visual clutter in code
- Preserves directives for AI assistants
## Installation
### Automatic Installation
The extension auto-installs when you open a terminal in VS Code if:
- The project has `"rspade.projectType": "rspade"` in `.vscode/settings.json`
- Auto-install is enabled in settings
### Manual Installation
```bash
# From the extension directory
./build.sh
code --install-extension rspade-framework.vsix
```
## Building the Extension
```bash
cd /app/RSpade/Extension
./build.sh
```
The build script:
- Auto-increments version number
- Compiles TypeScript to JavaScript
- Packages as `rspade-framework.vsix`
- Runs inside Docker for consistency
## Configuration
Add to `.vscode/settings.json`:
```json
{
"rspade.projectType": "rspade",
"rspade.autoCheckExtension": true,
"rspade.autoInstallExtension": true
}
```
## Setup Scripts
Located in `.vscode/ide_setup/`:
### Unix/Linux/macOS: `check_setup.sh`
- Checks for Python, PHP, Node.js dependencies
- Verifies npm packages
- Checks extension status
- Auto-installs updates if configured
### Windows: `check_setup.ps1`
- PowerShell equivalent of Unix script
- Uses Chocolatey for package management
- Same functionality as Unix version
## Development
### Source Structure
```
src/
├── extension.ts - Main extension entry
├── folding_provider.ts - LLMDIRECTIVE folding
├── decoration_provider.ts - RSX:USE highlighting
├── file_watcher.ts - File operation handling
└── formatting_provider.ts - PHP formatting integration
```
### Testing
The extension is tested with:
- Manual testing in VS Code
- Integration with formatter tests
- File operation scenarios
## Troubleshooting
### Extension Not Loading
- Check `"rspade.projectType": "rspade"` in settings
- Verify extension is installed: `code --list-extensions | grep rspade`
- Check VS Code developer console for errors
### Formatting Issues
- The main formatter is now at: `./bin/rsx-format`
- For PHP files: `./bin/formatters/php-formatter`
- For JSON files: `./bin/formatters/json-formatter`
- Check IDE setup: `.vscode/ide_setup/check_setup.sh`
- Verify PHP is in PATH

View File

@@ -0,0 +1,118 @@
# RSpade VS Code Extension Setup Guide
## Overview
The RSpade VS Code extension can be set up in multiple ways depending on your needs:
1. **Development Mode** - For active development of the extension
2. **Production Mode** - For regular use with a packaged extension
3. **Automatic Setup** - Using provided scripts
## Setup Methods
### Method 1: Automatic Setup (Recommended)
The easiest way is to use the VS Code task:
1. Open Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
2. Run "Tasks: Run Task"
3. Select "RSpade: Setup VS Code Extension"
4. Choose between development or production mode
### Method 2: Manual Development Mode
For extension development without packaging:
```bash
# Unix/Linux/macOS
cd ~/.vscode/extensions
ln -s /var/www/html/app/RSpade/Extension rspade.rspade-framework-dev
# Windows (requires admin or use junction)
cd %APPDATA%\Code\User\extensions
mklink /D rspade.rspade-framework-dev "C:\path\to\project\app\RSpade\Extension"
```
Then compile the TypeScript:
```bash
cd /var/www/html/app/RSpade/Extension
npm install
npm run compile
```
### Method 3: Packaged Installation
For production use:
```bash
cd /var/www/html/app/RSpade/Extension
./install.sh # or install.ps1 on Windows
```
## VS Code Integration
### Automatic Recommendation
When you open the project, VS Code will automatically recommend the extension because it's listed in `.vscode/extensions.json`.
### Extension Features
Once installed, the extension provides:
1. **Automatic LLMDIRECTIVE Folding**
- Folds coding convention blocks by default
- Toggle with "RSpade: Toggle LLMDIRECTIVE Folding"
2. **RSX:USE Visual Indicators**
- Yellow highlighting for auto-generated sections
- Warning when editing these sections
3. **Format on Move**
- Automatically updates namespaces when moving PHP files
- Works with drag-and-drop in VS Code explorer
4. **Native PHP Formatting**
- Replaces RunOnSave extension
- Format on save or manually with "Format Document"
## Development Workflow
If you're developing the extension:
1. Make changes to TypeScript files in `src/`
2. Run `npm run compile` to rebuild
3. Reload VS Code window (Ctrl+R / Cmd+R)
4. Test your changes
## Troubleshooting
### Extension Not Loading
1. Check if it appears in Extensions view
2. Look for errors in Output > Extension Host
3. Ensure TypeScript is compiled: `npm run compile`
### Formatting Not Working
1. Ensure PHP is installed and accessible
2. Check `./bin/rsx-format` and `./bin/formatters/` are present
3. Verify RunOnSave extension is configured in `.vscode/settings.json`
### Development Symlink Issues
- On Windows, you may need admin rights for symlinks
- Use junction (`mklink /J`) as alternative
- Ensure no spaces in symlink name
## Configuration
After installation, configure via VS Code settings:
```json
{
"rspade.enableCodeFolding": true,
"rspade.enableReadOnlyRegions": true,
"rspade.enableFormatOnMove": true,
"rspade.pythonPath": "python3" // or "python" on Windows
}
```

View File

@@ -0,0 +1,38 @@
# Third Party Licenses
This project includes code from the following open source projects:
## Laravel VS Code Extension
- **Project**: Laravel VS Code Extension
- **Source**: https://github.com/laravel/vs-code-extension
- **License**: MIT License
- **Copyright**: Copyright (c) Taylor Otwell
- **Files Used**:
- `syntaxes/blade.tmLanguage.json` - Blade syntax grammar
### MIT License
```
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```

View File

@@ -0,0 +1,126 @@
#!/bin/bash
# RSpade VS Code Extension Build Script
# Builds the extension inside the Docker container
set -e # Exit on error
echo "╔═══════════════════════════════════════════╗"
echo "║ RSpade VS Code Extension Build Script ║"
echo "╚═══════════════════════════════════════════╝"
echo ""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# Step 1: Check we're in Docker
if [ ! -f /.dockerenv ]; then
echo -e "${YELLOW}⚠ Not running in Docker container${NC}"
echo "This script is designed to run inside the RSpade Docker container"
echo ""
echo "To build from outside Docker, run:"
echo " docker exec -it <container-name> /var/www/html/app/RSpade/resource/vscode_extension/build.sh"
exit 1
fi
echo -e "${GREEN}✓ Running in Docker container${NC}"
# Step 2: Clean previous builds
echo ""
echo "🧹 Cleaning previous builds..."
rm -rf out/
rm -f *.vsix
rm -rf node_modules/
rm -f package-lock.json
echo -e "${GREEN}✓ Cleaned${NC}"
# Step 3: Install dependencies
echo ""
echo "📦 Installing dependencies..."
npm install --no-save
echo -e "${GREEN}✓ Dependencies installed${NC}"
# Step 4: Compile TypeScript
echo ""
echo "🔨 Compiling TypeScript..."
npx tsc -p ./
echo -e "${GREEN}✓ Compiled successfully${NC}"
# Step 5: Run linter (optional, allow failures)
echo ""
echo "🔍 Running linter..."
npx eslint src --ext ts || {
echo -e "${YELLOW}⚠ Linting warnings found (continuing)${NC}"
}
# Step 6: Install @vscode/vsce locally (newer version without keytar)
echo ""
echo "📥 Installing @vscode/vsce locally..."
npm install --no-save @vscode/vsce
echo -e "${GREEN}✓ @vscode/vsce installed${NC}"
# Step 6.5: Auto-increment version
echo ""
echo "📈 Auto-incrementing version..."
# Get current version
CURRENT_VERSION=$(node -p "require('./package.json').version")
# Increment patch version
NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')
# Update package.json
sed -i "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" package.json
echo -e "${GREEN}✓ Version updated from $CURRENT_VERSION to $NEW_VERSION${NC}"
# Step 7: Package extension
echo ""
echo "📦 Packaging extension..."
npx @vscode/vsce package --no-dependencies --allow-missing-repository
# Find the generated .vsix file
VSIX_FILE=$(ls -t *.vsix 2>/dev/null | head -n1)
# Rename to standard filename
if [ -n "$VSIX_FILE" ] && [ "$VSIX_FILE" != "rspade-framework.vsix" ]; then
mv "$VSIX_FILE" "rspade-framework.vsix"
VSIX_FILE="rspade-framework.vsix"
fi
if [ -z "$VSIX_FILE" ]; then
echo -e "${RED}✗ Failed to create .vsix package${NC}"
exit 1
fi
# Step 8: Clean up build artifacts (keep .vsix)
echo ""
echo "🧹 Cleaning up build artifacts..."
rm -rf node_modules/
rm -f package-lock.json
echo -e "${GREEN}✓ Build artifacts cleaned${NC}"
# Step 9: Success!
echo ""
echo -e "${GREEN}✅ Build completed successfully!${NC}"
echo ""
echo "📦 Package created: ${YELLOW}$VSIX_FILE${NC}"
echo " Size: $(du -h "$VSIX_FILE" | cut -f1)"
echo " Path: $(pwd)/$VSIX_FILE"
echo ""
echo "📝 To install on your host machine:"
echo " 1. The .vsix file is available at:"
echo " ${YELLOW}/var/www/html/app/RSpade/resource/vscode_extension/$VSIX_FILE${NC}"
echo ""
echo " 2. In VS Code on your host:"
echo " - Press Ctrl+Shift+X (Extensions)"
echo " - Click '...' → 'Install from VSIX...'"
echo " - Navigate to the file above"
echo ""
echo " 3. Or from command line on host:"
echo " ${YELLOW}code --install-extension /path/to/$VSIX_FILE${NC}"
echo ""
echo "✨ Extension built in Docker container!"

View File

@@ -0,0 +1,51 @@
#!/bin/bash
# RSpade VS Code Extension Installation Script
echo "RSpade VS Code Extension Installer"
echo "================================="
echo ""
# Check if npm is installed
if ! command -v npm &> /dev/null; then
echo "Error: npm is not installed. Please install Node.js and npm first."
exit 1
fi
# Check if vsce is installed
if ! command -v vsce &> /dev/null; then
echo "Installing vsce (Visual Studio Code Extension manager)..."
npm install -g vsce
fi
# Install dependencies
echo "Installing dependencies..."
npm install
# Compile TypeScript
echo "Compiling extension..."
npm run compile
# Package extension
echo "Packaging extension..."
vsce package --no-dependencies
# Find the generated vsix file
VSIX_FILE=$(ls -t *.vsix 2>/dev/null | head -n1)
if [ -z "$VSIX_FILE" ]; then
echo "Error: No .vsix file was generated"
exit 1
fi
echo ""
echo "Extension packaged successfully: $VSIX_FILE"
echo ""
echo "To install in VS Code:"
echo "1. Open VS Code"
echo "2. Press Ctrl+Shift+X to open Extensions"
echo "3. Click the '...' menu and select 'Install from VSIX...'"
echo "4. Select: $(pwd)/$VSIX_FILE"
echo ""
echo "Or install from command line:"
echo "code --install-extension \"$(pwd)/$VSIX_FILE\""

View File

@@ -0,0 +1,27 @@
{
"comments": {
"blockComment": ["{{--", "--}}"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"],
["{{", "}}"],
["{!!", "!!}"],
["{{--", "--}}"]
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
}

View File

@@ -0,0 +1,462 @@
"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.AutoRenameProvider = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
/**
* Provides automatic file renaming based on RSX naming conventions
*
* This provider watches for file saves in ./rsx directory and automatically
* renames files to match their class names, @rsx_id, or <Define:> tags
* according to RSX framework conventions.
*
* Only runs if auto_rename_files is set to true in config/rsx.php or rsx/resource/config/rsx.php
* (user config takes precedence over framework config).
* Files containing @FILENAME-CONVENTION-EXCEPTION are skipped.
*/
class AutoRenameProvider {
constructor() {
this.config_enabled = false;
this.workspace_root = '';
this.is_checking = false;
this.init();
}
find_rspade_root() {
const workspace_folders = vscode.workspace.workspaceFolders;
if (!workspace_folders) {
return undefined;
}
// Check each workspace folder for app/RSpade/
for (const folder of workspace_folders) {
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
async init() {
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
return;
}
this.workspace_root = rspade_root;
await this.load_config();
}
async load_config() {
const framework_config_path = path.join(this.workspace_root, 'config', 'rsx.php');
const user_config_path = path.join(this.workspace_root, 'rsx', 'resource', 'config', 'rsx.php');
let framework_enabled = false;
let user_enabled = null;
// Load framework config
if (fs.existsSync(framework_config_path)) {
try {
const content = fs.readFileSync(framework_config_path, 'utf8');
const value = this.extract_auto_rename_value(content);
if (value !== null) {
framework_enabled = value;
console.log('[AutoRename] Framework config - auto_rename_files:', framework_enabled);
}
}
catch (error) {
console.error('[AutoRename] Failed to load framework config:', error);
}
}
else {
console.log('[AutoRename] Framework config file not found:', framework_config_path);
}
// Load user config (takes precedence)
if (fs.existsSync(user_config_path)) {
try {
const content = fs.readFileSync(user_config_path, 'utf8');
const value = this.extract_auto_rename_value(content);
if (value !== null) {
user_enabled = value;
console.log('[AutoRename] User config - auto_rename_files:', user_enabled);
}
}
catch (error) {
console.error('[AutoRename] Failed to load user config:', error);
}
}
else {
console.log('[AutoRename] User config file not found:', user_config_path);
}
// User config takes precedence over framework config
this.config_enabled = user_enabled !== null ? user_enabled : framework_enabled;
console.log('[AutoRename] Final config - auto_rename_files:', this.config_enabled);
}
extract_auto_rename_value(content) {
// Look for development.auto_rename_files setting
// Match pattern: 'development' => [ ... 'auto_rename_files' => true/false ... ]
const development_section_match = content.match(/'development'\s*=>\s*\[([\s\S]*?)\],\s*\/\*/);
if (development_section_match) {
const development_content = development_section_match[1];
const auto_rename_match = development_content.match(/'auto_rename_files'\s*=>\s*(true|false)/);
if (auto_rename_match) {
return auto_rename_match[1] === 'true';
}
}
return null;
}
activate(context) {
console.log('[AutoRename] Provider activated');
// Watch for completed file saves (not before save)
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(async (document) => {
if (this.is_checking) {
console.log('[AutoRename] Already checking, skipping');
return; // Prevent recursive calls
}
// Only process if this document is currently active in the editor
const active_editor = vscode.window.activeTextEditor;
if (!active_editor || active_editor.document !== document) {
console.log('[AutoRename] Document not active, skipping (likely bulk save/replace)');
return;
}
await this.load_config(); // Reload config on each save
if (!this.config_enabled) {
console.log('[AutoRename] Feature disabled in config');
return;
}
const file_path = document.uri.fsPath;
console.log('[AutoRename] File saved:', file_path);
// Only process files in ./rsx directory
const relative_path = path.relative(this.workspace_root, file_path);
console.log('[AutoRename] Relative path:', relative_path);
if (!relative_path.startsWith('rsx/') && !relative_path.startsWith('rsx\\')) {
console.log('[AutoRename] Not in rsx/ directory, skipping');
return;
}
// Check for exception marker
const content = document.getText();
if (content.includes('@FILENAME-CONVENTION-EXCEPTION')) {
console.log('[AutoRename] File contains @FILENAME-CONVENTION-EXCEPTION, skipping');
return;
}
this.is_checking = true;
try {
console.log('[AutoRename] Starting rename check...');
await this.check_and_rename(document);
}
finally {
this.is_checking = false;
}
}));
}
async check_and_rename(document) {
const file_path = document.uri.fsPath;
const extension = this.get_extension(file_path);
const content = document.getText();
console.log('[AutoRename] Extension detected:', extension);
let identifier = null;
let suggested_filename = null;
// Determine identifier based on file type
if (extension === 'php') {
identifier = this.extract_php_class(content);
console.log('[AutoRename] PHP class extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_php_filename(file_path, identifier);
}
}
else if (extension === 'js') {
identifier = this.extract_js_class(content);
console.log('[AutoRename] JS class extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_js_filename(file_path, identifier, content);
}
}
else if (extension === 'blade.php') {
identifier = this.extract_rsx_id(content);
console.log('[AutoRename] Blade @rsx_id extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_blade_filename(file_path, identifier);
}
}
else if (extension === 'jqhtml') {
identifier = this.extract_jqhtml_component(content);
console.log('[AutoRename] Jqhtml component extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_jqhtml_filename(file_path, identifier);
}
}
console.log('[AutoRename] Identifier:', identifier);
console.log('[AutoRename] Suggested filename:', suggested_filename);
if (!identifier || !suggested_filename) {
console.log('[AutoRename] No identifier or suggested filename, skipping');
return;
}
const current_filename = path.basename(file_path);
console.log('[AutoRename] Current filename:', current_filename);
if (current_filename.toLowerCase() === suggested_filename.toLowerCase()) {
console.log('[AutoRename] Filename already correct (case-insensitive match)');
return; // Already correct
}
// Check if suggested filename already exists
const dir = path.dirname(file_path);
const new_path = path.join(dir, suggested_filename);
if (fs.existsSync(new_path)) {
console.log('[AutoRename] Cannot rename: ${suggested_filename} already exists at', new_path);
return;
}
// Perform rename
console.log('[AutoRename] Performing rename to:', suggested_filename);
await this.rename_file(file_path, new_path);
}
async rename_file(old_path, new_path) {
const old_uri = vscode.Uri.file(old_path);
const new_uri = vscode.Uri.file(new_path);
console.log('[AutoRename] Renaming from:', old_path);
console.log('[AutoRename] Renaming to:', new_path);
try {
// Capture cursor position and view column before closing
let cursor_position;
let view_column;
const old_doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === old_path);
if (old_doc) {
// Find the editor for this document to capture cursor position
const old_editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.fsPath === old_path);
if (old_editor) {
cursor_position = old_editor.selection.active;
view_column = old_editor.viewColumn;
console.log('[AutoRename] Captured cursor position:', cursor_position.line, cursor_position.character);
console.log('[AutoRename] Captured view column:', view_column);
}
console.log('[AutoRename] Closing old document');
await vscode.window.showTextDocument(old_doc, { preview: false, preserveFocus: false });
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
}
// Use VS Code's rename API for proper integration
const edit = new vscode.WorkspaceEdit();
edit.renameFile(old_uri, new_uri, { overwrite: false });
const success = await vscode.workspace.applyEdit(edit);
console.log('[AutoRename] Rename result:', success);
if (success) {
console.log(`[AutoRename] ✅ Auto-renamed: ${path.basename(old_path)}${path.basename(new_path)}`);
// Wait a bit for the file system to settle
await new Promise(resolve => setTimeout(resolve, 100));
// Open the renamed file
const new_document = await vscode.workspace.openTextDocument(new_uri);
console.log('[AutoRename] Opened renamed document');
// Show the document in the editor, restoring view column if we had one
const show_options = {
preview: false,
viewColumn: view_column
};
const editor = await vscode.window.showTextDocument(new_document, show_options);
console.log('[AutoRename] Showing renamed document in editor');
// Restore cursor position if we captured one
if (cursor_position) {
editor.selection = new vscode.Selection(cursor_position, cursor_position);
editor.revealRange(new vscode.Range(cursor_position, cursor_position));
console.log('[AutoRename] Restored cursor position');
}
// Format the document
console.log('[AutoRename] Formatting document...');
await vscode.commands.executeCommand('editor.action.formatDocument');
console.log('[AutoRename] Format command executed');
// Save the formatted document
await new_document.save();
console.log('[AutoRename] Saved formatted document');
}
}
catch (error) {
console.error('[AutoRename] ❌ Failed to rename file:', error);
}
}
get_extension(file_path) {
if (file_path.endsWith('.blade.php')) {
return 'blade.php';
}
if (file_path.endsWith('.jqhtml')) {
return 'jqhtml';
}
return path.extname(file_path).substring(1);
}
extract_php_class(content) {
// Match: class ClassName
const match = content.match(/^\s*class\s+([A-Za-z0-9_]+)/m);
return match ? match[1] : null;
}
extract_js_class(content) {
// Match: class ClassName
const match = content.match(/^\s*class\s+([A-Za-z0-9_]+)/m);
return match ? match[1] : null;
}
extract_rsx_id(content) {
// Match: @rsx_id('identifier')
const match = content.match(/@rsx_id\s*\(\s*['"]([^'"]+)['"]\s*\)/);
return match ? match[1] : null;
}
extract_jqhtml_component(content) {
// Match: <Define:ComponentName>
const match = content.match(/<Define:([A-Za-z0-9_]+)>/);
return match ? match[1] : null;
}
async get_suggested_php_filename(file_path, class_name) {
// rsx/ files use lowercase convention
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
const short_name = this.extract_short_name(class_name, relative_dir);
if (short_name) {
return short_name.toLowerCase() + '.php';
}
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/);
console.log('[AutoRename] JS - Is Jqhtml_Component:', is_jqhtml);
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
console.log('[AutoRename] JS - Relative directory:', relative_dir);
if (is_jqhtml) {
// For Jqhtml components, use snake_case convention
const snake_case = this.pascal_to_snake_case(class_name);
console.log('[AutoRename] JS - PascalCase to snake_case:', class_name, '→', snake_case);
const short_name = this.extract_short_name(class_name, relative_dir);
console.log('[AutoRename] JS - Short name extracted:', short_name);
if (short_name) {
const short_snake = this.pascal_to_snake_case(short_name);
console.log('[AutoRename] JS - Short name to snake_case:', short_name, '→', short_snake);
return short_snake.toLowerCase() + '.js';
}
return snake_case.toLowerCase() + '.js';
}
else {
// Regular JS classes use lowercase
const short_name = this.extract_short_name(class_name, relative_dir);
console.log('[AutoRename] JS - Short name extracted:', short_name);
if (short_name) {
return short_name.toLowerCase() + '.js';
}
return class_name.toLowerCase() + '.js';
}
}
async get_suggested_blade_filename(file_path, rsx_id) {
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
const short_name = this.extract_short_name(rsx_id, relative_dir);
if (short_name) {
return short_name.toLowerCase() + '.blade.php';
}
return rsx_id.toLowerCase() + '.blade.php';
}
async get_suggested_jqhtml_filename(file_path, component_name) {
// Jqhtml components use snake_case convention
const snake_case = this.pascal_to_snake_case(component_name);
console.log('[AutoRename] JQHTML - PascalCase to snake_case:', component_name, '→', snake_case);
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
console.log('[AutoRename] JQHTML - Relative directory:', relative_dir);
const short_name = this.extract_short_name(component_name, relative_dir);
console.log('[AutoRename] JQHTML - Short name extracted:', short_name);
if (short_name) {
const short_snake = this.pascal_to_snake_case(short_name);
console.log('[AutoRename] JQHTML - Short name to snake_case:', short_name, '→', short_snake);
return short_snake.toLowerCase() + '.jqhtml';
}
return snake_case.toLowerCase() + '.jqhtml';
}
/**
* Convert PascalCase to snake_case
* Inserts underscores before uppercase letters and before first digit in number sequences
* Example: TestComponent1 -> Test_Component_1
*/
pascal_to_snake_case(name) {
// Insert underscore before uppercase letters (except first character)
let result = name.replace(/(?<!^)([A-Z])/g, '_$1');
// Insert underscore before first digit in a run of digits
result = result.replace(/(?<!^)(?<![0-9])([0-9])/g, '_$1');
// Replace multiple consecutive underscores with single underscore
result = result.replace(/_+/g, '_');
return result;
}
/**
* Extract short name from class/id if directory structure matches prefix
* Returns null if directory structure doesn't match or if short name rules aren't met
*
* Rules:
* - Short names only allowed in ./rsx directory (NOT in /app/RSpade)
* - Original name must have 3+ segments for short name to be allowed
* - Short name must have 2+ segments
*/
extract_short_name(full_name, dir_path) {
// Short names only allowed in ./rsx directory, not in framework code (/app/RSpade)
if (dir_path.includes('/app/RSpade') || dir_path.includes('\\app\\RSpade')) {
return null;
}
// Split the full name by underscores
const name_parts = full_name.split('_');
const original_segment_count = name_parts.length;
// If original name has exactly 2 segments, short name is NOT allowed
if (original_segment_count === 2) {
return null;
}
// If only 1 segment, no prefix to match
if (original_segment_count === 1) {
return null;
}
// Split directory path into parts (handle both / and \ separators)
const dir_parts = dir_path.split(/[/\\]/).filter(p => p.length > 0);
// Find the maximum number of consecutive matches between end of dir_parts and start of name_parts
let matched_parts = 0;
const max_possible = Math.min(dir_parts.length, name_parts.length - 1);
// Try to match last N dir parts with first N name parts
for (let num_to_check = max_possible; num_to_check > 0; num_to_check--) {
let all_match = true;
for (let i = 0; i < num_to_check; i++) {
const dir_idx = dir_parts.length - num_to_check + i;
if (dir_parts[dir_idx].toLowerCase() !== name_parts[i].toLowerCase()) {
all_match = false;
break;
}
}
if (all_match) {
matched_parts = num_to_check;
break;
}
}
if (matched_parts === 0) {
return null; // No match
}
// Calculate the short name
const short_parts = name_parts.slice(matched_parts);
const short_segment_count = short_parts.length;
// Validate short name segment count
// Short name must have 2+ segments
if (short_segment_count < 2) {
return null; // Short name would be too short
}
return short_parts.join('_');
}
dispose() {
// Cleanup if needed
}
}
exports.AutoRenameProvider = AutoRenameProvider;
//# sourceMappingURL=auto_rename_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,71 @@
"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.init_blade_language_config = void 0;
const vscode = __importStar(require("vscode"));
const init_blade_language_config = () => {
// HTML empty elements that don't require closing tags
const EMPTY_ELEMENTS = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr',
];
// Configure Blade language indentation and auto-closing behavior
vscode.languages.setLanguageConfiguration('blade', {
indentationRules: {
increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/,
},
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
onEnterRules: [
{
// When pressing Enter between opening and closing tags, auto-indent
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: vscode.IndentAction.IndentOutdent },
},
{
// When pressing Enter after opening tag, auto-indent
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
action: { indentAction: vscode.IndentAction.Indent },
},
],
});
};
exports.init_blade_language_config = init_blade_language_config;
//# sourceMappingURL=blade_client.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"blade_client.js","sourceRoot":"","sources":["../src/blade_client.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AAE1B,MAAM,0BAA0B,GAAG,GAAG,EAAE;IAC3C,sDAAsD;IACtD,MAAM,cAAc,GAAa;QAC7B,MAAM;QACN,MAAM;QACN,IAAI;QACJ,KAAK;QACL,OAAO;QACP,IAAI;QACJ,KAAK;QACL,OAAO;QACP,QAAQ;QACR,MAAM;QACN,UAAU;QACV,MAAM;QACN,OAAO;QACP,QAAQ;QACR,OAAO;QACP,KAAK;KACR,CAAC;IAEF,iEAAiE;IACjE,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAAC,OAAO,EAAE;QAC/C,gBAAgB,EAAE;YACd,qBAAqB,EACjB,wJAAwJ;YAC5J,qBAAqB,EACjB,kDAAkD;SACzD;QACD,WAAW,EACP,gFAAgF;QACpF,YAAY,EAAE;YACV;gBACI,oEAAoE;gBACpE,UAAU,EAAE,IAAI,MAAM,CAClB,UAAU,cAAc,CAAC,IAAI,CACzB,GAAG,CACN,8CAA8C,EAC/C,GAAG,CACN;gBACD,SAAS,EAAE,+BAA+B;gBAC1C,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE;aAC9D;YACD;gBACI,qDAAqD;gBACrD,UAAU,EAAE,IAAI,MAAM,CAClB,UAAU,cAAc,CAAC,IAAI,CACzB,GAAG,CACN,sCAAsC,EACvC,GAAG,CACN;gBACD,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE;aACvD;SACJ;KACJ,CAAC,CAAC;AACP,CAAC,CAAC;AAvDW,QAAA,0BAA0B,8BAuDrC"}

View File

@@ -0,0 +1,81 @@
"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.BladeComponentSemanticTokensProvider = void 0;
const vscode = __importStar(require("vscode"));
/**
* Provides semantic tokens for uppercase component tags in Blade files
* Highlights component tag names in light green
* Highlights tag="" attribute in orange on jqhtml components
*/
class BladeComponentSemanticTokensProvider {
async provideDocumentSemanticTokens(document) {
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'blade') {
return tokens_builder.build();
}
const text = document.getText();
// Match opening tags that start with uppercase letter to find jqhtml components
// Matches: <ComponentName ...>, captures the entire tag up to >
const component_tag_regex = /<([A-Z][a-zA-Z0-9_]*)([^>]*?)>/g;
let component_match;
while ((component_match = component_tag_regex.exec(text)) !== null) {
const tag_name = component_match[1];
const tag_attributes = component_match[2];
const tag_start = component_match.index + component_match[0].indexOf(tag_name);
const tag_position = document.positionAt(tag_start);
// Push token for the component tag name
// Token type 0 maps to 'class' which VS Code themes style as entity.name.class (turquoise/cyan)
tokens_builder.push(tag_position.line, tag_position.character, tag_name.length, 0, 0);
// Now look for tag="" attribute within this component's attributes
// Matches: tag="..." or tag='...'
const tag_attr_regex = /\btag\s*=/g;
let attr_match;
while ((attr_match = tag_attr_regex.exec(tag_attributes)) !== null) {
// Calculate the position of 'tag' within the document
const attr_start = component_match.index + component_match[0].indexOf(tag_attributes) + attr_match.index;
const attr_position = document.positionAt(attr_start);
// Push token for 'tag' attribute name
// Token type 1 maps to 'jqhtmlTagAttribute' which we'll define to be orange
tokens_builder.push(attr_position.line, attr_position.character, 3, 1, 0);
}
}
// Also match closing tags that start with uppercase letter
// Matches: </ComponentName>
const closing_tag_regex = /<\/([A-Z][a-zA-Z0-9_]*)/g;
let closing_match;
while ((closing_match = closing_tag_regex.exec(text)) !== null) {
const tag_name = closing_match[1];
const tag_start = closing_match.index + closing_match[0].indexOf(tag_name);
const position = document.positionAt(tag_start);
// Push token for the tag name
// Token type 0 maps to 'class' which VS Code themes style as entity.name.class (turquoise/cyan)
tokens_builder.push(position.line, position.character, tag_name.length, 0, 0);
}
return tokens_builder.build();
}
}
exports.BladeComponentSemanticTokensProvider = BladeComponentSemanticTokensProvider;
//# sourceMappingURL=blade_component_provider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"blade_component_provider.js","sourceRoot":"","sources":["../src/blade_component_provider.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AAEjC;;;;GAIG;AACH,MAAa,oCAAoC;IAC7C,KAAK,CAAC,6BAA6B,CAAC,QAA6B;QAC7D,MAAM,cAAc,GAAG,IAAI,MAAM,CAAC,qBAAqB,EAAE,CAAC;QAE1D,IAAI,QAAQ,CAAC,UAAU,KAAK,OAAO,EAAE;YACjC,OAAO,cAAc,CAAC,KAAK,EAAE,CAAC;SACjC;QAED,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;QAEhC,gFAAgF;QAChF,gEAAgE;QAChE,MAAM,mBAAmB,GAAG,iCAAiC,CAAC;QAC9D,IAAI,eAAe,CAAC;QAEpB,OAAO,CAAC,eAAe,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE;YAChE,MAAM,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;YACpC,MAAM,cAAc,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC/E,MAAM,YAAY,GAAG,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAEpD,wCAAwC;YACxC,gGAAgG;YAChG,cAAc,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAEtF,mEAAmE;YACnE,kCAAkC;YAClC,MAAM,cAAc,GAAG,YAAY,CAAC;YACpC,IAAI,UAAU,CAAC;YAEf,OAAO,CAAC,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,KAAK,IAAI,EAAE;gBAChE,sDAAsD;gBACtD,MAAM,UAAU,GAAG,eAAe,CAAC,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC;gBACzG,MAAM,aAAa,GAAG,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;gBAEtD,sCAAsC;gBACtC,4EAA4E;gBAC5E,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;aAC7E;SACJ;QAED,2DAA2D;QAC3D,4BAA4B;QAC5B,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;QACrD,IAAI,aAAa,CAAC;QAElB,OAAO,CAAC,aAAa,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE;YAC5D,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC3E,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAEhD,8BAA8B;YAC9B,gGAAgG;YAChG,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;SACjF;QAED,OAAO,cAAc,CAAC,KAAK,EAAE,CAAC;IAClC,CAAC;CACJ;AA1DD,oFA0DC"}

View File

@@ -0,0 +1,114 @@
"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.blade_spacer = void 0;
const vscode = __importStar(require("vscode"));
const config_1 = require("./config");
const TAG_DOUBLE = 0;
const TAG_UNESCAPED = 1;
const TAG_COMMENT = 2;
const snippets = {
[TAG_DOUBLE]: '{{ ${1:${TM_SELECTED_TEXT/[{}]//g}} }}$0',
[TAG_UNESCAPED]: '{!! ${1:${TM_SELECTED_TEXT/[{} !]//g}} !!}$0',
[TAG_COMMENT]: '{{-- ${1:${TM_SELECTED_TEXT/(--)|[{} ]//g}} --}}$0',
};
const triggers = ['{}', '!', '-', '{'];
const regexes = [
/({{(?!\s|-))(.*?)(}})/,
/({!!(?!\s))(.*?)?(}?)/,
/({{[\s]?--)(.*?)?(}})/,
];
const translate = (position, offset) => {
try {
return position.translate(0, offset);
}
catch (error) {
// VS Code doesn't like negative numbers passed
// to translate (even though it works fine), so
// this block prevents debug console errors
}
return position;
};
const chars_for_change = (doc, change) => {
if (change.text === '!') {
return 2;
}
if (change.text !== '-') {
return 1;
}
const start = translate(change.range.start, -2);
const end = translate(change.range.start, -1);
return doc.getText(new vscode.Range(start, end)) === ' ' ? 4 : 3;
};
const blade_spacer = async (e, editor) => {
const config = (0, config_1.get_config)();
if (!config.get('enableBladeAutoSpacing', true) ||
!editor ||
editor.document.fileName.indexOf('.blade.php') === -1) {
return;
}
let tag_type = -1;
let ranges = [];
let offsets = [];
// Changes (per line) come in right-to-left when we need them left-to-right
const changes = e.contentChanges.slice().reverse();
changes.forEach((change) => {
if (triggers.indexOf(change.text) === -1) {
return;
}
if (!offsets[change.range.start.line]) {
offsets[change.range.start.line] = 0;
}
const start_offset = offsets[change.range.start.line] -
chars_for_change(e.document, change);
const start = translate(change.range.start, start_offset);
const line_end = e.document.lineAt(start.line).range.end;
for (let i = 0; i < regexes.length; i++) {
// If we typed a - or a !, don't consider the "double" tag type
if (i === TAG_DOUBLE && ['-', '!'].indexOf(change.text) !== -1) {
continue;
}
// Only look at unescaped tags if we need to
if (i === TAG_UNESCAPED && change.text !== '!') {
continue;
}
// Only look at comment tags if we need to
if (i === TAG_COMMENT && change.text !== '-') {
continue;
}
const tag = regexes[i].exec(e.document.getText(new vscode.Range(start, line_end)));
if (tag) {
tag_type = i;
ranges.push(new vscode.Range(start, start.translate(0, tag[0].length)));
offsets[start.line] += tag[1].length;
}
}
});
if (ranges.length > 0 && snippets[tag_type]) {
editor.insertSnippet(new vscode.SnippetString(snippets[tag_type]), ranges);
}
};
exports.blade_spacer = blade_spacer;
//# sourceMappingURL=blade_spacer.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"blade_spacer.js","sourceRoot":"","sources":["../src/blade_spacer.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AACjC,qCAAsC;AAEtC,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB,MAAM,QAAQ,GAA2B;IACrC,CAAC,UAAU,CAAC,EAAE,0CAA0C;IACxD,CAAC,aAAa,CAAC,EAAE,8CAA8C;IAC/D,CAAC,WAAW,CAAC,EAAE,oDAAoD;CACtE,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAEvC,MAAM,OAAO,GAAG;IACZ,uBAAuB;IACvB,uBAAuB;IACvB,uBAAuB;CAC1B,CAAC;AAEF,MAAM,SAAS,GAAG,CAAC,QAAyB,EAAE,MAAc,EAAmB,EAAE;IAC7E,IAAI;QACA,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;KACxC;IAAC,OAAO,KAAK,EAAE;QACZ,+CAA+C;QAC/C,+CAA+C;QAC/C,2CAA2C;KAC9C;IAED,OAAO,QAAQ,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAAG,CACrB,GAAwB,EACxB,MAA6C,EACvC,EAAE;IACR,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG,EAAE;QACrB,OAAO,CAAC,CAAC;KACZ;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG,EAAE;QACrB,OAAO,CAAC,CAAC;KACZ;IAED,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAE9C,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrE,CAAC,CAAC;AAEK,MAAM,YAAY,GAAG,KAAK,EAC7B,CAAiC,EACjC,MAA0B,EAC5B,EAAE;IACA,MAAM,MAAM,GAAG,IAAA,mBAAU,GAAE,CAAC;IAE5B,IACI,CAAC,MAAM,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC;QAC3C,CAAC,MAAM;QACP,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EACvD;QACE,OAAO;KACV;IAED,IAAI,QAAQ,GAAW,CAAC,CAAC,CAAC;IAC1B,IAAI,MAAM,GAAmB,EAAE,CAAC;IAChC,IAAI,OAAO,GAAa,EAAE,CAAC;IAE3B,2EAA2E;IAC3E,MAAM,OAAO,GAAG,CAAC,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC;IAEnD,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;QACvB,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE;YACtC,OAAO;SACV;QAED,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;YACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SACxC;QAED,MAAM,YAAY,GACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC;YAChC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAEzC,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;QAC1D,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;QAEzD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACrC,+DAA+D;YAC/D,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE;gBAC5D,SAAS;aACZ;YAED,4CAA4C;YAC5C,IAAI,CAAC,KAAK,aAAa,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG,EAAE;gBAC5C,SAAS;aACZ;YAED,0CAA0C;YAC1C,IAAI,CAAC,KAAK,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG,EAAE;gBAC1C,SAAS;aACZ;YAED,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CACvB,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CACxD,CAAC;YAEF,IAAI,GAAG,EAAE;gBACL,QAAQ,GAAG,CAAC,CAAC;gBACb,MAAM,CAAC,IAAI,CACP,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAC7D,CAAC;gBACF,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;aACxC;SACJ;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE;QACzC,MAAM,CAAC,aAAa,CAAC,IAAI,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;KAC9E;AACL,CAAC,CAAC;AAtEW,QAAA,YAAY,gBAsEvB"}

View File

@@ -0,0 +1,68 @@
"use strict";
/**
* RSpade Class Refactor Code Actions Provider
*
* Provides refactoring actions that appear in the "Refactor..." menu
* when the cursor is on a class definition line.
*/
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.RspadeClassRefactorCodeActionsProvider = void 0;
const vscode = __importStar(require("vscode"));
class RspadeClassRefactorCodeActionsProvider {
constructor(refactor_provider) {
this.refactor_provider = refactor_provider;
}
provideCodeActions(document, range, context, token) {
// Only provide actions for PHP files in ./rsx or ./app/RSpade
if (document.languageId !== 'php') {
return undefined;
}
const file_path = document.uri.fsPath;
if (!file_path.includes('/rsx/') && !file_path.includes('\\rsx\\') &&
!file_path.includes('/app/RSpade') && !file_path.includes('\\app\\RSpade')) {
return undefined;
}
// Check if line contains a class definition at indent level 0
const position = range.start;
const line = document.lineAt(position.line).text;
// Must be class definition at start of line (indent level 0)
const class_definition_match = line.match(/^(?:abstract\s+|final\s+)?class\s+([A-Z][a-zA-Z0-9_]*)/);
if (class_definition_match) {
return this.create_refactor_actions();
}
return undefined;
}
create_refactor_actions() {
const action = new vscode.CodeAction('Global Rename Class', vscode.CodeActionKind.Refactor);
action.command = {
command: 'rspade.refactorClass',
title: 'Global Rename Class'
};
return [action];
}
}
exports.RspadeClassRefactorCodeActionsProvider = RspadeClassRefactorCodeActionsProvider;
//# sourceMappingURL=class_refactor_code_actions.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"class_refactor_code_actions.js","sourceRoot":"","sources":["../src/class_refactor_code_actions.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,+CAAiC;AAGjC,MAAa,sCAAsC;IAG/C,YAAY,iBAA8C;QACtD,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;IAC/C,CAAC;IAEM,kBAAkB,CACrB,QAA6B,EAC7B,KAAsC,EACtC,OAAiC,EACjC,KAA+B;QAE/B,8DAA8D;QAC9D,IAAI,QAAQ,CAAC,UAAU,KAAK,KAAK,EAAE;YAC/B,OAAO,SAAS,CAAC;SACpB;QAED,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC;YAC9D,CAAC,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE;YAC5E,OAAO,SAAS,CAAC;SACpB;QAED,8DAA8D;QAC9D,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7B,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;QAEjD,6DAA6D;QAC7D,MAAM,sBAAsB,GAAG,IAAI,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;QACpG,IAAI,sBAAsB,EAAE;YACxB,OAAO,IAAI,CAAC,uBAAuB,EAAE,CAAC;SACzC;QAED,OAAO,SAAS,CAAC;IACrB,CAAC;IAEO,uBAAuB;QAC3B,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,UAAU,CAChC,qBAAqB,EACrB,MAAM,CAAC,cAAc,CAAC,QAAQ,CACjC,CAAC;QACF,MAAM,CAAC,OAAO,GAAG;YACb,OAAO,EAAE,sBAAsB;YAC/B,KAAK,EAAE,qBAAqB;SAC/B,CAAC;QACF,OAAO,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;CACJ;AAhDD,wFAgDC"}

View File

@@ -0,0 +1,230 @@
"use strict";
/**
* RSpade Class Refactor Provider
*
* Provides context menu refactoring options for PHP class definitions.
* Communicates with the server-side refactor commands via the IDE service.
*/
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.RspadeClassRefactorProvider = void 0;
const vscode = __importStar(require("vscode"));
class RspadeClassRefactorProvider {
constructor(formatting_provider, auto_rename_provider) {
this.formatting_provider = formatting_provider;
this.auto_rename_provider = auto_rename_provider;
this.output_channel = vscode.window.createOutputChannel('RSpade Refactor');
}
/**
* Register the refactor command
*/
register(context) {
const command = vscode.commands.registerCommand('rspade.refactorClass', async () => await this.refactor_class());
context.subscriptions.push(command);
}
/**
* Main refactor method
*/
async refactor_class() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
const document = editor.document;
const position = editor.selection.active;
this.output_channel.clear();
this.output_channel.show(true);
this.output_channel.appendLine('=== RSpade Class Refactor ===\n');
try {
// Extract class name from cursor position
const class_info = await this.extract_class_info(document, position);
if (!class_info) {
vscode.window.showErrorMessage('Could not identify class at cursor position');
return;
}
this.output_channel.appendLine(`Class: ${class_info.class_name}\n`);
// Show input dialog for new class name
const new_class_name = await vscode.window.showInputBox({
title: `Global Rename Class: ${class_info.class_name}`,
prompt: 'Enter new class name:',
placeHolder: 'New_Class_Name',
value: class_info.class_name,
ignoreFocusOut: true,
validateInput: (value) => {
if (!value) {
return 'Class name cannot be empty';
}
if (!/^[A-Z][a-zA-Z0-9_]*$/.test(value)) {
return 'Class name must be PascalCase (uppercase first letter)';
}
if (value === class_info.class_name) {
return 'New class name must be different from current name';
}
return null;
}
});
if (!new_class_name) {
this.output_channel.appendLine('Refactor cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Confirm refactoring
const confirmation = await vscode.window.showWarningMessage(`Global Rename: ${class_info.class_name}${new_class_name}\n\n` +
'This will rename the class across all usages in all files.', { modal: true }, 'Rename', 'Cancel');
if (confirmation !== 'Rename') {
this.output_channel.appendLine('Global rename cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Save all dirty files first
this.output_channel.appendLine('Checking for unsaved files...');
const dirty_documents = vscode.workspace.textDocuments.filter(doc => doc.isDirty);
if (dirty_documents.length > 0) {
this.output_channel.appendLine(`Found ${dirty_documents.length} unsaved file(s):`);
for (const doc of dirty_documents) {
this.output_channel.appendLine(` - ${doc.fileName}`);
}
this.output_channel.appendLine('\nSaving all files...');
const save_result = await vscode.workspace.saveAll(false);
if (!save_result) {
const error_msg = 'Failed to save all files. Refactor operation aborted.';
this.output_channel.appendLine(`\nERROR: ${error_msg}`);
vscode.window.showErrorMessage(error_msg);
return;
}
this.output_channel.appendLine('All files saved successfully\n');
}
else {
this.output_channel.appendLine('No unsaved files\n');
}
// Show output channel
this.output_channel.show(true);
// Show terminal and execute refactoring
this.output_channel.appendLine(`Refactoring ${class_info.class_name} to ${new_class_name}...`);
this.output_channel.appendLine('');
const result = await this.execute_refactor(class_info.class_name, new_class_name);
// Display result in terminal
this.output_channel.appendLine('\n=== Refactor Output ===\n');
this.output_channel.appendLine(result);
this.output_channel.appendLine('\n=== Refactor Complete ===');
// Check if refactor was successful
if (result.includes('=== Refactor Complete ===') || result.trim().length > 0) {
// Wait 3.5 seconds total (2s + 1.5s), hide panel, then reload files and auto-rename
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 500ms for filesystem changes to propagate (Samba/network issues)
setTimeout(async () => {
await this.reload_all_open_files();
// Wait another 500ms then check if current file needs renaming
setTimeout(async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
const file_path = editor.document.uri.fsPath;
// Only auto-rename if file is in ./rsx
if (file_path.includes('/rsx/') || file_path.includes('\\rsx\\')) {
await this.auto_rename_provider.check_and_rename(editor.document);
}
}
}, 500);
}, 500);
}, 3500);
vscode.window.showInformationMessage(`Successfully refactored ${class_info.class_name} to ${new_class_name}`);
}
}
catch (error) {
const error_message = error.message || String(error);
this.output_channel.appendLine(`\nERROR: ${error_message}`);
vscode.window.showErrorMessage(`Refactor failed: ${error_message}`);
}
}
/**
* Extract class name from cursor position
*/
async extract_class_info(document, position) {
const line = document.lineAt(position.line).text;
// Check for class definition at indent level 0: class ClassName or class ClassName extends Parent
// Must be at start of line (indent level 0)
const class_match = line.match(/^(?:abstract\s+|final\s+)?class\s+([A-Z][a-zA-Z0-9_]*)/);
if (class_match) {
const class_name = class_match[1];
return { class_name };
}
return null;
}
/**
* Reload all open text documents
*/
async reload_all_open_files() {
const text_documents = vscode.workspace.textDocuments;
for (const document of text_documents) {
// Skip untitled documents
if (document.uri.scheme === 'untitled') {
continue;
}
// Skip non-file schemes (git, output channels, etc)
if (document.uri.scheme !== 'file') {
continue;
}
// Get the text editor for this document
const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.toString() === document.uri.toString());
if (editors.length > 0) {
// Document is currently visible, reload it
const position = editors[0].selection.active;
const view_column = editors[0].viewColumn;
// Close and reopen to force reload
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
const new_document = await vscode.workspace.openTextDocument(document.uri);
const editor = await vscode.window.showTextDocument(new_document, view_column);
// Restore cursor position
editor.selection = new vscode.Selection(position, position);
editor.revealRange(new vscode.Range(position, position));
}
}
}
/**
* Execute the refactor command via IDE service
*/
async execute_refactor(old_class, new_class) {
// Ensure authentication
await this.formatting_provider.ensure_auth();
// Prepare request data
const request_data = {
command: 'rsx:refactor:rename_php_class',
arguments: [old_class, new_class, '--skip-rename-file']
};
this.output_channel.appendLine('Sending refactor request to server...');
this.output_channel.appendLine(`Command: ${request_data.command}`);
this.output_channel.appendLine(`Arguments: ${JSON.stringify(request_data.arguments)}\n`);
// Make authenticated request
const response = await this.formatting_provider.make_authenticated_request('/command', request_data);
if (!response.success) {
throw new Error(response.error || 'Refactor command failed');
}
return response.output || 'Refactor completed successfully (no output)';
}
}
exports.RspadeClassRefactorProvider = RspadeClassRefactorProvider;
//# sourceMappingURL=class_refactor_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,41 @@
"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.get_python_command = exports.get_config = void 0;
const vscode = __importStar(require("vscode"));
function get_config() {
return vscode.workspace.getConfiguration('rspade');
}
exports.get_config = get_config;
function get_python_command() {
const custom_path = get_config().get('pythonPath');
if (custom_path && custom_path.trim() !== '') {
return custom_path;
}
// Default based on platform
return process.platform === 'win32' ? 'python' : 'python3';
}
exports.get_python_command = get_python_command;
//# sourceMappingURL=config.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AAEjC,SAAgB,UAAU;IACtB,OAAO,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;AACvD,CAAC;AAFD,gCAEC;AAED,SAAgB,kBAAkB;IAC9B,MAAM,WAAW,GAAG,UAAU,EAAE,CAAC,GAAG,CAAS,YAAY,CAAC,CAAC;IAC3D,IAAI,WAAW,IAAI,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC1C,OAAO,WAAW,CAAC;KACtB;IAED,4BAA4B;IAC5B,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC;AARD,gDAQC"}

View File

@@ -0,0 +1,231 @@
"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.ConventionMethodDefinitionProvider = exports.ConventionMethodDiagnosticProvider = exports.ConventionMethodHoverProvider = exports.ConventionMethodSemanticTokensProvider = void 0;
const vscode = __importStar(require("vscode"));
/**
* Convention methods that are called automatically by the RSX framework
* These methods are invoked by name at runtime, not through direct references
*/
const CONVENTION_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready',
'on_jqhtml_ready'
];
/**
* Check if a method is static by examining the line text
*/
function is_static_method(line_text) {
return line_text.trim().startsWith('static ');
}
/**
* Check if position is inside a comment
*/
function is_in_comment(document, position) {
const line_text = document.lineAt(position.line).text;
const char_pos = position.character;
// Check for single-line comment
const single_comment_idx = line_text.indexOf('//');
if (single_comment_idx !== -1 && single_comment_idx < char_pos) {
return true;
}
// Check for multi-line comment by looking at text before position
const text_before = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
let in_block_comment = false;
let i = 0;
while (i < text_before.length) {
if (text_before.substring(i, i + 2) === '/*') {
in_block_comment = true;
i += 2;
}
else if (text_before.substring(i, i + 2) === '*/') {
in_block_comment = false;
i += 2;
}
else {
i++;
}
}
return in_block_comment;
}
/**
* Provides semantic tokens for convention methods (amber color)
*/
class ConventionMethodSemanticTokensProvider {
async provideDocumentSemanticTokens(document) {
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return tokens_builder.build();
}
const text = document.getText();
// Find all static method definitions matching convention methods
for (const method_name of CONVENTION_METHODS) {
// Match: static method_name(...)
const regex = new RegExp(`\\bstatic\\s+(${method_name})\\s*\\(`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
const method_start = match.index + 'static '.length;
const position = document.positionAt(method_start);
// Skip if this is inside a comment
if (is_in_comment(document, position)) {
continue;
}
tokens_builder.push(position.line, position.character, method_name.length, 0, // token type index for 'conventionMethod'
0 // token modifiers
);
}
}
return tokens_builder.build();
}
}
exports.ConventionMethodSemanticTokensProvider = ConventionMethodSemanticTokensProvider;
/**
* Provides hover information for convention methods
*/
class ConventionMethodHoverProvider {
provideHover(document, position) {
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return undefined;
}
const word = document.getText(word_range);
if (!CONVENTION_METHODS.includes(word)) {
return undefined;
}
// Check if this is a method definition
const line = document.lineAt(position.line).text;
if (!line.includes('(')) {
return undefined;
}
const markdown = new vscode.MarkdownString();
markdown.isTrusted = true;
if (is_static_method(line)) {
markdown.appendMarkdown(`**Convention Method**\n\n`);
markdown.appendMarkdown(`This method is automatically called by \`Rsx.js\` during initialization of the client-side RSpade runtime.\n\n`);
markdown.appendMarkdown(`Convention methods are invoked by name and do not appear as direct references in the codebase.`);
}
else {
markdown.appendMarkdown(`**⚠️ Non-Static Convention Method**\n\n`);
markdown.appendMarkdown(`This method name is reserved for framework convention methods, but it is not declared as \`static\`.\n\n`);
markdown.appendMarkdown(`Convention methods must be \`static\` to be called by the RSX framework.`);
}
return new vscode.Hover(markdown, word_range);
}
}
exports.ConventionMethodHoverProvider = ConventionMethodHoverProvider;
/**
* Provides diagnostics for non-static convention methods
*/
class ConventionMethodDiagnosticProvider {
constructor() {
this.diagnostics_collection = vscode.languages.createDiagnosticCollection('rspade-convention');
}
activate(context) {
// Update diagnostics on document change
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((event) => {
this.update_diagnostics(event.document);
}));
// Update diagnostics for all open documents
vscode.workspace.textDocuments.forEach(document => {
this.update_diagnostics(document);
});
// Update diagnostics when opening a document
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(document => {
this.update_diagnostics(document);
}));
}
update_diagnostics(document) {
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return;
}
const diagnostics = [];
const text = document.getText();
for (const method_name of CONVENTION_METHODS) {
// Match non-static methods with convention names: method_name(...) without 'static' before it
const regex = new RegExp(`^\\s*(?!static\\s)(${method_name})\\s*\\(`, 'gm');
let match;
while ((match = regex.exec(text)) !== null) {
const method_start = match.index + match[0].indexOf(method_name);
const start_pos = document.positionAt(method_start);
const end_pos = document.positionAt(method_start + method_name.length);
const range = new vscode.Range(start_pos, end_pos);
const diagnostic = new vscode.Diagnostic(range, `Convention method '${method_name}' must be declared as 'static' to be called by the RSX framework`, vscode.DiagnosticSeverity.Error);
diagnostic.source = 'RSpade';
diagnostics.push(diagnostic);
}
}
this.diagnostics_collection.set(document.uri, diagnostics);
}
dispose() {
this.diagnostics_collection.dispose();
}
}
exports.ConventionMethodDiagnosticProvider = ConventionMethodDiagnosticProvider;
/**
* Provides go-to-definition for convention methods
*/
class ConventionMethodDefinitionProvider {
async provideDefinition(document, position) {
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return undefined;
}
const word = document.getText(word_range);
if (!CONVENTION_METHODS.includes(word)) {
return undefined;
}
// Check if this is a method definition
const line = document.lineAt(position.line).text;
if (!line.includes('(') || !is_static_method(line)) {
return undefined;
}
// Find Rsx.js in the workspace
const files = await vscode.workspace.findFiles('**/Rsx.js', '**/node_modules/**');
if (files.length === 0) {
return undefined;
}
// Use the first match (should only be one Rsx.js)
const rsx_file = files[0];
const rsx_document = await vscode.workspace.openTextDocument(rsx_file);
const rsx_text = rsx_document.getText();
// Find _rsx_core_boot method
const boot_regex = /static\s+async\s+_rsx_core_boot\s*\(/;
const match = boot_regex.exec(rsx_text);
if (!match) {
return undefined;
}
const boot_position = rsx_document.positionAt(match.index);
return new vscode.Location(rsx_file, boot_position);
}
}
exports.ConventionMethodDefinitionProvider = ConventionMethodDefinitionProvider;
//# sourceMappingURL=convention_method_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,211 @@
"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.DebugClient = void 0;
const vscode = __importStar(require("vscode"));
const crypto = __importStar(require("crypto"));
class DebugClient {
constructor(formattingProvider) {
this.ws = null; // WebSocket instance
this.isConnecting = false;
this.reconnectTimer = null;
this.pingTimer = null;
this.sessionId = null;
this.serverKey = null;
this.formattingProvider = formattingProvider;
this.outputChannel = vscode.window.createOutputChannel('RSPade Debug Proxy');
this.outputChannel.show();
this.log('Debug client initialized');
}
async start() {
this.log('Starting debug client...');
await this.connect();
}
async connect() {
if (this.isConnecting || this.ws?.readyState === WebSocket.OPEN) {
return;
}
this.isConnecting = true;
try {
// Get authentication from formatting provider
await this.ensureAuthenticated();
const serverUrl = await this.formattingProvider.get_server_url();
if (!serverUrl) {
throw new Error('No server URL configured');
}
// Parse URL and construct WebSocket URL
const url = new URL(serverUrl);
const wsProtocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${url.host}/_ide/debug/ws`;
this.log(`Connecting to WebSocket: ${wsUrl}`);
// Create WebSocket (standard API doesn't support headers in constructor)
// We'll send auth after connection
this.ws = new WebSocket(wsUrl);
this.setupEventHandlers();
}
catch (error) {
this.log(`Connection failed: ${error.message}`);
this.isConnecting = false;
this.scheduleReconnect();
}
}
setupEventHandlers() {
if (!this.ws)
return;
this.ws.onopen = () => {
this.isConnecting = false;
this.log('WebSocket connected, sending authentication...');
// Send authentication as first message
const signature = crypto
.createHmac('sha256', this.serverKey)
.update(this.sessionId)
.digest('hex');
this.sendMessage({
type: 'auth',
data: {
sessionId: this.sessionId,
signature: signature
}
});
// Send initial hello message after auth
setTimeout(() => {
this.sendMessage({
type: 'hello',
data: { name: 'VS Code Debug Client' }
});
// Start ping timer
this.startPingTimer();
}, 100);
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
}
catch (error) {
this.log(`Failed to parse message: ${error}`);
}
};
this.ws.onclose = () => {
this.log('WebSocket disconnected');
this.ws = null;
this.stopPingTimer();
this.scheduleReconnect();
};
this.ws.onerror = (error) => {
this.log(`WebSocket error: ${error}`);
};
}
handleMessage(message) {
this.log(`Received: ${message.type}`, message.data);
switch (message.type) {
case 'welcome':
this.log('✅ Authentication successful! Connected to debug proxy');
this.log(`Session ID: ${message.data?.sessionId}`);
break;
case 'pong':
this.log(`PONG received! Server responded to ping`);
break;
case 'hello_response':
this.log(`Server says: ${message.data?.message}`);
break;
case 'error':
this.log(`❌ Error: ${message.data?.message}`);
break;
default:
this.log(`Unknown message type: ${message.type}`);
}
}
sendMessage(message) {
if (this.ws?.readyState === 1) { // 1 = OPEN in standard WebSocket API
this.ws.send(JSON.stringify(message));
this.log(`Sent: ${message.type}`, message.data);
}
}
startPingTimer() {
this.stopPingTimer();
// Send ping every 5 seconds
this.pingTimer = setInterval(() => {
this.sendMessage({
type: 'ping',
data: { timestamp: Date.now() }
});
this.log('PING sent to server');
}, 5000);
}
stopPingTimer() {
if (this.pingTimer) {
clearInterval(this.pingTimer);
this.pingTimer = null;
}
}
scheduleReconnect() {
if (this.reconnectTimer) {
return;
}
this.log('Scheduling reconnection in 5 seconds...');
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, 5000);
}
async ensureAuthenticated() {
// Get auth data from formatting provider
const authData = await this.formattingProvider.ensure_auth();
if (!authData) {
throw new Error('Failed to authenticate');
}
// Extract session ID and server key
this.sessionId = authData.session_id;
this.serverKey = authData.server_key;
if (!this.sessionId || !this.serverKey) {
throw new Error('Invalid auth data');
}
}
log(message, data) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}`;
if (data) {
this.outputChannel.appendLine(`${logMessage}\n${JSON.stringify(data, null, 2)}`);
}
else {
this.outputChannel.appendLine(logMessage);
}
}
dispose() {
this.stopPingTimer();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.outputChannel.dispose();
}
}
exports.DebugClient = DebugClient;
//# sourceMappingURL=debug_client.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,146 @@
"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.RspadeDecorationProvider = void 0;
const vscode = __importStar(require("vscode"));
class RspadeDecorationProvider {
constructor() {
this.decorations = new Map();
// Create decoration type for read-only sections
this.decoration_type = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(100, 100, 100, 0.1)',
borderWidth: '0px',
isWholeLine: true,
overviewRulerColor: 'rgba(100, 100, 100, 0.3)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
before: {
contentText: 'ⓘ ',
color: 'rgba(100, 150, 200, 0.7)',
margin: '0 4px 0 0'
}
});
}
activate(context) {
// RSX markers are no longer used - this functionality is disabled
return;
/* Original implementation preserved for reference
// Update decorations for active editor
if (vscode.window.activeTextEditor) {
this.update_decorations(vscode.window.activeTextEditor);
}
// Update decorations when active editor changes
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) {
this.update_decorations(editor);
}
})
);
// Update decorations when document changes
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument(event => {
const editor = vscode.window.activeTextEditor;
if (editor && event.document === editor.document) {
this.update_decorations(editor);
}
})
);
// Show warning when trying to edit protected region
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument(event => {
if (event.contentChanges.length === 0) return;
const editor = vscode.window.activeTextEditor;
if (!editor || event.document !== editor.document) return;
for (const change of event.contentChanges) {
if (this.is_in_protected_region(event.document, change.range)) {
vscode.window.showWarningMessage(
'You are editing an auto-generated RSX:USE section. These changes may be overwritten.',
'Understood'
);
break;
}
}
})
);
*/
}
update_decorations(editor) {
if (editor.document.languageId !== 'php')
return;
const decorations = [];
const document = editor.document;
let in_protected_region = false;
let start_line = null;
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
const text = line.text;
if (text.includes(RspadeDecorationProvider.RSX_USE_START)) {
in_protected_region = true;
start_line = i;
}
else if (text.includes(RspadeDecorationProvider.RSX_USE_END)) {
if (in_protected_region && start_line !== null) {
// Create decoration for the entire region
const start_pos = new vscode.Position(start_line, 0);
const end_pos = new vscode.Position(i, line.text.length);
const decoration = {
range: new vscode.Range(start_pos, end_pos),
hoverMessage: new vscode.MarkdownString('ⓘ **Deprecated RSX:USE section**\n\n' +
'These markers are no longer used by the RSpade formatter.')
};
decorations.push(decoration);
}
in_protected_region = false;
start_line = null;
}
}
// Apply decorations
editor.setDecorations(this.decoration_type, decorations);
this.decorations.set(editor.document.uri.toString(), decorations);
}
is_in_protected_region(document, range) {
const decorations = this.decorations.get(document.uri.toString()) || [];
for (const decoration of decorations) {
if (decoration.range.contains(range)) {
return true;
}
}
return false;
}
dispose() {
this.decoration_type.dispose();
this.decorations.clear();
}
}
exports.RspadeDecorationProvider = RspadeDecorationProvider;
// RSX markers are no longer used - keeping class for potential future use
RspadeDecorationProvider.RSX_USE_START = '// [RSX:USE:START]'; // Deprecated
RspadeDecorationProvider.RSX_USE_END = '// [RSX:USE:END]'; // Deprecated
//# sourceMappingURL=decoration_provider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"decoration_provider.js","sourceRoot":"","sources":["../src/decoration_provider.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AAEjC,MAAa,wBAAwB;IAQjC;QAFQ,gBAAW,GAAG,IAAI,GAAG,EAAsC,CAAC;QAGhE,gDAAgD;QAChD,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,8BAA8B,CAAC;YAChE,eAAe,EAAE,0BAA0B;YAC3C,WAAW,EAAE,KAAK;YAClB,WAAW,EAAE,IAAI;YACjB,kBAAkB,EAAE,0BAA0B;YAC9C,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,CAAC,IAAI;YAChD,MAAM,EAAE;gBACJ,WAAW,EAAE,IAAI;gBACjB,KAAK,EAAE,0BAA0B;gBACjC,MAAM,EAAE,WAAW;aACtB;SACJ,CAAC,CAAC;IACP,CAAC;IAED,QAAQ,CAAC,OAAgC;QACrC,kEAAkE;QAClE,OAAO;QAEP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4CE;IACN,CAAC;IAEO,kBAAkB,CAAC,MAAyB;QAChD,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,KAAK,KAAK;YAAE,OAAO;QAEjD,MAAM,WAAW,GAA+B,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAEjC,IAAI,mBAAmB,GAAG,KAAK,CAAC;QAChC,IAAI,UAAU,GAAkB,IAAI,CAAC;QAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE;YACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAChC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YAEvB,IAAI,IAAI,CAAC,QAAQ,CAAC,wBAAwB,CAAC,aAAa,CAAC,EAAE;gBACvD,mBAAmB,GAAG,IAAI,CAAC;gBAC3B,UAAU,GAAG,CAAC,CAAC;aAClB;iBAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,wBAAwB,CAAC,WAAW,CAAC,EAAE;gBAC5D,IAAI,mBAAmB,IAAI,UAAU,KAAK,IAAI,EAAE;oBAC5C,0CAA0C;oBAC1C,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;oBACrD,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;oBACzD,MAAM,UAAU,GAA6B;wBACzC,KAAK,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC;wBAC3C,YAAY,EAAE,IAAI,MAAM,CAAC,cAAc,CACnC,sCAAsC;4BACtC,2DAA2D,CAC9D;qBACJ,CAAC;oBACF,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;iBAChC;gBACD,mBAAmB,GAAG,KAAK,CAAC;gBAC5B,UAAU,GAAG,IAAI,CAAC;aACrB;SACJ;QAED,oBAAoB;QACpB,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC;QACzD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,WAAW,CAAC,CAAC;IACtE,CAAC;IAEO,sBAAsB,CAAC,QAA6B,EAAE,KAAmB;QAC7E,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QAExE,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE;YAClC,IAAI,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE;gBAClC,OAAO,IAAI,CAAC;aACf;SACJ;QAED,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,OAAO;QACH,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC;QAC/B,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;;AAlIL,4DAmIC;AAlIG,0EAA0E;AAClD,sCAAa,GAAG,oBAAoB,CAAC,CAAC,aAAa;AACnD,oCAAW,GAAG,kBAAkB,CAAC,CAAC,aAAa"}

View File

@@ -0,0 +1,571 @@
"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 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 app/RSpade/
for (const folder of vscode.workspace.workspaceFolders) {
const app_rspade = path.join(folder.uri.fsPath, '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;
}
// 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 "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
try {
const result = await this.queryIdeHelper(stringContent, undefined, 'view');
return this.createLocationFromResult(result);
}
catch (error) {
console.error('Error querying IDE helper for view:', error);
}
}
}
// 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('/_idehelper', params);
return result;
}
catch (error) {
this.show_error_status('IDE helper request failed');
throw error;
}
}
}
exports.RspadeDefinitionProvider = RspadeDefinitionProvider;
//# sourceMappingURL=definition_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,419 @@
"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.deactivate = exports.activate = void 0;
const vscode = __importStar(require("vscode"));
const folding_provider_1 = require("./folding_provider");
const decoration_provider_1 = require("./decoration_provider");
const file_watcher_1 = require("./file_watcher");
const formatting_provider_1 = require("./formatting_provider");
const definition_provider_1 = require("./definition_provider");
const config_1 = require("./config");
const laravel_completion_provider_1 = require("./laravel_completion_provider");
const blade_spacer_1 = require("./blade_spacer");
const blade_client_1 = require("./blade_client");
const convention_method_provider_1 = require("./convention_method_provider");
const jqhtml_lifecycle_provider_1 = require("./jqhtml_lifecycle_provider");
const php_attribute_provider_1 = require("./php_attribute_provider");
const blade_component_provider_1 = require("./blade_component_provider");
const auto_rename_provider_1 = require("./auto_rename_provider");
const folder_color_provider_1 = require("./folder_color_provider");
const git_status_provider_1 = require("./git_status_provider");
const git_diff_provider_1 = require("./git_diff_provider");
const refactor_provider_1 = require("./refactor_provider");
const refactor_code_actions_1 = require("./refactor_code_actions");
const class_refactor_provider_1 = require("./class_refactor_provider");
const class_refactor_code_actions_1 = require("./class_refactor_code_actions");
const sort_class_methods_provider_1 = require("./sort_class_methods_provider");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
let folding_provider;
let decoration_provider;
let file_watcher;
let formatting_provider;
let definition_provider;
let debug_client;
let laravel_completion_provider;
let auto_rename_provider;
/**
* Check for conflicting PHP extensions and prompt user to disable them
*/
async function check_conflicting_extensions() {
const intelephense = vscode.extensions.getExtension('bmewburn.vscode-intelephense-client');
const php_intellisense = vscode.extensions.getExtension('zobo.php-intellisense');
// Only warn if both Intelephense and PHP IntelliSense are installed
if (intelephense && php_intellisense) {
const action = await vscode.window.showWarningMessage(`Both "Intelephense" and "PHP IntelliSense" are installed. ` +
`It is recommended to disable "PHP IntelliSense" to avoid conflicts.`, 'Disable PHP IntelliSense', 'Ignore');
if (action === 'Disable PHP IntelliSense') {
await vscode.commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [['zobo.php-intellisense']]);
}
}
}
/**
* Find the RSpade project root folder (contains rsx/ and system/app/RSpade/)
* Works in both single-folder and multi-root workspace modes
*/
function find_rspade_root() {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
// Check each workspace folder for rsx/ and system/app/RSpade/ (new structure)
// or app/RSpade/ (legacy structure)
for (const folder of vscode.workspace.workspaceFolders) {
const rsx_dir = path.join(folder.uri.fsPath, 'rsx');
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
// New structure: requires both rsx/ and system/app/RSpade/
if (fs.existsSync(rsx_dir) && fs.existsSync(system_app_rspade)) {
console.log(`[RSpade] Found project root (new structure): ${folder.uri.fsPath}`);
return folder.uri.fsPath;
}
// Legacy structure: just app/RSpade/
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
console.log(`[RSpade] Found project root (legacy structure): ${folder.uri.fsPath}`);
return folder.uri.fsPath;
}
}
return undefined;
}
async function activate(context) {
console.log('RSpade Framework extension is now active');
// Find RSpade project root
const rspade_root = find_rspade_root();
if (!rspade_root) {
console.log('Not an RSpade project (no rsx/ and system/app/RSpade/ found), extension features disabled');
return;
}
console.log(`[RSpade] Project root: ${rspade_root}`);
// Get config scoped to RSpade root for multi-root workspace support
const config = (0, config_1.get_config)();
// Get JQHTML extension API for component navigation
// Try both possible extension IDs
let jqhtml_api = undefined;
const possible_jqhtml_ids = [
'jqhtml.jqhtml-vscode-extension',
'jqhtml.@jqhtml/vscode-extension',
'jqhtml.jqhtml-language'
];
console.log('[RSpade] Searching for JQHTML extension...');
console.log('[RSpade] All installed extensions:', vscode.extensions.all.map(e => e.id).filter(id => id.includes('jqhtml')));
let jqhtml_extension = null;
for (const ext_id of possible_jqhtml_ids) {
console.log(`[RSpade] Trying extension ID: ${ext_id}`);
jqhtml_extension = vscode.extensions.getExtension(ext_id);
if (jqhtml_extension) {
console.log(`[RSpade] JQHTML extension found with ID: ${ext_id}`);
console.log(`[RSpade] Extension isActive: ${jqhtml_extension.isActive}`);
break;
}
else {
console.log(`[RSpade] Extension ID not found: ${ext_id}`);
}
}
if (!jqhtml_extension) {
console.warn('[RSpade] JQHTML extension not found - component navigation in Blade files will be unavailable');
}
else {
try {
console.log('[RSpade] JQHTML extension isActive before activate():', jqhtml_extension.isActive);
console.log('[RSpade] Calling activate() on JQHTML extension...');
// Always call activate() - it returns the API or exports if already active
jqhtml_api = await jqhtml_extension.activate();
console.log('[RSpade] JQHTML extension isActive after activate():', jqhtml_extension.isActive);
console.log('[RSpade] JQHTML extension API loaded successfully');
console.log('[RSpade] API type:', typeof jqhtml_api);
console.log('[RSpade] API value:', jqhtml_api);
console.log('[RSpade] API methods:', Object.keys(jqhtml_api || {}));
console.log('[RSpade] findComponent exists:', typeof (jqhtml_api && jqhtml_api.findComponent));
console.log('[RSpade] getAllComponentNames exists:', typeof (jqhtml_api && jqhtml_api.getAllComponentNames));
console.log('[RSpade] reindexWorkspace exists:', typeof (jqhtml_api && jqhtml_api.reindexWorkspace));
}
catch (error) {
console.warn('[RSpade] JQHTML extension found but API could not be loaded:', error);
}
}
// Initialize providers
folding_provider = new folding_provider_1.RspadeFoldingProvider();
decoration_provider = new decoration_provider_1.RspadeDecorationProvider();
file_watcher = new file_watcher_1.RspadeFileWatcher();
formatting_provider = new formatting_provider_1.RspadeFormattingProvider();
definition_provider = new definition_provider_1.RspadeDefinitionProvider(jqhtml_api);
laravel_completion_provider = new laravel_completion_provider_1.LaravelCompletionProvider();
// Register folder color provider
const folder_color_provider = new folder_color_provider_1.FolderColorProvider();
context.subscriptions.push(vscode.window.registerFileDecorationProvider(folder_color_provider));
// Register git status provider
const git_status_provider = new git_status_provider_1.GitStatusProvider(rspade_root);
context.subscriptions.push(vscode.window.registerFileDecorationProvider(git_status_provider));
// Register git diff provider
const git_diff_provider = new git_diff_provider_1.GitDiffProvider(rspade_root);
git_diff_provider.activate(context);
// Register refactor provider
const refactor_provider = new refactor_provider_1.RspadeRefactorProvider(formatting_provider);
refactor_provider.register(context);
// Register refactor code actions provider
const refactor_code_actions = new refactor_code_actions_1.RspadeRefactorCodeActionsProvider(refactor_provider);
context.subscriptions.push(vscode.languages.registerCodeActionsProvider({ language: 'php' }, refactor_code_actions, {
providedCodeActionKinds: [vscode.CodeActionKind.Refactor]
}));
// Register auto-rename provider early (needed by class refactor provider)
auto_rename_provider = new auto_rename_provider_1.AutoRenameProvider();
auto_rename_provider.activate(context);
console.log('Auto-rename provider registered for rsx/ files');
// Register class refactor provider
const class_refactor_provider = new class_refactor_provider_1.RspadeClassRefactorProvider(formatting_provider, auto_rename_provider);
class_refactor_provider.register(context);
// Register class refactor code actions provider
const class_refactor_code_actions = new class_refactor_code_actions_1.RspadeClassRefactorCodeActionsProvider(class_refactor_provider);
context.subscriptions.push(vscode.languages.registerCodeActionsProvider({ language: 'php' }, class_refactor_code_actions, {
providedCodeActionKinds: [vscode.CodeActionKind.Refactor]
}));
// Register sort class methods provider
const sort_methods_provider = new sort_class_methods_provider_1.RspadeSortClassMethodsProvider(formatting_provider);
sort_methods_provider.register(context);
// Register folding provider
if (config.get('enableCodeFolding', true)) {
context.subscriptions.push(vscode.languages.registerFoldingRangeProvider({ language: 'php' }, folding_provider));
}
// Activate decoration provider
if ((0, config_1.get_config)().get('enableReadOnlyRegions', true)) {
decoration_provider.activate(context);
}
// Activate file watcher
if ((0, config_1.get_config)().get('enableFormatOnMove', true)) {
file_watcher.activate(context);
}
// Register formatting provider
context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider({ language: 'php' }, formatting_provider));
console.log('RSpade formatter registered for PHP files');
// Initialize Blade language configuration (indentation, auto-closing)
(0, blade_client_1.init_blade_language_config)();
console.log('Blade language configuration initialized');
// Register Blade auto-spacing on text change
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((event) => {
(0, blade_spacer_1.blade_spacer)(event, vscode.window.activeTextEditor);
}));
console.log('Blade auto-spacing enabled');
// Register definition provider for JavaScript/TypeScript and PHP/Blade/jqhtml files
context.subscriptions.push(vscode.languages.registerDefinitionProvider([
{ language: 'javascript' },
{ language: 'typescript' },
{ language: 'php' },
{ language: 'blade' },
{ language: 'html' },
{ pattern: '**/*.jqhtml' },
{ pattern: '**/*.blade.php' }
], definition_provider));
console.log('RSpade definition provider registered for JavaScript/TypeScript/PHP/Blade/jqhtml files');
// Register Laravel completion provider for PHP files
context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'php' }, laravel_completion_provider));
console.log('Laravel completion provider registered for PHP files');
// Register convention method providers for JavaScript/TypeScript
// Note: Semantic tokens are handled by JqhtmlLifecycleSemanticTokensProvider to avoid duplicate registration
const convention_hover_provider = new convention_method_provider_1.ConventionMethodHoverProvider();
const convention_diagnostic_provider = new convention_method_provider_1.ConventionMethodDiagnosticProvider();
const convention_definition_provider = new convention_method_provider_1.ConventionMethodDefinitionProvider();
context.subscriptions.push(vscode.languages.registerHoverProvider([{ language: 'javascript' }, { language: 'typescript' }], convention_hover_provider));
context.subscriptions.push(vscode.languages.registerDefinitionProvider([{ language: 'javascript' }, { language: 'typescript' }], convention_definition_provider));
convention_diagnostic_provider.activate(context);
console.log('Convention method providers registered for JavaScript/TypeScript');
// Register JQHTML lifecycle method providers for JavaScript/TypeScript
const jqhtml_semantic_provider = new jqhtml_lifecycle_provider_1.JqhtmlLifecycleSemanticTokensProvider();
const jqhtml_hover_provider = new jqhtml_lifecycle_provider_1.JqhtmlLifecycleHoverProvider();
const jqhtml_diagnostic_provider = new jqhtml_lifecycle_provider_1.JqhtmlLifecycleDiagnosticProvider();
context.subscriptions.push(vscode.languages.registerDocumentSemanticTokensProvider([{ language: 'javascript' }, { language: 'typescript' }], jqhtml_semantic_provider, new vscode.SemanticTokensLegend(['conventionMethod'])));
context.subscriptions.push(vscode.languages.registerHoverProvider([{ language: 'javascript' }, { language: 'typescript' }], jqhtml_hover_provider));
jqhtml_diagnostic_provider.activate(context);
console.log('JQHTML lifecycle providers registered for JavaScript/TypeScript');
// Register PHP attribute provider
const php_attribute_provider = new php_attribute_provider_1.PhpAttributeSemanticTokensProvider();
context.subscriptions.push(vscode.languages.registerDocumentSemanticTokensProvider([{ language: 'php' }], php_attribute_provider, new vscode.SemanticTokensLegend(['conventionMethod'])));
console.log('PHP attribute provider registered for PHP files');
// Register Blade component provider for uppercase component tags
const blade_component_provider = new blade_component_provider_1.BladeComponentSemanticTokensProvider();
context.subscriptions.push(vscode.languages.registerDocumentSemanticTokensProvider([{ language: 'blade' }, { pattern: '**/*.blade.php' }], blade_component_provider, new vscode.SemanticTokensLegend(['class', 'jqhtmlTagAttribute'])));
console.log('Blade component provider registered for Blade files');
// Debug client disabled
// debug_client = new DebugClient(formatting_provider as any);
// debug_client.start().catch(error => {
// console.error('Failed to start debug client:', error);
// });
// console.log('RSpade debug client started (WebSocket test)');
// Clear status bar on document save
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(() => {
definition_provider.clear_status_bar();
}));
// Register commands
context.subscriptions.push(vscode.commands.registerCommand('rspade.toggleFolding', () => {
const config = (0, config_1.get_config)();
const current = config.get('enableCodeFolding', true);
config.update('enableCodeFolding', !current, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`RSpade code folding ${!current ? 'enabled' : 'disabled'}`);
}));
context.subscriptions.push(vscode.commands.registerCommand('rspade.formatPhpFile', async () => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.languageId === 'php') {
await vscode.commands.executeCommand('editor.action.formatDocument');
}
}));
context.subscriptions.push(vscode.commands.registerCommand('rspade.updateNamespace', async () => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.languageId === 'php') {
await formatting_provider.update_namespace_only(editor.document);
}
}));
// Override built-in copyRelativePath commands to use project root
const copy_relative_path_handler = async (uri) => {
const rspade_root = find_rspade_root();
if (!rspade_root) {
vscode.window.showErrorMessage('Could not find RSpade project root');
return;
}
// Get URI from context menu click or active editor
const file_uri = uri || vscode.window.activeTextEditor?.document.uri;
if (!file_uri) {
return;
}
// Get path relative to project root
const relative_path = path.relative(rspade_root, file_uri.fsPath);
// Copy to clipboard
await vscode.env.clipboard.writeText(relative_path);
vscode.window.showInformationMessage(`Copied: ${relative_path}`);
};
// Register our custom command
context.subscriptions.push(vscode.commands.registerCommand('rspade.copyRelativePathFromRoot', copy_relative_path_handler));
// Override built-in commands
context.subscriptions.push(vscode.commands.registerCommand('copyRelativePath', copy_relative_path_handler));
context.subscriptions.push(vscode.commands.registerCommand('copyRelativeFilePath', copy_relative_path_handler));
// Watch for configuration changes
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('rspade')) {
vscode.window.showInformationMessage('RSpade configuration changed. Restart VS Code for some changes to take effect.');
}
}));
// Watch for extension update marker file
watch_for_self_update(context);
// Watch for terminal close marker file
watch_for_terminal_close(context);
}
exports.activate = activate;
function watch_for_self_update(context) {
// Check for update marker file every 2 seconds
const rspade_root = find_rspade_root();
if (!rspade_root) {
return;
}
const marker_file = path.join(rspade_root, '.vscode', '.rspade-extension-updated');
const check_interval = setInterval(() => {
if (fs.existsSync(marker_file)) {
console.log('[RSpade] Extension update marker detected, reloading window in 2 seconds...');
// Clear the interval immediately
clearInterval(check_interval);
// Wait 2 seconds before reloading to allow other VS Code instances to see the marker
setTimeout(async () => {
// Try to delete the marker file (may already be deleted by another instance)
try {
if (fs.existsSync(marker_file)) {
fs.unlinkSync(marker_file);
console.log('[RSpade] Deleted marker file');
}
}
catch (error) {
console.error('[RSpade] Failed to delete marker file:', error);
}
// Close the terminal panel
console.log('[RSpade] Closing terminal panel');
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 200ms for panel to close
await new Promise(resolve => setTimeout(resolve, 200));
// Check for conflicting extensions after panel closes
await check_conflicting_extensions();
// Auto-reload VS Code
console.log('[RSpade] Reloading window now');
vscode.commands.executeCommand('workbench.action.reloadWindow');
}, 2000);
}
}, 2000); // Check every 2 seconds
// Clean up interval on deactivate
context.subscriptions.push({
dispose: () => clearInterval(check_interval)
});
}
function watch_for_terminal_close(context) {
// Check for terminal close marker file every second
const rspade_root = find_rspade_root();
if (!rspade_root) {
return;
}
const marker_file = path.join(rspade_root, '.vscode', '.rspade-close-terminal');
const check_interval = setInterval(() => {
if (fs.existsSync(marker_file)) {
console.log('[RSpade] Terminal close marker detected, hiding panel in 2 seconds...');
// Clear the interval immediately
clearInterval(check_interval);
// Wait 2 seconds to allow other VS Code instances to see the marker
setTimeout(async () => {
// Try to delete the marker file (may already be deleted by another instance)
try {
if (fs.existsSync(marker_file)) {
fs.unlinkSync(marker_file);
console.log('[RSpade] Deleted terminal close marker');
}
}
catch (error) {
console.error('[RSpade] Failed to delete terminal close marker:', error);
}
// Close all terminals
console.log('[RSpade] Closing all terminals');
vscode.window.terminals.forEach(terminal => terminal.dispose());
// Close the terminal panel
console.log('[RSpade] Closing terminal panel');
await vscode.commands.executeCommand('workbench.action.closePanel');
}, 2000);
}
}, 1000); // Check every second
// Clean up interval on deactivate
context.subscriptions.push({
dispose: () => clearInterval(check_interval)
});
}
function deactivate() {
// Cleanup
if (decoration_provider) {
decoration_provider.dispose();
}
if (file_watcher) {
file_watcher.dispose();
}
if (auto_rename_provider) {
auto_rename_provider.dispose();
}
// if (debug_client) {
// debug_client.dispose();
// }
}
exports.deactivate = deactivate;
//# sourceMappingURL=extension.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,117 @@
"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.RspadeFileWatcher = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const util_1 = require("util");
const config_1 = require("./config");
const exec_async = (0, util_1.promisify)(child_process_1.exec);
class RspadeFileWatcher {
constructor() {
this.disposables = [];
}
activate(context) {
// Watch for file rename/move events
const watcher = vscode.workspace.createFileSystemWatcher('**/*.php');
// Track file renames
const file_tracker = new Map();
// Before delete, store the content
watcher.onDidDelete(uri => {
if (this.is_php_file_in_rsx(uri)) {
// Store file path for potential rename detection
file_tracker.set(uri.fsPath, uri.fsPath);
// Clean up old entries after 1 second
setTimeout(() => {
file_tracker.delete(uri.fsPath);
}, 1000);
}
});
// On create, check if it's a rename
watcher.onDidCreate(async (uri) => {
if (this.is_php_file_in_rsx(uri)) {
// Check if this might be a rename
let old_path;
// Look for recently deleted files
for (const [deleted_path, _] of file_tracker) {
// If the file was deleted within the last second, it might be a rename
if (path.basename(deleted_path) !== path.basename(uri.fsPath)) {
old_path = deleted_path;
break;
}
}
// Format the file to update namespace
await this.format_php_file(uri.fsPath);
if (old_path) {
// Silent update - no notification needed
console.log(`Updated namespace for moved file ${path.basename(uri.fsPath)}`);
file_tracker.delete(old_path);
}
}
});
this.disposables.push(watcher);
context.subscriptions.push(...this.disposables);
// Also handle explicit rename commands
context.subscriptions.push(vscode.workspace.onDidRenameFiles(async (e) => {
for (const file of e.files) {
if (this.is_php_file_in_rsx(file.newUri)) {
await this.format_php_file(file.newUri.fsPath);
// Silent update - no notification needed
console.log(`Updated namespace for renamed file ${path.basename(file.newUri.fsPath)}`);
}
}
}));
}
is_php_file_in_rsx(uri) {
const file_path = uri.fsPath;
return file_path.endsWith('.php') &&
(file_path.includes(path.sep + 'rsx' + path.sep) ||
file_path.includes('/rsx/'));
}
async format_php_file(file_path) {
try {
const workspace_folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(file_path));
if (!workspace_folder)
return;
const workspace_root = workspace_folder.uri.fsPath;
const orchestrator_path = path.join(workspace_root, '.vscode', 'formatters', 'orchestrator.py');
const python_cmd = (0, config_1.get_python_command)();
const command = `${python_cmd} "${orchestrator_path}" "${file_path}"`;
await exec_async(command, { cwd: workspace_root });
}
catch (error) {
console.error('Failed to format PHP file:', error);
}
}
dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
this.disposables = [];
}
}
exports.RspadeFileWatcher = RspadeFileWatcher;
//# sourceMappingURL=file_watcher.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"file_watcher.js","sourceRoot":"","sources":["../src/file_watcher.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AACjC,2CAA6B;AAC7B,iDAAqC;AACrC,+BAAiC;AACjC,qCAA8C;AAE9C,MAAM,UAAU,GAAG,IAAA,gBAAS,EAAC,oBAAI,CAAC,CAAC;AAEnC,MAAa,iBAAiB;IAA9B;QACY,gBAAW,GAAwB,EAAE,CAAC;IA+FlD,CAAC;IA7FG,QAAQ,CAAC,OAAgC;QACrC,oCAAoC;QACpC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC;QAErE,qBAAqB;QACrB,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;QAE/C,mCAAmC;QACnC,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE;YACtB,IAAI,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,EAAE;gBAC9B,iDAAiD;gBACjD,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;gBAEzC,sCAAsC;gBACtC,UAAU,CAAC,GAAG,EAAE;oBACZ,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACpC,CAAC,EAAE,IAAI,CAAC,CAAC;aACZ;QACL,CAAC,CAAC,CAAC;QAEH,oCAAoC;QACpC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAC,GAAG,EAAC,EAAE;YAC5B,IAAI,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,EAAE;gBAC9B,kCAAkC;gBAClC,IAAI,QAA4B,CAAC;gBAEjC,kCAAkC;gBAClC,KAAK,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,YAAY,EAAE;oBAC1C,uEAAuE;oBACvE,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;wBAC3D,QAAQ,GAAG,YAAY,CAAC;wBACxB,MAAM;qBACT;iBACJ;gBAED,sCAAsC;gBACtC,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAEvC,IAAI,QAAQ,EAAE;oBACV,yCAAyC;oBACzC,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;oBAC7E,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;iBACjC;aACJ;QACL,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QAEhD,uCAAuC;QACvC,OAAO,CAAC,aAAa,CAAC,IAAI,CACtB,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,KAAK,EAAC,CAAC,EAAC,EAAE;YACxC,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE;gBACxB,IAAI,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;oBACtC,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;oBAC/C,yCAAyC;oBACzC,OAAO,CAAC,GAAG,CAAC,sCAAsC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;iBAC1F;aACJ;QACL,CAAC,CAAC,CACL,CAAC;IACN,CAAC;IAEO,kBAAkB,CAAC,GAAe;QACtC,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC;QAC7B,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC1B,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC;gBAC/C,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACzC,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,SAAiB;QAC3C,IAAI;YACA,MAAM,gBAAgB,GAAG,MAAM,CAAC,SAAS,CAAC,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YACzF,IAAI,CAAC,gBAAgB;gBAAE,OAAO;YAE9B,MAAM,cAAc,GAAG,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC;YACnD,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,EAAE,YAAY,EAAE,iBAAiB,CAAC,CAAC;YAEhG,MAAM,UAAU,GAAG,IAAA,2BAAkB,GAAE,CAAC;YACxC,MAAM,OAAO,GAAG,GAAG,UAAU,KAAK,iBAAiB,MAAM,SAAS,GAAG,CAAC;YAEtE,MAAM,UAAU,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,CAAC;SACtD;QAAC,OAAO,KAAK,EAAE;YACZ,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;SACtD;IACL,CAAC;IAED,OAAO;QACH,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,WAAW,EAAE;YACvC,UAAU,CAAC,OAAO,EAAE,CAAC;SACxB;QACD,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;IAC1B,CAAC;CACJ;AAhGD,8CAgGC"}

View File

@@ -0,0 +1,111 @@
"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.FolderColorProvider = void 0;
const vscode = __importStar(require("vscode"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Provides folder coloring for the RSpade framework
*
* Colors:
* - rsx/ - Blue (highlight important directory)
* - system/ - Muted gray
* - app/ - Muted gray (legacy structure)
* - routes/ - Muted gray (legacy structure)
*/
class FolderColorProvider {
constructor() {
this._onDidChangeFileDecorations = new vscode.EventEmitter();
this.onDidChangeFileDecorations = this._onDidChangeFileDecorations.event;
}
/**
* Find the RSpade project root folder (contains rsx/ and 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 rsx/ and system/app/RSpade/ (new structure)
// or app/RSpade/ (legacy structure)
for (const folder of vscode.workspace.workspaceFolders) {
const rsx_dir = path.join(folder.uri.fsPath, 'rsx');
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
// New structure: requires both rsx/ and system/app/RSpade/
if (fs.existsSync(rsx_dir) && fs.existsSync(system_app_rspade)) {
return folder.uri.fsPath;
}
// Legacy structure: just app/RSpade/
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
provideFileDecoration(uri, token) {
if (!vscode.workspace.workspaceFolders) {
console.log('[FolderColor] No workspace folders');
return undefined;
}
const uriPath = uri.fsPath.replace(/\\/g, '/');
// Check if this URI is a workspace folder root (for multi-root workspaces)
const workspaceFolder = vscode.workspace.workspaceFolders.find(folder => folder.uri.fsPath.replace(/\\/g, '/') === uriPath);
if (workspaceFolder) {
const folderName = workspaceFolder.name.toLowerCase();
console.log('[FolderColor] Workspace folder:', folderName);
// Color workspace folders based on name
if (folderName.includes('rsx')) {
return new vscode.FileDecoration(undefined, undefined, new vscode.ThemeColor('charts.blue'));
}
// docs, database, public - no coloring (default)
if (folderName.includes('system')) {
return new vscode.FileDecoration(undefined, undefined, new vscode.ThemeColor('descriptionForeground'));
}
}
// For single-folder workspaces, color top-level directories
const workspaceRoot = this.find_rspade_root();
if (!workspaceRoot) {
return undefined;
}
const relativePath = uriPath.replace(workspaceRoot.replace(/\\/g, '/') + '/', '');
// Only color top-level directories (no subdirectories)
if (relativePath.includes('/')) {
return undefined;
}
// Color blue for rsx
if (relativePath === 'rsx') {
return new vscode.FileDecoration(undefined, undefined, new vscode.ThemeColor('charts.blue'));
}
// Color muted gray for system, app, and routes
if (relativePath === 'system' || relativePath === 'app' || relativePath === 'routes') {
return new vscode.FileDecoration(undefined, undefined, new vscode.ThemeColor('descriptionForeground'));
}
return undefined;
}
}
exports.FolderColorProvider = FolderColorProvider;
//# sourceMappingURL=folder_color_provider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"folder_color_provider.js","sourceRoot":"","sources":["../src/folder_color_provider.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AACjC,uCAAyB;AACzB,2CAA6B;AAE7B;;;;;;;;GAQG;AACH,MAAa,mBAAmB;IAAhC;QACqB,gCAA2B,GACxC,IAAI,MAAM,CAAC,YAAY,EAAyC,CAAC;QAErD,+BAA0B,GACtC,IAAI,CAAC,2BAA2B,CAAC,KAAK,CAAC;IAsG/C,CAAC;IApGG;;;OAGG;IACK,gBAAgB;QACpB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,gBAAgB,EAAE;YACpC,OAAO,SAAS,CAAC;SACpB;QAED,8EAA8E;QAC9E,oCAAoC;QACpC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,SAAS,CAAC,gBAAgB,EAAE;YACpD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YACpD,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;YAElF,2DAA2D;YAC3D,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE;gBAC5D,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;aAC5B;YAED,qCAAqC;YACrC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;YACjE,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;gBAC3B,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;aAC5B;SACJ;QAED,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,qBAAqB,CACjB,GAAe,EACf,KAA+B;QAE/B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,gBAAgB,EAAE;YACpC,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;YAClD,OAAO,SAAS,CAAC;SACpB;QAED,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAE/C,2EAA2E;QAC3E,MAAM,eAAe,GAAG,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAC1D,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,OAAO,CAC9D,CAAC;QAEF,IAAI,eAAe,EAAE;YACjB,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtD,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,UAAU,CAAC,CAAC;YAE3D,wCAAwC;YACxC,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE;gBAC5B,OAAO,IAAI,MAAM,CAAC,cAAc,CAC5B,SAAS,EACT,SAAS,EACT,IAAI,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,CACvC,CAAC;aACL;YACD,iDAAiD;YACjD,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;gBAC/B,OAAO,IAAI,MAAM,CAAC,cAAc,CAC5B,SAAS,EACT,SAAS,EACT,IAAI,MAAM,CAAC,UAAU,CAAC,uBAAuB,CAAC,CACjD,CAAC;aACL;SACJ;QAED,4DAA4D;QAC5D,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9C,IAAI,CAAC,aAAa,EAAE;YAChB,OAAO,SAAS,CAAC;SACpB;QACD,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,EAAE,CAAC,CAAC;QAElF,uDAAuD;QACvD,IAAI,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;YAC5B,OAAO,SAAS,CAAC;SACpB;QAED,qBAAqB;QACrB,IAAI,YAAY,KAAK,KAAK,EAAE;YACxB,OAAO,IAAI,MAAM,CAAC,cAAc,CAC5B,SAAS,EACT,SAAS,EACT,IAAI,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,CACvC,CAAC;SACL;QAED,+CAA+C;QAC/C,IAAI,YAAY,KAAK,QAAQ,IAAI,YAAY,KAAK,KAAK,IAAI,YAAY,KAAK,QAAQ,EAAE;YAClF,OAAO,IAAI,MAAM,CAAC,cAAc,CAC5B,SAAS,EACT,SAAS,EACT,IAAI,MAAM,CAAC,UAAU,CAAC,uBAAuB,CAAC,CACjD,CAAC;SACL;QAED,OAAO,SAAS,CAAC;IACrB,CAAC;CACJ;AA3GD,kDA2GC"}

View File

@@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RspadeFoldingProvider = void 0;
class RspadeFoldingProvider {
provideFoldingRanges(document, _context, _token) {
// RSX markers are no longer used - returning empty array
return [];
/* Original implementation preserved for reference
const folding_ranges: vscode.FoldingRange[] = [];
let start_line: number | null = null;
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
const text = line.text.trim();
if (text.includes(RspadeFoldingProvider.LLMDIRECTIVE_START)) {
start_line = i;
} else if (text.includes(RspadeFoldingProvider.LLMDIRECTIVE_END) && start_line !== null) {
// Create folding range from start to end
folding_ranges.push(new vscode.FoldingRange(
start_line,
i,
vscode.FoldingRangeKind.Region
));
start_line = null;
}
}
return folding_ranges;
*/
}
}
exports.RspadeFoldingProvider = RspadeFoldingProvider;
// RSX markers are no longer used - keeping class for potential future use
RspadeFoldingProvider.LLMDIRECTIVE_START = '// [RSX:LLMDIRECTIVE:START]'; // Deprecated
RspadeFoldingProvider.LLMDIRECTIVE_END = '// [RSX:LLMDIRECTIVE:END]'; // Deprecated
//# sourceMappingURL=folding_provider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"folding_provider.js","sourceRoot":"","sources":["../src/folding_provider.ts"],"names":[],"mappings":";;;AAEA,MAAa,qBAAqB;IAK9B,oBAAoB,CAChB,QAA6B,EAC7B,QAA+B,EAC/B,MAAgC;QAEhC,yDAAyD;QACzD,OAAO,EAAE,CAAC;QAEV;;;;;;;;;;;;;;;;;;;;;;;UAuBE;IACN,CAAC;;AArCL,sDAsCC;AArCG,0EAA0E;AAClD,wCAAkB,GAAG,6BAA6B,CAAC,CAAC,aAAa;AACjE,sCAAgB,GAAG,2BAA2B,CAAC,CAAC,aAAa"}

View File

@@ -0,0 +1,487 @@
"use strict";
/**
* RSpade Formatting Provider
*
* Handles code formatting via remote IDE service endpoints.
* All formatting is performed on the server - no local PHP execution.
*
* Authentication Flow:
* 1. Reads domain from storage/rsx-ide-bridge/domain.txt (auto-discovered)
* 2. Creates session with auth tokens on first use
* 3. Signs all requests with SHA1(body + client_key)
* 4. Validates server responses with SHA1(body + server_key)
*/
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.RspadeFormattingProvider = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const crypto = __importStar(require("crypto"));
const util_1 = require("util");
const read_file = (0, util_1.promisify)(fs.readFile);
const write_file = (0, util_1.promisify)(fs.writeFile);
const exists = (0, util_1.promisify)(fs.exists);
class RspadeFormattingProvider {
constructor() {
this.auth_data = null;
this.server_url = null;
this.output_channel = vscode.window.createOutputChannel('RSpade Formatter');
console.log('[RSpade Formatter] Provider initialized');
this.output_channel.appendLine('=== RSpade Formatter Initialized ===');
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
this.output_channel.appendLine('Ready to format PHP files via remote server');
}
find_rspade_root() {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
// Check each workspace folder for system/app/RSpade/ (new structure) or app/RSpade/ (legacy)
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first (both rsx/ and system/ must exist)
const rsx_dir = path.join(folder.uri.fsPath, 'rsx');
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
if (fs.existsSync(rsx_dir) && fs.existsSync(system_app_rspade)) {
// Return the project root (not system/)
return folder.uri.fsPath;
}
// Fall back to legacy structure
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
async provideDocumentFormattingEdits(document, _options, _token) {
console.log(`[RSpade Formatter] Format request: ${path.basename(document.fileName)}`);
this.output_channel.appendLine(`\n=== FORMAT REQUEST ===`);
this.output_channel.appendLine(`File: ${document.fileName}`);
this.output_channel.appendLine(`Language: ${document.languageId}`);
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
if (document.languageId !== 'php') {
this.output_channel.appendLine('Result: Skipped (not a PHP file)');
return [];
}
try {
// Get the current document text
const original_text = document.getText();
this.output_channel.appendLine(`Original text length: ${original_text.length} chars`);
// Find RSpade project root
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
throw new Error('RSpade project root not found');
}
// Get relative path from project root
let relative_path = path.relative(rspade_root, document.uri.fsPath);
relative_path = relative_path.replace(/\\/g, '/'); // Convert Windows paths to Unix
this.output_channel.appendLine(`Relative path: ${relative_path}`);
this.output_channel.appendLine(`Project root: ${rspade_root}`);
// Format via server
this.output_channel.appendLine('Calling format_via_server...');
const formatted_text = await this.format_via_server(relative_path, original_text);
// If content changed, return a TextEdit to replace entire document
if (formatted_text !== original_text) {
const full_range = new vscode.Range(document.positionAt(0), document.positionAt(original_text.length));
this.output_channel.appendLine(`Result: SUCCESS - Content changed`);
this.output_channel.appendLine(`Formatted text length: ${formatted_text.length} chars`);
return [vscode.TextEdit.replace(full_range, formatted_text)];
}
this.output_channel.appendLine('Result: SUCCESS - Content unchanged');
return [];
}
catch (error) {
console.error('[RSpade Formatter] Format failed:', error.message);
this.output_channel.appendLine(`Result: ERROR`);
this.output_channel.appendLine(`Error message: ${error.message}`);
this.output_channel.appendLine(`Error stack: ${error.stack}`);
vscode.window.showErrorMessage(`RSpade formatting failed: ${error.message || error}`);
return [];
}
}
async update_namespace_only(document) {
this.output_channel.appendLine(`\n=== NAMESPACE UPDATE ===`);
this.output_channel.appendLine(`File: ${document.fileName}`);
this.output_channel.appendLine(`Language: ${document.languageId}`);
if (document.languageId !== 'php') {
this.output_channel.appendLine('Skipped: Not a PHP file');
return;
}
try {
// Save the document first if it has unsaved changes
if (document.isDirty) {
this.output_channel.appendLine('Saving unsaved changes...');
await document.save();
this.output_channel.appendLine('Document saved');
}
// Get relative path
const workspace_folder = vscode.workspace.getWorkspaceFolder(document.uri);
if (!workspace_folder) {
this.output_channel.appendLine('ERROR: No workspace folder found');
throw new Error('No workspace folder found');
}
let relative_path = path.relative(workspace_folder.uri.fsPath, document.uri.fsPath);
relative_path = relative_path.replace(/\\/g, '/');
this.output_channel.appendLine(`Relative path: ${relative_path}`);
// Read current content
const content = await read_file(document.uri.fsPath, 'utf8');
this.output_channel.appendLine(`File content length: ${content.length} chars`);
// Format via server
this.output_channel.appendLine('Calling format_via_server for namespace update...');
await this.format_via_server(relative_path, content);
this.output_channel.appendLine('Namespace update completed successfully');
// Silent update - no notification needed
}
catch (error) {
console.error('[RSpade Formatter] Namespace update failed:', error.message);
this.output_channel.appendLine(`ERROR: ${error.message}`);
this.output_channel.appendLine(`Stack: ${error.stack}`);
vscode.window.showErrorMessage(`RSpade namespace update failed: ${error.message || error}`);
}
}
async format_via_server(relative_path, content) {
this.output_channel.appendLine('\n--- SERVER FORMATTING ---');
this.output_channel.appendLine(`File path: ${relative_path}`);
this.output_channel.appendLine(`Content length: ${content.length} chars`);
// Ensure we have authentication
await this.ensure_auth();
// Find RSpade project root for temp file creation
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
this.output_channel.appendLine('ERROR: RSpade project root not found');
throw new Error('RSpade project root not found');
}
const temp_file_path = relative_path + '.formatting.tmp';
const full_temp_path = path.join(rspade_root, temp_file_path);
this.output_channel.appendLine(`Creating temp file: ${temp_file_path}`);
// Write content to temp file
await write_file(full_temp_path, content, 'utf8');
this.output_channel.appendLine('Temp file written successfully');
try {
// Prepare request data
const request_data = {
file: temp_file_path,
return_content: true
};
this.output_channel.appendLine(`Request data: ${JSON.stringify(request_data)}`);
const response = await this.make_authenticated_request('/format', request_data);
if (!response.success) {
this.output_channel.appendLine(`ERROR: Server formatting failed - ${response.error}`);
throw new Error(response.error || 'Formatting failed');
}
// Clean up temp file
try {
fs.unlinkSync(full_temp_path);
this.output_channel.appendLine('Temp file cleaned up');
}
catch (e) {
this.output_channel.appendLine(`Warning: Failed to clean up temp file - ${e.message}`);
}
const formatted_content = response.content || content;
this.output_channel.appendLine(`Formatted content length: ${formatted_content.length} chars`);
this.output_channel.appendLine(`Content changed: ${formatted_content !== content}`);
if (formatted_content !== content) {
this.output_channel.appendLine(`First 100 chars of original: ${content.substring(0, 100)}`);
this.output_channel.appendLine(`First 100 chars of formatted: ${formatted_content.substring(0, 100)}`);
}
return formatted_content;
}
catch (error) {
this.output_channel.appendLine(`ERROR during formatting: ${error.message}`);
// Clean up temp file on error
try {
fs.unlinkSync(full_temp_path);
this.output_channel.appendLine('Temp file cleaned up after error');
}
catch (e) {
this.output_channel.appendLine(`Warning: Failed to clean up temp file - ${e.message}`);
}
throw error;
}
}
async ensure_auth(force_new = false) {
if (this.auth_data && !force_new) {
console.log('[RSpade Formatter] Reusing session:', this.auth_data.session.substring(0, 8) + '...');
this.output_channel.appendLine(`Using existing auth session: ${this.auth_data.session}`);
return {
session_id: this.auth_data.session,
server_key: this.auth_data.server_key
};
}
console.log('[RSpade Formatter] Creating new auth session');
this.output_channel.appendLine('\n--- AUTHENTICATION ---');
this.output_channel.appendLine('No existing auth session, creating new...');
// Get server URL (force refresh if this is a retry)
await this.get_server_url(force_new);
// Create new auth session
this.output_channel.appendLine(`Creating auth session at: ${this.server_url}/_ide/service/auth/create`);
const response = await this.make_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
console.log('[RSpade Formatter] Auth created:', this.auth_data.session.substring(0, 8) + '...');
this.output_channel.appendLine(`Auth session created successfully!`);
this.output_channel.appendLine(` Session ID: ${this.auth_data.session}`);
this.output_channel.appendLine(` Client Key: ${this.auth_data.client_key.substring(0, 8)}...`);
this.output_channel.appendLine(` Server Key: ${this.auth_data.server_key.substring(0, 8)}...`);
return {
session_id: this.auth_data.session,
server_key: this.auth_data.server_key
};
}
async get_server_url(force_refresh = false) {
// Only re-check if forced (due to connection failure) or no URL cached
if (this.server_url && !force_refresh) {
this.output_channel.appendLine(`Using cached server URL: ${this.server_url}`);
return this.server_url;
}
this.output_channel.appendLine('\n--- SERVER URL DISCOVERY ---');
if (force_refresh) {
this.output_channel.appendLine('Re-checking server URL due to connection failure...');
}
// First check VS Code settings
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get('serverUrl');
this.output_channel.appendLine(`Checking VS Code settings: ${configured_url ? 'found' : 'not found'}`);
if (configured_url) {
this.server_url = configured_url;
console.log('[RSpade Formatter] Using configured URL:', this.server_url);
this.output_channel.appendLine(`Using configured server URL: ${this.server_url}`);
return this.server_url;
}
// Try to auto-discover from file
// Find RSpade project root (works in both single-folder and multi-root workspace)
let rspade_root = null;
if (vscode.workspace.workspaceFolders) {
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
if (fs.existsSync(system_app_rspade)) {
rspade_root = path.join(folder.uri.fsPath, 'system');
break;
}
// Fall back to legacy structure
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
rspade_root = folder.uri.fsPath;
break;
}
}
}
if (!rspade_root) {
this.output_channel.appendLine('ERROR: RSpade project root not found');
throw new Error('RSpade project root not found');
}
const domain_file = path.join(rspade_root, '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');
this.server_url = domain.trim();
console.log('[RSpade Formatter] Auto-discovered:', this.server_url);
this.output_channel.appendLine(`Auto-discovered server URL: ${this.server_url}`);
return this.server_url;
}
// Provide helpful error message
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
this.output_channel.appendLine('ERROR: RSpade Extension cannot connect to server');
this.output_channel.appendLine('════════════════════════════════════════════════════════');
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('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');
this.output_channel.appendLine('3. Enable IDE integration in Laravel config');
this.output_channel.appendLine(' In config/rsx.php, set ide_integration.enabled = true');
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
throw new Error('RSpade: storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
}
async make_authenticated_request(endpoint, data, retry_count = 0) {
this.output_channel.appendLine(`\n--- AUTHENTICATED REQUEST ${retry_count > 0 ? '(RETRY)' : ''} ---`);
this.output_channel.appendLine(`Endpoint: ${endpoint}`);
// If no auth data or this is a retry, create new session
if (!this.auth_data || retry_count > 0) {
if (retry_count === 0) {
this.output_channel.appendLine('No auth session exists, creating new one...');
}
else {
this.output_channel.appendLine('Session lost, creating new session...');
}
try {
await this.ensure_auth();
}
catch (error) {
this.output_channel.appendLine(`ERROR: Failed to create auth session: ${error.message}`);
throw error;
}
}
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data.client_key).digest('hex');
console.log('[RSpade Formatter] Request sig:', signature.substring(0, 8) + '...');
this.output_channel.appendLine(`Body length: ${body.length} bytes`);
this.output_channel.appendLine(`Session: ${this.auth_data.session}`);
this.output_channel.appendLine(`Client key (first 8 chars): ${this.auth_data.client_key.substring(0, 8)}...`);
this.output_channel.appendLine(`Generated signature: ${signature}`);
try {
const response = await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data.session,
'X-Signature': signature
});
return response;
}
catch (error) {
// Check if this is a recoverable error and we haven't retried yet
if (retry_count === 0) {
const error_msg = error.message || '';
// Session expired or not found
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.output_channel.appendLine('Session/signature error, attempting recovery...');
this.auth_data = null;
// Force URL refresh in case server changed
this.server_url = null;
return this.make_authenticated_request(endpoint, data, retry_count + 1);
}
// Connection failure - maybe server URL changed
if (error_msg.includes('ECONNREFUSED') || error_msg.includes('ENOTFOUND') ||
error_msg.includes('ETIMEDOUT') || error_msg.includes('getaddrinfo')) {
this.output_channel.appendLine('Connection failed, refreshing server URL...');
this.auth_data = null;
// Force URL refresh
this.server_url = null;
return this.make_authenticated_request(endpoint, data, retry_count + 1);
}
}
// Not a recoverable error or already retried
throw error;
}
}
async make_request(endpoint, data, method = 'POST', validate_signature = false, extra_headers = {}) {
return new Promise((resolve, reject) => {
this.output_channel.appendLine('\n--- HTTP REQUEST ---');
this.output_channel.appendLine(`Method: ${method}`);
this.output_channel.appendLine(`Validate signature: ${validate_signature}`);
if (!this.server_url) {
this.output_channel.appendLine('ERROR: Server URL not configured');
reject(new Error('Server URL not configured'));
return;
}
const url = new URL(this.server_url);
const is_https = url.protocol === 'https:';
const http_module = is_https ? https : http;
const body = JSON.stringify(data);
const options = {
hostname: url.hostname,
port: url.port || (is_https ? 443 : 80),
path: '/_ide/service' + endpoint,
method: method,
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
...extra_headers
},
timeout: 30000,
rejectUnauthorized: false // Allow self-signed certificates
};
this.output_channel.appendLine(`Full URL: ${is_https ? 'https' : 'http'}://${options.hostname}:${options.port}${options.path}`);
this.output_channel.appendLine(`Request body size: ${Buffer.byteLength(body)} bytes`);
const start_time = Date.now();
const req = http_module.request(options, (res) => {
let response_data = '';
let total_bytes = 0;
this.output_channel.appendLine(`\n--- HTTP RESPONSE ---`);
this.output_channel.appendLine(`Status: ${res.statusCode}`);
res.on('data', (chunk) => {
response_data += chunk;
total_bytes += chunk.length;
});
res.on('end', () => {
const elapsed = Date.now() - start_time;
console.log(`[RSpade Formatter] Response: ${res.statusCode} in ${elapsed}ms (${total_bytes} bytes)`);
this.output_channel.appendLine(`Response time: ${elapsed}ms`);
this.output_channel.appendLine(`Response body size: ${response_data.length} bytes`);
try {
// Validate signature if required
if (validate_signature && this.auth_data) {
const response_signature = res.headers['x-signature'];
this.output_channel.appendLine(`\n--- SIGNATURE VALIDATION ---`);
this.output_channel.appendLine(`Response signature: ${response_signature || 'missing'}`);
if (response_signature) {
const expected_signature = crypto.createHash('sha1')
.update(response_data + this.auth_data.server_key)
.digest('hex');
console.log('[RSpade Formatter] Sig match:', response_signature.substring(0, 8) + '... == ' + expected_signature.substring(0, 8) + '...');
this.output_channel.appendLine(`Server key (first 8 chars): ${this.auth_data.server_key.substring(0, 8)}...`);
this.output_channel.appendLine(`Expected signature: ${expected_signature}`);
this.output_channel.appendLine(`Validation: ${response_signature === expected_signature ? 'PASSED' : 'FAILED'}`);
if (response_signature !== expected_signature) {
console.error('[RSpade Formatter] Signature mismatch!');
this.output_channel.appendLine('ERROR: Invalid server signature');
reject(new Error('Invalid server signature'));
return;
}
}
}
const response = JSON.parse(response_data);
if (res.statusCode !== 200) {
this.output_channel.appendLine(`ERROR: ${response.error || `HTTP ${res.statusCode}`}`);
reject(new Error(response.error || `HTTP ${res.statusCode}`));
}
else {
this.output_channel.appendLine('Result: SUCCESS');
resolve(response);
}
}
catch (e) {
this.output_channel.appendLine(`ERROR: Failed to parse response - ${e.message}`);
reject(new Error('Invalid response from server'));
}
});
});
req.on('error', (error) => {
const elapsed = Date.now() - start_time;
console.error(`[RSpade Formatter] Request failed after ${elapsed}ms:`, error.message);
this.output_channel.appendLine(`\nERROR after ${elapsed}ms: ${error.message}`);
reject(error);
});
req.on('timeout', () => {
this.output_channel.appendLine('\nERROR: Request timed out after 30 seconds');
req.destroy();
reject(new Error('Request timed out'));
});
req.write(body);
req.end();
});
}
}
exports.RspadeFormattingProvider = RspadeFormattingProvider;
//# sourceMappingURL=formatting_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,473 @@
"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.GitDiffProvider = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const http = __importStar(require("http"));
const https = __importStar(require("https"));
const crypto = __importStar(require("crypto"));
const url_1 = require("url");
/**
* Git diff provider - shows line-level change indicators in gutter
*/
class GitDiffProvider {
constructor(rspade_root) {
this.auth_data = null;
this.server_url = null;
this.cached_protocol = null;
this.retry_timer = null;
this.retry_interval = 60000; // 60 seconds
this.domain_file_watcher = null;
// Track git diff state and local modifications per document
this.git_state = new Map();
this.original_content = new Map();
this.rspade_root = rspade_root;
// Watch domain.txt for changes
this.setup_domain_file_watcher();
// Create decoration types for different change types
this.added_decoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
overviewRulerColor: 'rgba(0, 255, 0, 0.6)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
gutterIconPath: this.create_colored_bar('#28a745'),
gutterIconSize: 'contain'
});
this.modified_decoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
overviewRulerColor: 'rgba(33, 150, 243, 0.6)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
gutterIconPath: this.create_colored_bar('#2196f3'),
gutterIconSize: 'contain'
});
this.deleted_decoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
overviewRulerColor: 'rgba(244, 67, 54, 0.6)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
gutterIconPath: this.create_colored_bar('#f44336'),
gutterIconSize: 'contain'
});
}
activate(context) {
// Track document changes for real-time line marking
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(e => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document === e.document) {
this.update_decorations_for_changes(editor);
}
}));
// Refresh git diff on file save
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(async (document) => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document === document) {
await this.fetch_git_diff(editor);
this.original_content.set(document.uri.toString(), document.getText());
this.update_decorations_for_changes(editor);
}
}));
// Refresh on active editor change (only if clean)
context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(async (editor) => {
if (editor) {
const doc_key = editor.document.uri.toString();
// Only fetch git diff if document is clean
if (!editor.document.isDirty) {
await this.fetch_git_diff(editor);
this.original_content.set(doc_key, editor.document.getText());
}
this.update_decorations_for_changes(editor);
}
}));
// Initial refresh for current editor
const editor = vscode.window.activeTextEditor;
if (editor) {
this.fetch_git_diff(editor).then(() => {
this.original_content.set(editor.document.uri.toString(), editor.document.getText());
this.update_decorations_for_changes(editor);
});
}
}
create_colored_bar(color) {
// Create a simple SVG colored bar for the gutter
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="3" height="16"><rect width="3" height="16" fill="${color}"/></svg>`;
const data_uri = 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
return vscode.Uri.parse(data_uri);
}
async fetch_git_diff(editor) {
if (!this.rspade_root) {
return;
}
try {
const relative_path = path.relative(this.rspade_root, editor.document.uri.fsPath).replace(/\\/g, '/');
const response = await this.make_authenticated_request('/git/diff', { file: relative_path });
if (response.success) {
this.git_state.set(editor.document.uri.toString(), response);
}
}
catch (error) {
console.error('[GitDiff] Failed to fetch git diff:', error);
}
}
update_decorations_for_changes(editor) {
const doc_key = editor.document.uri.toString();
const git_diff = this.git_state.get(doc_key);
const original_text = this.original_content.get(doc_key);
if (!git_diff) {
return;
}
// Start with git diff changes
const added_lines = new Set();
const modified_lines = new Set();
const deleted_lines = new Set();
// Add git diff lines
for (const [start, end] of git_diff.added) {
for (let line = start; line <= end; line++) {
added_lines.add(line);
}
}
for (const [start, end] of git_diff.modified) {
for (let line = start; line <= end; line++) {
modified_lines.add(line);
}
}
for (const [start, end] of git_diff.deleted) {
for (let line = start; line <= end; line++) {
deleted_lines.add(line);
}
}
// If document is dirty and we have original content, compute proper diff
if (editor.document.isDirty && original_text) {
const local_changes = this.compute_diff(original_text, editor.document.getText());
// Overlay local changes on top of git changes
for (const line of local_changes.added) {
added_lines.add(line);
}
for (const line of local_changes.modified) {
modified_lines.add(line);
}
for (const line of local_changes.deleted) {
deleted_lines.add(line);
}
}
// Convert to ranges and apply decorations
const added_ranges = Array.from(added_lines).map(line => new vscode.Range(line - 1, 0, line - 1, 0));
const modified_ranges = Array.from(modified_lines).map(line => new vscode.Range(line - 1, 0, line - 1, 0));
const deleted_ranges = Array.from(deleted_lines).map(line => new vscode.Range(line - 1, 0, line - 1, 0));
editor.setDecorations(this.added_decoration, added_ranges);
editor.setDecorations(this.modified_decoration, modified_ranges);
editor.setDecorations(this.deleted_decoration, deleted_ranges);
}
compute_diff(original, current) {
const original_lines = original.split('\n');
const current_lines = current.split('\n');
// Build LCS table for longest common subsequence
const m = original_lines.length;
const n = current_lines.length;
const lcs = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (original_lines[i - 1] === current_lines[j - 1]) {
lcs[i][j] = lcs[i - 1][j - 1] + 1;
}
else {
lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]);
}
}
}
// Backtrack to find which lines changed
const added = [];
const modified = [];
const deleted = [];
let i = m;
let j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && original_lines[i - 1] === current_lines[j - 1]) {
// Line unchanged
i--;
j--;
}
else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
// Line added in current
added.push(j); // 1-indexed
j--;
}
else if (i > 0) {
// Line deleted from original
deleted.push(j + 1); // Mark position where it was deleted (1-indexed)
i--;
}
}
return { added, modified, deleted };
}
async get_server_url() {
if (this.server_url) {
return this.server_url;
}
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get('serverUrl');
if (configured_url) {
this.server_url = await this.negotiate_protocol(configured_url);
return this.server_url;
}
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
if (fs.existsSync(domain_file)) {
const domain = fs.readFileSync(domain_file, 'utf8').trim();
this.server_url = await this.negotiate_protocol(domain);
return this.server_url;
}
// domain.txt doesn't exist yet - schedule retry
this.schedule_retry();
throw new Error('Server URL not configured - waiting for domain.txt');
}
async negotiate_protocol(url_or_hostname) {
// Parse the input to extract hostname
let hostname;
// If it looks like a URL, parse it
if (url_or_hostname.includes('://')) {
const parsed = new url_1.URL(url_or_hostname);
hostname = parsed.hostname;
// If we have a cached protocol from previous successful connection, use it
if (this.cached_protocol) {
console.log(`[GitDiff] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
}
else {
hostname = url_or_hostname;
}
// If we have a cached protocol, use it
if (this.cached_protocol) {
console.log(`[GitDiff] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
// Try HTTPS first
console.log(`[GitDiff] Negotiating protocol for: ${hostname}`);
const https_url = `https://${hostname}`;
if (await this.test_connection(https_url)) {
console.log(`[GitDiff] HTTPS connection successful`);
this.cached_protocol = 'https:';
return https_url;
}
console.log(`[GitDiff] HTTPS failed, trying HTTP...`);
const http_url = `http://${hostname}`;
if (await this.test_connection(http_url)) {
console.log(`[GitDiff] HTTP connection successful`);
this.cached_protocol = 'http:';
return http_url;
}
throw new Error(`Unable to connect to ${hostname} via HTTPS or HTTP`);
}
async test_connection(base_url) {
try {
const parsed_url = new url_1.URL(base_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const test_path = '/_ide/service/auth/create';
return new Promise((resolve) => {
const options = {
hostname: parsed_url.hostname,
port: port,
path: test_path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': '2'
},
timeout: 5000
};
const client = is_https ? https : http;
const req = client.request(options, (res) => {
// 200 OK or 301/302 redirect means server is responding
// We'll handle redirects by just noting protocol works
const success = res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302;
// If we got 301/302, it means we should probably use the other protocol
// But we'll return false to try the other one
if (res.statusCode === 301 || res.statusCode === 302) {
console.log(`[GitDiff] Got redirect ${res.statusCode} from ${base_url}`);
resolve(false);
}
else {
resolve(success);
}
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.write('{}');
req.end();
});
}
catch (error) {
return false;
}
}
setup_domain_file_watcher() {
if (!this.rspade_root) {
return;
}
const domain_file = path.join(this.rspade_root, '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)) {
try {
this.domain_file_watcher = fs.watch(domain_dir, (eventType, filename) => {
if (filename === 'domain.txt' && eventType === 'change') {
console.log('[GitDiff] domain.txt changed, clearing cached URL and protocol');
this.server_url = null;
this.cached_protocol = null;
this.auth_data = null;
// Cancel any pending retries
if (this.retry_timer) {
clearTimeout(this.retry_timer);
this.retry_timer = null;
}
// Refresh git diff for active editor
const editor = vscode.window.activeTextEditor;
if (editor) {
this.fetch_git_diff(editor);
}
}
});
}
catch (error) {
console.error('[GitDiff] Failed to set up domain file watcher:', error);
}
}
}
schedule_retry() {
// Don't schedule multiple retries
if (this.retry_timer) {
return;
}
console.log(`[GitDiff] Scheduling retry in ${this.retry_interval / 1000} seconds...`);
this.retry_timer = setTimeout(() => {
console.log('[GitDiff] Retrying git diff fetch...');
this.retry_timer = null;
const editor = vscode.window.activeTextEditor;
if (editor) {
this.fetch_git_diff(editor);
}
}, this.retry_interval);
}
async ensure_auth() {
if (this.auth_data) {
return;
}
await this.get_server_url();
const response = await this.make_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
}
async make_authenticated_request(endpoint, data) {
if (!this.auth_data) {
await this.ensure_auth();
}
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data.client_key).digest('hex');
try {
const response = await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data.session,
'X-Signature': signature
});
return response;
}
catch (error) {
const error_msg = error.message || '';
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.auth_data = null;
await this.ensure_auth();
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data.client_key).digest('hex');
return await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data.session,
'X-Signature': signature
});
}
throw error;
}
}
make_request(endpoint, data, method, signed, extra_headers = {}) {
return new Promise(async (resolve, reject) => {
const server_url = await this.get_server_url();
const parsed_url = new url_1.URL(server_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const full_path = `/_ide/service${endpoint}`;
const body_str = JSON.stringify(data);
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body_str),
...extra_headers
};
const options = {
hostname: parsed_url.hostname,
port: port,
path: full_path,
method: method,
headers: headers
};
console.log(`[GitDiff] Making request to: ${parsed_url.protocol}//${parsed_url.hostname}:${port}${full_path}`);
console.log(`[GitDiff] Server URL from config/file: ${server_url}`);
const client = is_https ? https : http;
const req = client.request(options, (res) => {
console.log(`[GitDiff] Response status: ${res.statusCode}`);
let response_data = '';
res.on('data', chunk => response_data += chunk);
res.on('end', () => {
console.log(`[GitDiff] Response body: ${response_data.substring(0, 200)}`);
try {
const parsed = JSON.parse(response_data);
resolve(parsed);
}
catch (e) {
console.error(`[GitDiff] JSON parse error. Full response: ${response_data}`);
reject(new Error('Invalid JSON response'));
}
});
});
req.on('error', (err) => {
console.error(`[GitDiff] Request error:`, err);
reject(err);
});
req.write(body_str);
req.end();
});
}
dispose() {
this.added_decoration.dispose();
this.modified_decoration.dispose();
this.deleted_decoration.dispose();
}
}
exports.GitDiffProvider = GitDiffProvider;
//# sourceMappingURL=git_diff_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,387 @@
"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.GitStatusProvider = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const http = __importStar(require("http"));
const https = __importStar(require("https"));
const crypto = __importStar(require("crypto"));
const url_1 = require("url");
/**
* Git status provider that fetches status from IDE service
* Colors files based on git status without using local git
*/
class GitStatusProvider {
constructor(rspade_root) {
this._onDidChangeFileDecorations = new vscode.EventEmitter();
this.onDidChangeFileDecorations = this._onDidChangeFileDecorations.event;
this.git_status = new Map();
this.auth_data = null;
this.server_url = null;
this.cached_protocol = null;
this.retry_timer = null;
this.retry_interval = 60000; // 60 seconds
this.domain_file_watcher = null;
this.rspade_root = rspade_root;
// Initial fetch
this.refresh_git_status();
// Fetch on file save
vscode.workspace.onDidSaveTextDocument(() => {
this.refresh_git_status();
});
// Fetch on window focus
vscode.window.onDidChangeWindowState(e => {
if (e.focused) {
this.refresh_git_status();
}
});
// Watch domain.txt for changes
this.setup_domain_file_watcher();
}
async refresh_git_status() {
if (!this.rspade_root) {
return;
}
try {
const response = await this.make_authenticated_request('/git', {});
if (response.success) {
// Build new status map from response
const new_status = new Map();
for (const file of response.modified || []) {
new_status.set(file, 'modified');
}
for (const file of response.added || []) {
new_status.set(file, 'added');
}
for (const file of response.conflicts || []) {
new_status.set(file, 'conflict');
}
// Collect URIs that need decoration updates
const changed_uris = [];
// First pass: update or remove existing tracked files
for (const [file_path, old_status] of this.git_status.entries()) {
const new_file_status = new_status.get(file_path);
// File changed status or was removed from git status
if (new_file_status !== old_status) {
const file_uri = vscode.Uri.file(path.join(this.rspade_root, file_path));
changed_uris.push(file_uri);
if (new_file_status === undefined) {
// File no longer in git status - remove it
this.git_status.delete(file_path);
}
else {
// File changed status - update it
this.git_status.set(file_path, new_file_status);
}
}
}
// Second pass: add newly tracked files
for (const [file_path, status] of new_status.entries()) {
if (!this.git_status.has(file_path)) {
this.git_status.set(file_path, status);
const file_uri = vscode.Uri.file(path.join(this.rspade_root, file_path));
changed_uris.push(file_uri);
}
}
// Only fire decoration updates for files that actually changed
if (changed_uris.length > 0) {
this._onDidChangeFileDecorations.fire(changed_uris);
}
}
}
catch (error) {
console.error('[GitStatus] Failed to fetch status:', error);
}
}
async get_server_url() {
if (this.server_url) {
return this.server_url;
}
// Check VS Code settings
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get('serverUrl');
if (configured_url) {
this.server_url = await this.negotiate_protocol(configured_url);
return this.server_url;
}
// Try to auto-discover from domain.txt
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
if (fs.existsSync(domain_file)) {
const domain = fs.readFileSync(domain_file, 'utf8').trim();
this.server_url = await this.negotiate_protocol(domain);
return this.server_url;
}
// domain.txt doesn't exist yet - schedule retry
this.schedule_retry();
throw new Error('Server URL not configured - waiting for domain.txt');
}
async negotiate_protocol(url_or_hostname) {
// Parse the input to extract hostname
let hostname;
// If it looks like a URL, parse it
if (url_or_hostname.includes('://')) {
const parsed = new url_1.URL(url_or_hostname);
hostname = parsed.hostname;
// If we have a cached protocol from previous successful connection, use it
if (this.cached_protocol) {
console.log(`[GitStatus] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
}
else {
hostname = url_or_hostname;
}
// If we have a cached protocol, use it
if (this.cached_protocol) {
console.log(`[GitStatus] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
// Try HTTPS first
console.log(`[GitStatus] Negotiating protocol for: ${hostname}`);
const https_url = `https://${hostname}`;
if (await this.test_connection(https_url)) {
console.log(`[GitStatus] HTTPS connection successful`);
this.cached_protocol = 'https:';
return https_url;
}
console.log(`[GitStatus] HTTPS failed, trying HTTP...`);
const http_url = `http://${hostname}`;
if (await this.test_connection(http_url)) {
console.log(`[GitStatus] HTTP connection successful`);
this.cached_protocol = 'http:';
return http_url;
}
throw new Error(`Unable to connect to ${hostname} via HTTPS or HTTP`);
}
async test_connection(base_url) {
try {
const parsed_url = new url_1.URL(base_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const test_path = '/_ide/service/auth/create';
return new Promise((resolve) => {
const options = {
hostname: parsed_url.hostname,
port: port,
path: test_path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': '2'
},
timeout: 5000
};
const client = is_https ? https : http;
const req = client.request(options, (res) => {
// 200 OK or 301/302 redirect means server is responding
// We'll handle redirects by just noting protocol works
const success = res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302;
// If we got 301/302, it means we should probably use the other protocol
// But we'll return false to try the other one
if (res.statusCode === 301 || res.statusCode === 302) {
console.log(`[GitStatus] Got redirect ${res.statusCode} from ${base_url}`);
resolve(false);
}
else {
resolve(success);
}
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.write('{}');
req.end();
});
}
catch (error) {
return false;
}
}
setup_domain_file_watcher() {
if (!this.rspade_root) {
return;
}
const domain_file = path.join(this.rspade_root, '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)) {
try {
this.domain_file_watcher = fs.watch(domain_dir, (eventType, filename) => {
if (filename === 'domain.txt' && eventType === 'change') {
console.log('[GitStatus] domain.txt changed, clearing cached URL and protocol');
this.server_url = null;
this.cached_protocol = null;
this.auth_data = null;
// Cancel any pending retries
if (this.retry_timer) {
clearTimeout(this.retry_timer);
this.retry_timer = null;
}
// Refresh git status with new URL
this.refresh_git_status();
}
});
}
catch (error) {
console.error('[GitStatus] Failed to set up domain file watcher:', error);
}
}
}
schedule_retry() {
// Don't schedule multiple retries
if (this.retry_timer) {
return;
}
console.log(`[GitStatus] Scheduling retry in ${this.retry_interval / 1000} seconds...`);
this.retry_timer = setTimeout(() => {
console.log('[GitStatus] Retrying git status fetch...');
this.retry_timer = null;
this.refresh_git_status();
}, this.retry_interval);
}
async ensure_auth() {
if (this.auth_data) {
return;
}
// Get server URL
await this.get_server_url();
// Create new auth session
const response = await this.make_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
}
async make_authenticated_request(endpoint, data) {
// Ensure we have auth
if (!this.auth_data) {
await this.ensure_auth();
}
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data.client_key).digest('hex');
try {
const response = await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data.session,
'X-Signature': signature
});
return response;
}
catch (error) {
// If session lost, retry once with new auth
const error_msg = error.message || '';
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.auth_data = null;
await this.ensure_auth();
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data.client_key).digest('hex');
return await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data.session,
'X-Signature': signature
});
}
throw error;
}
}
make_request(endpoint, data, method, signed, extra_headers = {}) {
return new Promise(async (resolve, reject) => {
const server_url = await this.get_server_url();
const parsed_url = new url_1.URL(server_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const full_path = `/_ide/service${endpoint}`;
const body_str = JSON.stringify(data);
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body_str),
...extra_headers
};
const options = {
hostname: parsed_url.hostname,
port: port,
path: full_path,
method: method,
headers: headers
};
console.log(`[GitStatus] Making request to: ${parsed_url.protocol}//${parsed_url.hostname}:${port}${full_path}`);
console.log(`[GitStatus] Server URL from config/file: ${server_url}`);
const client = is_https ? https : http;
const req = client.request(options, (res) => {
console.log(`[GitStatus] Response status: ${res.statusCode}`);
let response_data = '';
res.on('data', chunk => response_data += chunk);
res.on('end', () => {
console.log(`[GitStatus] Response body: ${response_data.substring(0, 200)}`);
try {
const parsed = JSON.parse(response_data);
resolve(parsed);
}
catch (e) {
console.error(`[GitStatus] JSON parse error. Full response: ${response_data}`);
reject(new Error('Invalid JSON response'));
}
});
});
req.on('error', (err) => {
console.error(`[GitStatus] Request error:`, err);
reject(err);
});
req.write(body_str);
req.end();
});
}
provideFileDecoration(uri) {
if (!this.rspade_root) {
return undefined;
}
// Get path relative to project root
const relative_path = path.relative(this.rspade_root, uri.fsPath).replace(/\\/g, '/');
const status = this.git_status.get(relative_path);
if (!status) {
return undefined;
}
// Return decoration based on status
if (status === 'conflict') {
const decoration = new vscode.FileDecoration();
decoration.color = new vscode.ThemeColor('charts.red');
return decoration;
}
else if (status === 'modified' || status === 'added') {
const decoration = new vscode.FileDecoration();
decoration.color = new vscode.ThemeColor('gitDecoration.modifiedResourceForeground');
return decoration;
}
return undefined;
}
}
exports.GitStatusProvider = GitStatusProvider;
//# sourceMappingURL=git_status_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,511 @@
"use strict";
/**
* RSpade IDE Bridge Client
*
* 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
* 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
*
* AUTHENTICATION FLOW:
* 1. Client requests session from /_idehelper/auth/create
* 2. Server generates session ID, client_key, server_key
* 3. Client signs requests: SHA1(body + client_key)
* 4. Server validates signature and signs response: SHA1(body + server_key)
* 5. Client validates server response signature
*
* ERROR HANDLING:
* - ALWAYS fails loud with descriptive errors
* - NO silent fallbacks
* - Logs all activity to output channel
* - Shows status bar notifications for user visibility
*
* USAGE FOR NEW IDE HELPER INTEGRATIONS:
*
* ```typescript
* import { IdeBridgeClient } from './ide_bridge_client';
*
* // Create client instance (pass output channel for logging)
* const client = new IdeBridgeClient(output_channel);
*
* // Make request to IDE helper endpoint
* const response = await client.request('/_idehelper/your_endpoint', {
* param1: 'value1',
* param2: 'value2'
* });
*
* // Client handles:
* // - Auto-discovery of server URL
* // - Authentication session creation
* // - Request signing
* // - Response validation
* // - Auto-retry on connection/auth failure
* // - Comprehensive error reporting
* ```
*
* ADDING NEW IDE HELPER ENDPOINTS:
*
* Backend (PHP):
* 1. Add method to /app/RSpade/Ide/Helper/Ide_Helper_Controller.php
* 2. Register route in /routes/web.php:
* Route::get('/_idehelper/your_endpoint', [Ide_Helper_Controller::class, 'your_method']);
* 3. Return JsonResponse with data
*
* Frontend (TypeScript):
* 1. Use IdeBridgeClient.request() to call endpoint
* 2. NO hardcoded URLs
* 3. NO silent error handling
* 4. Let client handle all auth/retry/discovery logic
*/
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.IdeBridgeClient = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const crypto = __importStar(require("crypto"));
const util_1 = require("util");
const read_file = (0, util_1.promisify)(fs.readFile);
const exists = (0, util_1.promisify)(fs.exists);
class IdeBridgeClient {
constructor(output_channel) {
this.auth_data = null;
this.server_url = null;
this.cached_protocol = null;
this.retry_timer = null;
this.retry_interval = 60000; // 60 seconds
this.domain_file_watcher = null;
this.output_channel = output_channel || vscode.window.createOutputChannel('RSpade IDE Bridge');
this.output_channel.appendLine('=== RSpade IDE Bridge Client Initialized ===');
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
// Watch domain.txt for changes
this.setup_domain_file_watcher();
}
/**
* Make authenticated request to IDE helper endpoint
* Handles auto-discovery, authentication, signing, and retry logic
*/
async request(endpoint, data = {}, method = 'GET') {
this.output_channel.appendLine(`\n=== IDE BRIDGE REQUEST ===`);
this.output_channel.appendLine(`Endpoint: ${endpoint}`);
this.output_channel.appendLine(`Method: ${method}`);
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
return this.make_request_with_retry(endpoint, data, method, 0);
}
async make_request_with_retry(endpoint, data, method, retry_count) {
if (retry_count > 0) {
this.output_channel.appendLine(`\n--- RETRY ATTEMPT ${retry_count} ---`);
}
// Ensure we have server URL (force refresh on retry)
await this.discover_server_url(retry_count > 0);
// For authenticated endpoints, ensure we have auth session
const needs_auth = !endpoint.includes('/auth/create');
if (needs_auth) {
await this.ensure_auth(retry_count > 0);
}
try {
return await this.make_http_request(endpoint, data, method, needs_auth);
}
catch (error) {
// 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...');
this.auth_data = null;
return this.make_request_with_retry(endpoint, data, method, retry_count + 1);
}
// Connection failure - server URL might have changed
if (error_msg.includes('ECONNREFUSED') || error_msg.includes('ENOTFOUND') ||
error_msg.includes('ETIMEDOUT') || error_msg.includes('getaddrinfo')) {
this.output_channel.appendLine('Connection failed, refreshing server URL...');
this.auth_data = null;
this.server_url = null;
return this.make_request_with_retry(endpoint, data, method, retry_count + 1);
}
}
// Not recoverable or already retried - fail loud
this.show_error_status('IDE Bridge request failed');
throw error;
}
}
async make_http_request(endpoint, data, method, needs_auth) {
return new Promise((resolve, reject) => {
if (!this.server_url) {
const error = new Error('Server URL not configured');
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
return;
}
const url = new URL(this.server_url);
const is_https = url.protocol === 'https:';
const http_module = is_https ? https : http;
// For GET requests, encode data as query string
let body = '';
let full_path = endpoint;
if (method === 'GET' && Object.keys(data).length > 0) {
const params = new URLSearchParams(data);
full_path += (endpoint.includes('?') ? '&' : '?') + params.toString();
}
else if (method === 'POST') {
body = JSON.stringify(data);
}
const headers = {
'Content-Type': 'application/json'
};
if (body) {
headers['Content-Length'] = Buffer.byteLength(body);
}
// Add authentication headers if needed
if (needs_auth && this.auth_data) {
const signature = crypto.createHash('sha1')
.update(body + this.auth_data.client_key)
.digest('hex');
headers['X-Session'] = this.auth_data.session;
headers['X-Signature'] = signature;
this.output_channel.appendLine(`Session: ${this.auth_data.session}`);
this.output_channel.appendLine(`Signature: ${signature}`);
}
const options = {
hostname: url.hostname,
port: url.port || (is_https ? 443 : 80),
path: full_path,
method: method,
headers: headers,
timeout: 30000,
rejectUnauthorized: false
};
this.output_channel.appendLine(`Full URL: ${is_https ? 'https' : 'http'}://${options.hostname}:${options.port}${options.path}`);
const start_time = Date.now();
const req = http_module.request(options, (res) => {
let response_data = '';
this.output_channel.appendLine(`\n--- HTTP RESPONSE ---`);
this.output_channel.appendLine(`Status: ${res.statusCode}`);
res.on('data', (chunk) => {
response_data += chunk;
});
res.on('end', () => {
const elapsed = Date.now() - start_time;
this.output_channel.appendLine(`Response time: ${elapsed}ms`);
this.output_channel.appendLine(`Response size: ${response_data.length} bytes`);
try {
// Validate server signature if authenticated request
if (needs_auth && this.auth_data) {
const response_signature = res.headers['x-signature'];
this.output_channel.appendLine(`\n--- SIGNATURE VALIDATION ---`);
this.output_channel.appendLine(`Response signature: ${response_signature || 'MISSING'}`);
if (response_signature) {
const expected_signature = crypto.createHash('sha1')
.update(response_data + this.auth_data.server_key)
.digest('hex');
this.output_channel.appendLine(`Expected signature: ${expected_signature}`);
this.output_channel.appendLine(`Validation: ${response_signature === expected_signature ? 'PASSED' : 'FAILED'}`);
if (response_signature !== expected_signature) {
const error = new Error('Invalid server signature');
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
return;
}
}
}
const response = JSON.parse(response_data);
if (res.statusCode !== 200) {
const error = new Error(response.error || `HTTP ${res.statusCode}`);
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
}
else {
this.output_channel.appendLine('Result: SUCCESS');
this.clear_status_bar();
resolve(response);
}
}
catch (e) {
const error = new Error(`Failed to parse response: ${e.message}`);
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
}
});
});
req.on('error', (error) => {
const elapsed = Date.now() - start_time;
this.output_channel.appendLine(`\nERROR after ${elapsed}ms: ${error.message}`);
reject(error);
});
req.on('timeout', () => {
this.output_channel.appendLine('\nERROR: Request timed out after 30 seconds');
req.destroy();
reject(new Error('Request timed out'));
});
if (body) {
req.write(body);
}
req.end();
});
}
async ensure_auth(force_new = false) {
if (this.auth_data && !force_new) {
this.output_channel.appendLine(`Using existing auth session: ${this.auth_data.session}`);
return;
}
this.output_channel.appendLine('\n--- AUTHENTICATION ---');
this.output_channel.appendLine('Creating new auth session...');
// Request new session (this endpoint doesn't require auth)
const response = await this.make_http_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
this.output_channel.appendLine(`✅ Auth session created successfully!`);
this.output_channel.appendLine(` Server: ${this.server_url}`);
this.output_channel.appendLine(` Session ID: ${this.auth_data.session}`);
this.output_channel.appendLine(` Client Key: ${this.auth_data.client_key.substring(0, 8)}...`);
this.output_channel.appendLine(` Server Key: ${this.auth_data.server_key.substring(0, 8)}...`);
}
async discover_server_url(force_refresh = false) {
if (this.server_url && !force_refresh) {
this.output_channel.appendLine(`Using cached server URL: ${this.server_url}`);
return;
}
this.output_channel.appendLine('\n--- SERVER URL DISCOVERY ---');
if (force_refresh) {
this.output_channel.appendLine('Re-checking server URL due to connection failure...');
}
// Check VS Code settings first
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get('serverUrl');
this.output_channel.appendLine(`VS Code setting: ${configured_url ? 'found' : 'not found'}`);
if (configured_url) {
this.server_url = await this.negotiate_protocol(configured_url);
this.output_channel.appendLine(`✅ Using configured server URL: ${this.server_url}`);
return;
}
// Auto-discover from domain file
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
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');
this.output_channel.appendLine(`Checking for domain file: ${domain_file}`);
if (await exists(domain_file)) {
const domain = (await read_file(domain_file, 'utf8')).trim();
this.server_url = await this.negotiate_protocol(domain);
this.output_channel.appendLine(`✅ Auto-discovered server URL from domain.txt: ${this.server_url}`);
return;
}
// 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.');
}
async negotiate_protocol(url_or_hostname) {
// Parse the input to extract hostname
let hostname;
// If it looks like a URL, parse it
if (url_or_hostname.includes('://')) {
const parsed = new URL(url_or_hostname);
hostname = parsed.hostname;
// If we have a cached protocol from previous successful connection, use it
if (this.cached_protocol) {
this.output_channel.appendLine(`Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
}
else {
hostname = url_or_hostname;
}
// If we have a cached protocol, use it
if (this.cached_protocol) {
this.output_channel.appendLine(`Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
// Try HTTPS first
this.output_channel.appendLine(`Negotiating protocol for: ${hostname}`);
const https_url = `https://${hostname}`;
if (await this.test_connection(https_url)) {
this.output_channel.appendLine(`✅ HTTPS connection successful`);
this.cached_protocol = 'https:';
return https_url;
}
this.output_channel.appendLine(`HTTPS failed, trying HTTP...`);
const http_url = `http://${hostname}`;
if (await this.test_connection(http_url)) {
this.output_channel.appendLine(`✅ HTTP connection successful`);
this.cached_protocol = 'http:';
return http_url;
}
throw new Error(`Unable to connect to ${hostname} via HTTPS or HTTP`);
}
async test_connection(base_url) {
try {
const parsed_url = new URL(base_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const test_path = '/_ide/service/auth/create';
return new Promise((resolve) => {
const options = {
hostname: parsed_url.hostname,
port: port,
path: test_path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': '2'
},
timeout: 5000,
rejectUnauthorized: false
};
const client = is_https ? https : http;
const req = client.request(options, (res) => {
// 200 OK means success
// 301/302 redirect means we should try the other protocol
if (res.statusCode === 301 || res.statusCode === 302) {
this.output_channel.appendLine(`Got redirect ${res.statusCode} from ${base_url}`);
resolve(false);
}
else {
resolve(res.statusCode === 200);
}
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.write('{}');
req.end();
});
}
catch (error) {
return false;
}
}
setup_domain_file_watcher() {
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
return;
}
const domain_file = path.join(rspade_root, '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)) {
try {
this.domain_file_watcher = fs.watch(domain_dir, (eventType, filename) => {
if (filename === 'domain.txt' && eventType === 'change') {
this.output_channel.appendLine('[IdeBridge] domain.txt changed, clearing cached URL and protocol');
this.server_url = null;
this.cached_protocol = null;
this.auth_data = null;
// Cancel any pending retries
if (this.retry_timer) {
clearTimeout(this.retry_timer);
this.retry_timer = null;
}
}
});
}
catch (error) {
this.output_channel.appendLine(`Failed to set up domain file watcher: ${error}`);
}
}
}
schedule_retry() {
// Don't schedule multiple retries
if (this.retry_timer) {
return;
}
this.output_channel.appendLine(`Scheduling retry in ${this.retry_interval / 1000} seconds...`);
this.retry_timer = setTimeout(() => {
this.output_channel.appendLine('Retrying server URL discovery...');
this.retry_timer = null;
this.server_url = null;
// Next request will trigger discovery
}, this.retry_interval);
}
find_rspade_root() {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first
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');
}
// Fall back to legacy structure
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
show_error_status(message) {
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';
}
this.status_bar_item.text = `$(error) RSpade: ${message}`;
this.status_bar_item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
this.status_bar_item.show();
setTimeout(() => {
this.clear_status_bar();
}, 5000);
}
clear_status_bar() {
if (this.status_bar_item) {
this.status_bar_item.hide();
}
}
show_detailed_error() {
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
this.output_channel.appendLine('ERROR: RSpade Extension cannot connect to server');
this.output_channel.appendLine('════════════════════════════════════════════════════════');
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('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');
this.output_channel.appendLine('3. Enable IDE integration in Laravel config');
this.output_channel.appendLine(' In config/rsx.php, set ide_integration.enabled = true');
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
}
}
exports.IdeBridgeClient = IdeBridgeClient;
//# sourceMappingURL=ide_bridge_client.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,435 @@
"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.JqhtmlLifecycleDiagnosticProvider = exports.JqhtmlLifecycleHoverProvider = exports.JqhtmlLifecycleSemanticTokensProvider = void 0;
const vscode = __importStar(require("vscode"));
const ide_bridge_client_1 = require("./ide_bridge_client");
/**
* JQHTML lifecycle methods that are called automatically by the framework
*/
const JQHTML_LIFECYCLE_METHODS = ['on_render', 'on_create', 'on_load', 'on_ready', 'on_destroy'];
/**
* Convention methods that are called automatically by the RSX framework
*/
const CONVENTION_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready',
'on_jqhtml_ready'
];
/**
* Lifecycle method documentation
*/
const LIFECYCLE_DOCS = {
on_render: 'Initial render phase - template DOM created. Parent completes before children. DOM manipulation allowed. MUST be synchronous.',
on_create: 'Quick setup after DOM exists. Children complete before parent. DOM manipulation allowed. MUST be synchronous.',
on_load: 'Data loading phase - fetch async data. NO DOM manipulation allowed, only update `this.data`. Template re-renders after load. MUST be async.',
on_ready: 'Final setup phase - all data loaded. Children complete before parent. DOM manipulation allowed. Can be sync or async.',
on_destroy: 'Component destruction phase - cleanup resources. Called before component is removed. MUST be synchronous.',
};
/**
* Cache for lineage lookups
*/
const lineage_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
*/
async function get_js_lineage(class_name) {
// Check cache first
if (lineage_cache.has(class_name)) {
return lineage_cache.get(class_name);
}
// Initialize IDE bridge client if needed
if (!ide_bridge_client) {
const output_channel = vscode.window.createOutputChannel('RSpade JQHTML Lifecycle');
ide_bridge_client = new ide_bridge_client_1.IdeBridgeClient(output_channel);
}
try {
const response = await ide_bridge_client.request('/_idehelper/js_lineage', { class: class_name });
const lineage = response.lineage || [];
// Cache the result
lineage_cache.set(class_name, lineage);
return lineage;
}
catch (error) {
// Re-throw error to fail loud - no silent fallbacks
throw new Error(`Failed to get JS lineage for ${class_name}: ${error.message}`);
}
}
/**
* Extract class name from document text
*/
function extract_class_name(text) {
const regex = /class\s+([A-Za-z0-9_]+)\s+extends/;
const match = regex.exec(text);
return match ? match[1] : null;
}
/**
* Check if class extends Jqhtml_Component
*/
function directly_extends_jqhtml(text) {
const regex = /class\s+[A-Za-z0-9_]+\s+extends\s+Jqhtml_Component/;
return regex.test(text);
}
/**
* Check if class has extends clause
*/
function has_extends_clause(text) {
const regex = /class\s+[A-Za-z0-9_]+\s+extends\s+[A-Za-z0-9_]+/;
return regex.test(text);
}
/**
* Check if a method is async
*/
function is_async_method(line_text) {
return line_text.trim().startsWith('async ');
}
/**
* Check if position is inside a comment
*/
function is_in_comment(document, position) {
const line_text = document.lineAt(position.line).text;
const char_pos = position.character;
// Check for single-line comment
const single_comment_idx = line_text.indexOf('//');
if (single_comment_idx !== -1 && single_comment_idx < char_pos) {
return true;
}
// Check for multi-line comment by looking at text before position
const text_before = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
let in_block_comment = false;
let i = 0;
while (i < text_before.length) {
if (text_before.substring(i, i + 2) === '/*') {
in_block_comment = true;
i += 2;
}
else if (text_before.substring(i, i + 2) === '*/') {
in_block_comment = false;
i += 2;
}
else {
i++;
}
}
return in_block_comment;
}
/**
* Provides semantic tokens for JQHTML lifecycle methods (amber color)
*/
class JqhtmlLifecycleSemanticTokensProvider {
async provideDocumentSemanticTokens(document) {
console.log(`[JQHTML] provideDocumentSemanticTokens called for: ${document.fileName}`);
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
console.log(`[JQHTML] Skipping - not JS/TS, language is: ${document.languageId}`);
return tokens_builder.build();
}
const text = document.getText();
// Quick check: does this file have an extends clause?
if (!has_extends_clause(text)) {
console.log(`[JQHTML] No extends clause found, checking for convention methods only`);
// Still process convention methods even without extends
}
else {
console.log(`[JQHTML] Found extends clause`);
}
// 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
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}`);
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}`);
}
}
// Highlight lifecycle methods (only if extends Jqhtml_Component)
if (extends_jqhtml) {
console.log(`[JQHTML] Scanning for JQHTML lifecycle methods...`);
let lifecycle_count = 0;
for (const method_name of JQHTML_LIFECYCLE_METHODS) {
const regex = new RegExp(`\\b(async\\s+)?(${method_name})\\s*\\(`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
const method_start = match.index + (match[1] ? match[1].length : 0);
const position = document.positionAt(method_start);
// Skip if this is inside a comment
if (is_in_comment(document, position)) {
console.log(`[JQHTML] Skipping ${method_name} - inside comment`);
continue;
}
console.log(`[JQHTML] Found lifecycle method: ${method_name} at line ${position.line}`);
tokens_builder.push(position.line, position.character, method_name.length, 0, 0);
lifecycle_count++;
}
}
console.log(`[JQHTML] Total lifecycle methods highlighted: ${lifecycle_count}`);
}
// Highlight convention methods (for all classes)
console.log(`[JQHTML] Scanning for convention methods...`);
let convention_count = 0;
for (const method_name of CONVENTION_METHODS) {
const regex = new RegExp(`\\b(static\\s+)?(async\\s+)?(${method_name})\\s*\\(`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
const prefix_length = (match[1] ? match[1].length : 0) + (match[2] ? match[2].length : 0);
const method_start = match.index + prefix_length;
const position = document.positionAt(method_start);
// Skip if this is inside a comment
if (is_in_comment(document, position)) {
console.log(`[JQHTML] Skipping ${method_name} - inside comment`);
continue;
}
console.log(`[JQHTML] Found convention method: ${method_name} at line ${position.line}`);
tokens_builder.push(position.line, position.character, method_name.length, 0, 0);
convention_count++;
}
}
console.log(`[JQHTML] Total convention methods highlighted: ${convention_count}`);
// Highlight @Instantiatable in JSDoc comments
console.log(`[JQHTML] Scanning for @Instantiatable annotations...`);
let instantiatable_count = 0;
const instantiatable_regex = /@(Instantiatable)\b/g;
let instantiatable_match;
while ((instantiatable_match = instantiatable_regex.exec(text)) !== null) {
const annotation_start = instantiatable_match.index + 1; // Skip the @ symbol
const position = document.positionAt(annotation_start);
console.log(`[JQHTML] Found @Instantiatable at line ${position.line}`);
tokens_builder.push(position.line, position.character, 'Instantiatable'.length, 0, 0);
instantiatable_count++;
}
console.log(`[JQHTML] Total @Instantiatable annotations highlighted: ${instantiatable_count}`);
const result = tokens_builder.build();
console.log(`[JQHTML] Returning ${result.data.length} semantic tokens`);
return result;
}
}
exports.JqhtmlLifecycleSemanticTokensProvider = JqhtmlLifecycleSemanticTokensProvider;
/**
* Provides hover information for JQHTML lifecycle methods
*/
class JqhtmlLifecycleHoverProvider {
async provideHover(document, position) {
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return undefined;
}
const word = document.getText(word_range);
if (!JQHTML_LIFECYCLE_METHODS.includes(word)) {
return undefined;
}
// Check if this is a method definition
const line = document.lineAt(position.line).text;
if (!line.includes('(')) {
return undefined;
}
// Check if class extends Jqhtml_Component
const text = document.getText();
if (!has_extends_clause(text)) {
return undefined;
}
const is_jqhtml = directly_extends_jqhtml(text);
let extends_jqhtml = is_jqhtml;
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');
}
}
if (!extends_jqhtml) {
return undefined;
}
const markdown = new vscode.MarkdownString();
markdown.isTrusted = true;
const is_async = is_async_method(line);
// Determine if async is required, forbidden, or optional
const must_be_sync = ['on_create', 'on_render', 'on_destroy'].includes(word);
const must_be_async = word === 'on_load';
const can_be_either = word === 'on_ready';
let has_error = false;
if (must_be_sync && is_async) {
markdown.appendMarkdown(`**⚠️ Incorrect Async Declaration**\n\n`);
markdown.appendMarkdown(`\`${word}\` must be synchronous - remove 'async' keyword.\n\n`);
has_error = true;
}
else if (must_be_async && !is_async) {
markdown.appendMarkdown(`**⚠️ Missing Async Declaration**\n\n`);
markdown.appendMarkdown(`\`${word}\` must be async - add 'async' keyword.\n\n`);
has_error = true;
}
if (!has_error) {
markdown.appendMarkdown(`**JQHTML Lifecycle Method**\n\n`);
}
markdown.appendMarkdown(`${LIFECYCLE_DOCS[word]}\n\n`);
markdown.appendMarkdown(`Run \`php artisan rsx:man jqhtml\` for more information.`);
return new vscode.Hover(markdown, word_range);
}
}
exports.JqhtmlLifecycleHoverProvider = JqhtmlLifecycleHoverProvider;
/**
* Diagnostic provider for non-async lifecycle methods
*/
class JqhtmlLifecycleDiagnosticProvider {
constructor() {
this.document_cache = new Map();
this.diagnostics_collection = vscode.languages.createDiagnosticCollection('rspade-jqhtml');
}
activate(context) {
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((event) => {
this.update_diagnostics(event.document);
}));
vscode.workspace.textDocuments.forEach((document) => {
this.update_diagnostics(document);
});
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument((document) => {
this.update_diagnostics(document);
}));
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument((document) => {
// Clear cache on save to force lineage re-check
this.document_cache.delete(document.uri.toString());
this.update_diagnostics(document);
}));
}
async update_diagnostics(document) {
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return;
}
const text = document.getText();
if (!has_extends_clause(text)) {
return;
}
// Check cache
const cache_key = document.uri.toString();
let extends_jqhtml = this.document_cache.get(cache_key);
if (extends_jqhtml === undefined) {
const is_jqhtml = directly_extends_jqhtml(text);
extends_jqhtml = is_jqhtml;
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');
}
}
this.document_cache.set(cache_key, extends_jqhtml);
}
if (!extends_jqhtml) {
return;
}
const diagnostics = [];
const lines = text.split('\n');
// Find each lifecycle method and validate
for (const method_name of JQHTML_LIFECYCLE_METHODS) {
// Find method definition and extract its body
const method_regex = new RegExp(`^\\s*(static\\s+)?(async\\s+)?(${method_name})\\s*\\([^)]*\\)\\s*\\{`, 'gm');
let method_match;
while ((method_match = method_regex.exec(text)) !== null) {
const is_static = !!method_match[1];
const is_async = !!method_match[2];
const method_start_index = method_match.index + method_match[0].indexOf(method_name);
const method_start_pos = document.positionAt(method_start_index);
const method_end_pos = document.positionAt(method_start_index + method_name.length);
const method_name_range = new vscode.Range(method_start_pos, method_end_pos);
// Check if method is static (should not be)
if (is_static) {
diagnostics.push(new vscode.Diagnostic(method_name_range, `JQHTML lifecycle method '${method_name}' should not be static`, vscode.DiagnosticSeverity.Warning));
}
// Check async requirements
if (method_name === 'on_create' && is_async) {
diagnostics.push(new vscode.Diagnostic(method_name_range, `'on_create' must be synchronous - remove 'async' keyword`, vscode.DiagnosticSeverity.Error));
}
else if (method_name === 'on_render' && is_async) {
diagnostics.push(new vscode.Diagnostic(method_name_range, `'on_render' must be synchronous - remove 'async' keyword`, vscode.DiagnosticSeverity.Error));
}
else if (method_name === 'on_destroy' && is_async) {
diagnostics.push(new vscode.Diagnostic(method_name_range, `'on_destroy' must be synchronous - remove 'async' keyword`, vscode.DiagnosticSeverity.Error));
}
else if (method_name === 'on_load' && !is_async) {
diagnostics.push(new vscode.Diagnostic(method_name_range, `'on_load' must be async - add 'async' keyword`, vscode.DiagnosticSeverity.Error));
}
// on_ready can be either async or non-async - no requirement
// Find method body to check for violations
const method_body_start = method_match.index + method_match[0].length;
let brace_count = 1;
let body_end = method_body_start;
for (let i = method_body_start; i < text.length && brace_count > 0; i++) {
if (text[i] === '{')
brace_count++;
if (text[i] === '}')
brace_count--;
if (brace_count === 0) {
body_end = i;
break;
}
}
const method_body = text.substring(method_body_start, body_end);
// Check for violations in method body
if (method_name === 'on_create') {
// Check for this.data or that.data access
const data_access_regex = /(this|that)\.data/g;
let data_match;
while ((data_match = data_access_regex.exec(method_body)) !== null) {
const violation_pos = document.positionAt(method_body_start + data_match.index);
const violation_end = document.positionAt(method_body_start + data_match.index + data_match[0].length);
diagnostics.push(new vscode.Diagnostic(new vscode.Range(violation_pos, violation_end), `'${data_match[0]}' is populated during on_load, which happens after on_create. Did you mean ${data_match[1]}.args?`, vscode.DiagnosticSeverity.Warning));
}
}
if (method_name === 'on_load') {
// Check for DOM access: this.$, this.$id, that.$, that.$id
const dom_access_regex = /(this|that)\.\$(?:id)?/g;
let dom_match;
while ((dom_match = dom_access_regex.exec(method_body)) !== null) {
const violation_pos = document.positionAt(method_body_start + dom_match.index);
const violation_end = document.positionAt(method_body_start + dom_match.index + dom_match[0].length);
diagnostics.push(new vscode.Diagnostic(new vscode.Range(violation_pos, violation_end), `'on_load' should not access DOM elements. It should be headless, using only ${dom_match[1]}.args for inputs and setting ${dom_match[1]}.data for outputs`, vscode.DiagnosticSeverity.Warning));
}
}
}
}
this.diagnostics_collection.set(document.uri, diagnostics);
}
dispose() {
this.diagnostics_collection.dispose();
}
}
exports.JqhtmlLifecycleDiagnosticProvider = JqhtmlLifecycleDiagnosticProvider;
//# sourceMappingURL=jqhtml_lifecycle_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,204 @@
"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.LaravelCompletionProvider = void 0;
const vscode = __importStar(require("vscode"));
class LaravelCompletionProvider {
constructor() {
this.laravel_functions = new Map();
this.initialize_laravel_functions();
}
initialize_laravel_functions() {
// Path Helpers
this.add_function('base_path', 'base_path(string $path = \'\'): string', 'Get the base path of the Laravel installation. Optionally append a path.');
this.add_function('app_path', 'app_path(string $path = \'\'): string', 'Get the path to the app folder. Optionally append a path.');
this.add_function('config_path', 'config_path(string $path = \'\'): string', 'Get the configuration path. Optionally append a path.');
this.add_function('database_path', 'database_path(string $path = \'\'): string', 'Get the database path. Optionally append a path.');
this.add_function('public_path', 'public_path(string $path = \'\'): string', 'Get the public path. Optionally append a path.');
this.add_function('resource_path', 'resource_path(string $path = \'\'): string', 'Get the path to the resources folder. Optionally append a path.');
this.add_function('storage_path', 'storage_path(string $path = \'\'): string', 'Get the path to the storage folder. Optionally append a path.');
// Environment & Config
this.add_function('env', 'env(string $key, mixed $default = null): mixed', 'Get the value of an environment variable. Supports a default value.');
this.add_function('config', 'config(string|array|null $key = null, mixed $default = null): mixed', 'Get/set a configuration value. Pass an array to set multiple values.');
this.add_function('app', 'app(string|null $abstract = null, array $parameters = []): mixed', 'Get the available container instance or resolve a type from the container.');
// URL & Asset Helpers
this.add_function('url', 'url(string|null $path = null, mixed $parameters = [], bool|null $secure = null): string', 'Generate a URL for the application.');
this.add_function('asset', 'asset(string $path, bool|null $secure = null): string', 'Generate an asset URL.');
this.add_function('secure_asset', 'secure_asset(string $path): string', 'Generate an asset URL using HTTPS.');
this.add_function('route', 'route(string $name, mixed $parameters = [], bool $absolute = true): string', 'Generate the URL to a named route.');
this.add_function('mix', 'mix(string $path, string $manifestDirectory = \'\'): string', 'Get the path to a versioned Mix file.');
// Request & Response
this.add_function('request', 'request(array|string|null $key = null, mixed $default = null): mixed', 'Get an instance of the current request or an input item from the request.');
this.add_function('response', 'response(mixed $content = \'\', int $status = 200, array $headers = []): mixed', 'Create a new response instance.');
this.add_function('redirect', 'redirect(string|null $to = null, int $status = 302, array $headers = [], bool|null $secure = null): mixed', 'Get an instance of the redirector or create a new redirect response.');
this.add_function('back', 'back(int $status = 302, array $headers = [], mixed $fallback = false): mixed', 'Create a new redirect response to the previous location.');
this.add_function('abort', 'abort(int $code, string $message = \'\', array $headers = []): never', 'Throw an HttpException with the given data.');
this.add_function('abort_if', 'abort_if(bool $boolean, int $code, string $message = \'\', array $headers = []): void', 'Throw an HttpException with the given data if the given condition is true.');
this.add_function('abort_unless', 'abort_unless(bool $boolean, int $code, string $message = \'\', array $headers = []): void', 'Throw an HttpException with the given data unless the given condition is true.');
// Authentication & Session
this.add_function('auth', 'auth(string|null $guard = null): mixed', 'Get the available auth instance or a specific guard.');
this.add_function('session', 'session(array|string|null $key = null, mixed $default = null): mixed', 'Get/set the specified session value.');
this.add_function('old', 'old(string|null $key = null, mixed $default = null): mixed', 'Retrieve an old input item.');
this.add_function('cookie', 'cookie(string|null $name = null, string|null $value = null, int $minutes = 0, ...): mixed', 'Create a new cookie instance.');
this.add_function('csrf_token', 'csrf_token(): string', 'Get the CSRF token value.');
this.add_function('csrf_field', 'csrf_field(): string', 'Generate a CSRF token form field.');
// Caching
this.add_function('cache', 'cache(mixed ...$arguments): mixed', 'Get/set the specified cache value.');
// Collections & Arrays
this.add_function('collect', 'collect(mixed $value = null): mixed', 'Create a collection from the given value.');
this.add_function('data_get', 'data_get(mixed $target, string|array|null $key, mixed $default = null): mixed', 'Get an item from an array or object using "dot" notation.');
this.add_function('data_set', 'data_set(mixed &$target, string|array $key, mixed $value, bool $overwrite = true): mixed', 'Set an item on an array or object using dot notation.');
this.add_function('data_forget', 'data_forget(mixed &$target, string|array $key): mixed', 'Remove an item from an array or object using "dot" notation.');
// Debugging
this.add_function('dd', 'dd(mixed ...$vars): never', 'Dump the given variables and end the script.');
this.add_function('dump', 'dump(mixed ...$vars): void', 'Dump the given variables.');
this.add_function('info', 'info(string $message, array $context = []): void', 'Write some information to the log.');
this.add_function('logger', 'logger(string|null $message = null, array $context = []): mixed', 'Log a debug message to the logs or get a logger instance.');
// Events & Jobs
this.add_function('event', 'event(mixed ...$args): mixed', 'Dispatch an event and call the listeners.');
this.add_function('dispatch', 'dispatch(mixed $job): mixed', 'Dispatch a job to its appropriate handler.');
this.add_function('dispatch_now', 'dispatch_now(mixed $job): mixed', 'Dispatch a job immediately (synchronously).');
this.add_function('dispatch_sync', 'dispatch_sync(mixed $job): mixed', 'Dispatch a job synchronously.');
// Encryption & Hashing
this.add_function('bcrypt', 'bcrypt(string $value, array $options = []): string', 'Hash the given value using Bcrypt.');
this.add_function('encrypt', 'encrypt(mixed $value, bool $serialize = true): string', 'Encrypt the given value.');
this.add_function('decrypt', 'decrypt(string $payload, bool $unserialize = true): mixed', 'Decrypt the given value.');
// Date & Time
this.add_function('now', 'now(mixed $tz = null): mixed', 'Create a new Carbon instance for the current time.');
this.add_function('today', 'today(mixed $tz = null): mixed', 'Create a new Carbon instance for the current date.');
// Translation
this.add_function('trans', 'trans(string|null $key = null, array $replace = [], string|null $locale = null): string', 'Translate the given message.');
this.add_function('trans_choice', 'trans_choice(string $key, int|array|\\Countable $number, array $replace = [], string|null $locale = null): string', 'Translate the given message with a count.');
this.add_function('__', '__(string|null $key = null, array $replace = [], string|null $locale = null): string', 'Translate the given message (alias for trans).');
// Validation
this.add_function('validator', 'validator(array $data = [], array $rules = [], array $messages = [], array $customAttributes = []): mixed', 'Create a new Validator instance.');
// Other Laravel Helpers
this.add_function('class_basename', 'class_basename(string|object $class): string', 'Get the class "basename" of the given object/class.');
this.add_function('class_uses_recursive', 'class_uses_recursive(string|object $class): array', 'Returns all traits used by a class, its parent classes and trait of their traits.');
this.add_function('trait_uses_recursive', 'trait_uses_recursive(string|object $trait): array', 'Returns all traits used by a trait and its traits.');
this.add_function('value', 'value(mixed $value, mixed ...$args): mixed', 'Return the default value of the given value.');
this.add_function('with', 'with(mixed $value, callable|null $callback = null): mixed', 'Return the given value, optionally passed through the given callback.');
this.add_function('tap', 'tap(mixed $value, callable|null $callback = null): mixed', 'Call the given Closure with the given value then return the value.');
this.add_function('blank', 'blank(mixed $value): bool', 'Determine if the given value is "blank".');
this.add_function('filled', 'filled(mixed $value): bool', 'Determine if a value is "filled".');
this.add_function('optional', 'optional(mixed $value, callable|null $callback = null): mixed', 'Provide access to optional objects.');
this.add_function('rescue', 'rescue(callable $callback, mixed $rescue = null, bool|callable $report = true): mixed', 'Catch a potential exception and return a default value.');
this.add_function('retry', 'retry(int $times, callable $callback, int|\\Closure $sleepMilliseconds = 0, callable|null $when = null): mixed', 'Retry an operation a given number of times.');
this.add_function('throw_if', 'throw_if(mixed $condition, \\Throwable|string $exception = \'RuntimeException\', mixed ...$parameters): mixed', 'Throw the given exception if the given condition is true.');
this.add_function('throw_unless', 'throw_unless(mixed $condition, \\Throwable|string $exception = \'RuntimeException\', mixed ...$parameters): mixed', 'Throw the given exception unless the given condition is true.');
this.add_function('windows_os', 'windows_os(): bool', 'Determine whether the current environment is Windows based.');
}
add_function(name, signature, documentation) {
this.laravel_functions.set(name, {
name,
signature,
documentation
});
}
provideCompletionItems(document, position, token, context) {
const line_text = document.lineAt(position).text;
const before_cursor = line_text.substring(0, position.character);
// Check if we're in a position where a function name might be typed
const function_pattern = /([a-z_][a-z0-9_]*)?$/i;
const match = before_cursor.match(function_pattern);
if (!match) {
return [];
}
const prefix = match[1] || '';
const completion_items = [];
this.laravel_functions.forEach((func_data, func_name) => {
if (func_name.toLowerCase().startsWith(prefix.toLowerCase())) {
const item = new vscode.CompletionItem(func_name, vscode.CompletionItemKind.Function);
item.detail = func_data.signature;
item.documentation = new vscode.MarkdownString(func_data.documentation);
// Create snippet for function with parentheses
const params = this.extract_parameters(func_data.signature);
if (params.length > 0) {
// Create snippet with placeholders for required parameters
const snippet_params = params
.filter(p => !p.optional)
.map((p, i) => `\${${i + 1}:${p.name}}`)
.join(', ');
item.insertText = new vscode.SnippetString(`${func_name}(${snippet_params})`);
}
else {
item.insertText = new vscode.SnippetString(`${func_name}()`);
}
item.sortText = '0' + func_name; // Prioritize Laravel functions
completion_items.push(item);
}
});
return completion_items;
}
extract_parameters(signature) {
const params = [];
// Extract everything between parentheses
const match = signature.match(/\((.*?)\)/);
if (!match) {
return params;
}
const params_str = match[1];
if (!params_str.trim()) {
return params;
}
// Split by comma but handle nested parentheses/brackets
const param_parts = this.smart_split(params_str, ',');
for (const param of param_parts) {
const param_match = param.match(/\$([a-z_][a-z0-9_]*)/i);
if (param_match) {
const param_name = param_match[1];
const is_optional = param.includes('=');
params.push({ name: param_name, optional: is_optional });
}
}
return params;
}
smart_split(str, delimiter) {
const result = [];
let current = '';
let depth = 0;
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === '(' || char === '[' || char === '{') {
depth++;
}
else if (char === ')' || char === ']' || char === '}') {
depth--;
}
else if (char === delimiter && depth === 0) {
result.push(current.trim());
current = '';
continue;
}
current += char;
}
if (current.trim()) {
result.push(current.trim());
}
return result;
}
}
exports.LaravelCompletionProvider = LaravelCompletionProvider;
//# sourceMappingURL=laravel_completion_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,68 @@
"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.PhpAttributeSemanticTokensProvider = void 0;
const vscode = __importStar(require("vscode"));
/**
* Framework PHP attributes that should be highlighted
*/
const FRAMEWORK_ATTRIBUTES = [
'Ajax_Endpoint',
'Route',
'Auth',
'Relationship',
'Monoprogenic',
'Instantiatable'
];
/**
* Provides semantic tokens for PHP attributes (amber color)
*/
class PhpAttributeSemanticTokensProvider {
async provideDocumentSemanticTokens(document) {
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'php') {
return tokens_builder.build();
}
const text = document.getText();
// Find all PHP attributes: #[AttributeName] or #[\AttributeName]
for (const attribute_name of FRAMEWORK_ATTRIBUTES) {
// Match: #[AttributeName or #[\AttributeName with optional namespace prefix
// Captures the attribute name only (not brackets or backslash)
const regex = new RegExp(`#\\[\\\\?(${attribute_name})(?:\\s|\\(|\\])`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
// match[1] contains the attribute name without namespace prefix
const attr_start = match.index + match[0].indexOf(match[1]);
const position = document.positionAt(attr_start);
tokens_builder.push(position.line, position.character, attribute_name.length, 0, // token type index for 'conventionMethod'
0 // token modifiers
);
}
}
return tokens_builder.build();
}
}
exports.PhpAttributeSemanticTokensProvider = PhpAttributeSemanticTokensProvider;
//# sourceMappingURL=php_attribute_provider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"php_attribute_provider.js","sourceRoot":"","sources":["../src/php_attribute_provider.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AAEjC;;GAEG;AACH,MAAM,oBAAoB,GAAG;IACzB,eAAe;IACf,OAAO;IACP,MAAM;IACN,cAAc;IACd,cAAc;IACd,gBAAgB;CACnB,CAAC;AAEF;;GAEG;AACH,MAAa,kCAAkC;IAC3C,KAAK,CAAC,6BAA6B,CAAC,QAA6B;QAC7D,MAAM,cAAc,GAAG,IAAI,MAAM,CAAC,qBAAqB,EAAE,CAAC;QAE1D,IAAI,QAAQ,CAAC,UAAU,KAAK,KAAK,EAAE;YAC/B,OAAO,cAAc,CAAC,KAAK,EAAE,CAAC;SACjC;QAED,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;QAEhC,iEAAiE;QACjE,KAAK,MAAM,cAAc,IAAI,oBAAoB,EAAE;YAC/C,4EAA4E;YAC5E,+DAA+D;YAC/D,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,aAAa,cAAc,kBAAkB,EAAE,GAAG,CAAC,CAAC;YAC7E,IAAI,KAAK,CAAC;YAEV,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE;gBACxC,gEAAgE;gBAChE,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC5D,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;gBAEjD,cAAc,CAAC,IAAI,CACf,QAAQ,CAAC,IAAI,EACb,QAAQ,CAAC,SAAS,EAClB,cAAc,CAAC,MAAM,EACrB,CAAC,EAAE,0CAA0C;gBAC7C,CAAC,CAAE,kBAAkB;iBACxB,CAAC;aACL;SACJ;QAED,OAAO,cAAc,CAAC,KAAK,EAAE,CAAC;IAClC,CAAC;CACJ;AAlCD,gFAkCC"}

View File

@@ -0,0 +1,73 @@
"use strict";
/**
* RSpade Refactor Code Actions Provider
*
* Provides refactoring actions that appear in the "Refactor..." menu
* when the cursor is on a static method definition or call.
*/
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.RspadeRefactorCodeActionsProvider = void 0;
const vscode = __importStar(require("vscode"));
class RspadeRefactorCodeActionsProvider {
constructor(refactor_provider) {
this.refactor_provider = refactor_provider;
}
provideCodeActions(document, range, context, token) {
// Only provide actions for PHP files in ./rsx or ./app/RSpade
if (document.languageId !== 'php') {
return undefined;
}
const file_path = document.uri.fsPath;
if (!file_path.includes('/rsx/') && !file_path.includes('\\rsx\\') &&
!file_path.includes('/app/RSpade') && !file_path.includes('\\app\\RSpade')) {
return undefined;
}
// Check if line contains a static method (cursor can be anywhere on the line)
const position = range.start;
const line = document.lineAt(position.line).text;
// Check for static method definition: public/protected/private static function method_name
const static_definition_match = line.match(/\b(?:public|protected|private)\s+static\s+function\s+(\w+)/);
if (static_definition_match) {
return this.create_refactor_actions();
}
// Check for static method call: ClassName::method_name
const static_call_match = line.match(/(\w+)::(\w+)/);
if (static_call_match) {
return this.create_refactor_actions();
}
return undefined;
}
create_refactor_actions() {
const action = new vscode.CodeAction('Global Rename Method', vscode.CodeActionKind.Refactor);
action.command = {
command: 'rspade.refactorStaticMethod',
title: 'Global Rename Method'
};
return [action];
}
}
exports.RspadeRefactorCodeActionsProvider = RspadeRefactorCodeActionsProvider;
//# sourceMappingURL=refactor_code_actions.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"refactor_code_actions.js","sourceRoot":"","sources":["../src/refactor_code_actions.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,+CAAiC;AAGjC,MAAa,iCAAiC;IAG1C,YAAY,iBAAyC;QACjD,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;IAC/C,CAAC;IAEM,kBAAkB,CACrB,QAA6B,EAC7B,KAAsC,EACtC,OAAiC,EACjC,KAA+B;QAE/B,8DAA8D;QAC9D,IAAI,QAAQ,CAAC,UAAU,KAAK,KAAK,EAAE;YAC/B,OAAO,SAAS,CAAC;SACpB;QAED,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC;YAC9D,CAAC,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE;YAC5E,OAAO,SAAS,CAAC;SACpB;QAED,8EAA8E;QAC9E,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC;QAC7B,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;QAEjD,2FAA2F;QAC3F,MAAM,uBAAuB,GAAG,IAAI,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;QACzG,IAAI,uBAAuB,EAAE;YACzB,OAAO,IAAI,CAAC,uBAAuB,EAAE,CAAC;SACzC;QAED,uDAAuD;QACvD,MAAM,iBAAiB,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACrD,IAAI,iBAAiB,EAAE;YACnB,OAAO,IAAI,CAAC,uBAAuB,EAAE,CAAC;SACzC;QAED,OAAO,SAAS,CAAC;IACrB,CAAC;IAEO,uBAAuB;QAC3B,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,UAAU,CAChC,sBAAsB,EACtB,MAAM,CAAC,cAAc,CAAC,QAAQ,CACjC,CAAC;QACF,MAAM,CAAC,OAAO,GAAG;YACb,OAAO,EAAE,6BAA6B;YACtC,KAAK,EAAE,sBAAsB;SAChC,CAAC;QACF,OAAO,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;CACJ;AAtDD,8EAsDC"}

View File

@@ -0,0 +1,284 @@
"use strict";
/**
* RSpade Refactor Provider
*
* Provides context menu refactoring options for PHP static methods.
* Communicates with the server-side refactor commands via the IDE service.
*/
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.RspadeRefactorProvider = void 0;
const vscode = __importStar(require("vscode"));
class RspadeRefactorProvider {
constructor(formatting_provider) {
this.formatting_provider = formatting_provider;
this.output_channel = vscode.window.createOutputChannel('RSpade Refactor');
}
/**
* Register the refactor command
*/
register(context) {
const command = vscode.commands.registerCommand('rspade.refactorStaticMethod', async () => await this.refactor_static_method());
context.subscriptions.push(command);
}
/**
* Check if the refactor command should be available
*/
should_show_refactor_menu() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return false;
}
const document = editor.document;
// Only PHP files
if (document.languageId !== 'php') {
return false;
}
// Must be in ./rsx or ./app/RSpade directory
const file_path = document.uri.fsPath;
if (!file_path.includes('/rsx/') && !file_path.includes('\\rsx\\') &&
!file_path.includes('/app/RSpade') && !file_path.includes('\\app\\RSpade')) {
return false;
}
// Check if cursor is on a static method definition or call
const position = editor.selection.active;
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return false;
}
const word = document.getText(word_range);
const line = document.lineAt(position.line).text;
// Check for static method definition: public/protected/private static function method_name
const static_definition_match = line.match(/\b(?:public|protected|private)\s+static\s+function\s+(\w+)/);
if (static_definition_match) {
const method_name = static_definition_match[1];
// Cursor must be on the method name itself
return word === method_name;
}
// Check for static method call: ClassName::method_name
const static_call_match = line.match(/(\w+)::(\w+)/);
if (static_call_match) {
const method_name = static_call_match[2];
// Cursor must be on the method name itself
return word === method_name;
}
return false;
}
/**
* Main refactor method
*/
async refactor_static_method() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
const document = editor.document;
const position = editor.selection.active;
this.output_channel.clear();
this.output_channel.show(true);
this.output_channel.appendLine('=== RSpade Method Refactor ===\n');
try {
// Extract method information from cursor position
const method_info = await this.extract_method_info(document, position);
if (!method_info) {
vscode.window.showErrorMessage('Could not identify static method at cursor position');
return;
}
this.output_channel.appendLine(`Class: ${method_info.class_name}`);
this.output_channel.appendLine(`Method: ${method_info.method_name}\n`);
// Show input dialog for new method name
const new_method_name = await vscode.window.showInputBox({
title: `Global Rename Method: ${method_info.class_name}::${method_info.method_name}`,
prompt: 'Enter new method name:',
placeHolder: 'new_method_name',
value: method_info.method_name,
ignoreFocusOut: true,
validateInput: (value) => {
if (!value) {
return 'Method name cannot be empty';
}
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
return 'Method name must be snake_case (lowercase with underscores)';
}
if (value === method_info.method_name) {
return 'New method name must be different from current name';
}
return null;
}
});
if (!new_method_name) {
this.output_channel.appendLine('Refactor cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Confirm refactoring
const confirmation = await vscode.window.showWarningMessage(`Global Rename: ${method_info.class_name}::${method_info.method_name}${method_info.class_name}::${new_method_name}\n\n` +
'This will rename the method across all usages in all files.', { modal: true }, 'Rename', 'Cancel');
if (confirmation !== 'Rename') {
this.output_channel.appendLine('Global rename cancelled by user');
return;
}
// Save all dirty files first
this.output_channel.appendLine('Checking for unsaved files...');
const dirty_documents = vscode.workspace.textDocuments.filter(doc => doc.isDirty);
if (dirty_documents.length > 0) {
this.output_channel.appendLine(`Found ${dirty_documents.length} unsaved file(s):`);
for (const doc of dirty_documents) {
this.output_channel.appendLine(` - ${doc.fileName}`);
}
this.output_channel.appendLine('\nSaving all files...');
const save_result = await vscode.workspace.saveAll(false);
if (!save_result) {
const error_msg = 'Failed to save all files. Refactor operation aborted.';
this.output_channel.appendLine(`\nERROR: ${error_msg}`);
vscode.window.showErrorMessage(error_msg);
return;
}
this.output_channel.appendLine('All files saved successfully\n');
}
else {
this.output_channel.appendLine('No unsaved files\n');
}
// Show output channel
this.output_channel.show(true);
// Show terminal and execute refactoring
this.output_channel.appendLine(`Refactoring ${method_info.class_name}::${method_info.method_name} to ${method_info.class_name}::${new_method_name}...`);
this.output_channel.appendLine('');
const result = await this.execute_refactor(method_info.class_name, method_info.method_name, new_method_name);
// Display result in terminal
this.output_channel.appendLine('\n=== Refactor Output ===\n');
this.output_channel.appendLine(result);
this.output_channel.appendLine('\n=== Refactor Complete ===');
// Check if refactor was successful
if (result.includes('=== Refactor Complete ===') || result.trim().length > 0) {
// Wait 3.5 seconds total (2s + 1.5s), hide panel, then reload files
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 500ms for filesystem changes to propagate (Samba/network issues)
setTimeout(async () => {
await this.reload_all_open_files();
}, 500);
}, 3500);
vscode.window.showInformationMessage(`Successfully refactored ${method_info.class_name}::${method_info.method_name} to ${new_method_name}`);
}
}
catch (error) {
const error_message = error.message || String(error);
this.output_channel.appendLine(`\nERROR: ${error_message}`);
vscode.window.showErrorMessage(`Refactor failed: ${error_message}`);
}
}
/**
* Extract method information from cursor position
*/
async extract_method_info(document, position) {
const line = document.lineAt(position.line).text;
// Check for static method definition: public/protected/private static function method_name
const definition_match = line.match(/\b(?:public|protected|private)\s+static\s+function\s+(\w+)/);
if (definition_match) {
const method_name = definition_match[1];
// Extract class name from the file
const class_name = await this.extract_class_name(document);
if (class_name) {
return { class_name, method_name };
}
}
// Check for static method call: ClassName::method_name
const static_call_match = line.match(/(\w+)::(\w+)/);
if (static_call_match) {
const class_name = static_call_match[1];
const method_name = static_call_match[2];
return { class_name, method_name };
}
return null;
}
/**
* Extract class name from PHP file
*/
async extract_class_name(document) {
const text = document.getText();
// Match actual class declaration, not @class in comments
// Look for: class ClassName or abstract class ClassName or final class ClassName
const class_match = text.match(/^\s*(?:abstract\s+|final\s+)?class\s+(\w+)/m);
if (class_match) {
return class_match[1];
}
return null;
}
/**
* Reload all open text documents
*/
async reload_all_open_files() {
const text_documents = vscode.workspace.textDocuments;
for (const document of text_documents) {
// Skip untitled documents
if (document.uri.scheme === 'untitled') {
continue;
}
// Skip non-file schemes (git, output channels, etc)
if (document.uri.scheme !== 'file') {
continue;
}
// Get the text editor for this document
const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.toString() === document.uri.toString());
if (editors.length > 0) {
// Document is currently visible, reload it
const position = editors[0].selection.active;
const view_column = editors[0].viewColumn;
// Close and reopen to force reload
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
const new_document = await vscode.workspace.openTextDocument(document.uri);
const editor = await vscode.window.showTextDocument(new_document, view_column);
// Restore cursor position
editor.selection = new vscode.Selection(position, position);
editor.revealRange(new vscode.Range(position, position));
}
}
}
/**
* Execute the refactor command via IDE service
*/
async execute_refactor(class_name, old_method, new_method) {
// Ensure authentication
await this.formatting_provider.ensure_auth();
// Prepare request data
const request_data = {
command: 'rsx:refactor:rename_php_class_function',
arguments: [class_name, old_method, new_method]
};
this.output_channel.appendLine('Sending refactor request to server...');
this.output_channel.appendLine(`Command: ${request_data.command}`);
this.output_channel.appendLine(`Arguments: ${JSON.stringify(request_data.arguments)}\n`);
// Make authenticated request
const response = await this.formatting_provider.make_authenticated_request('/command', request_data);
if (!response.success) {
throw new Error(response.error || 'Refactor command failed');
}
return response.output || 'Refactor completed successfully (no output)';
}
}
exports.RspadeRefactorProvider = RspadeRefactorProvider;
//# sourceMappingURL=refactor_provider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,156 @@
"use strict";
/**
* RSpade Sort Class Methods Provider
*
* Reorganizes methods in PHP class files according to RSpade conventions
*/
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.RspadeSortClassMethodsProvider = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
class RspadeSortClassMethodsProvider {
constructor(formatting_provider) {
this.formatting_provider = formatting_provider;
this.output_channel = vscode.window.createOutputChannel('RSpade Sort Methods');
}
/**
* Register the sort command
*/
register(context) {
const command = vscode.commands.registerCommand('rspade.sortClassMethods', async (uri) => await this.sort_class_methods(uri));
context.subscriptions.push(command);
}
/**
* Main sort method
*/
async sort_class_methods(uri) {
// Determine file path
let file_path;
if (uri) {
// Called from explorer context menu
file_path = uri.fsPath;
}
else {
// Called from command palette - use active editor
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active file to sort');
return;
}
file_path = editor.document.uri.fsPath;
}
// Validate it's a PHP file
if (!file_path.endsWith('.php')) {
vscode.window.showErrorMessage('Can only sort PHP class files');
return;
}
this.output_channel.clear();
this.output_channel.show(true);
this.output_channel.appendLine('=== RSpade Sort Class Methods ===\n');
this.output_channel.appendLine(`File: ${file_path}\n`);
try {
// Get workspace root to make path relative
const workspace_folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(file_path));
if (!workspace_folder) {
throw new Error('File is not in workspace');
}
const relative_path = path.relative(workspace_folder.uri.fsPath, file_path);
this.output_channel.appendLine(`Relative path: ${relative_path}\n`);
// Confirm sorting
const confirmation = await vscode.window.showWarningMessage(`Sort methods in ${path.basename(file_path)}?\n\n` +
'This will reorganize all methods according to RSpade conventions.', { modal: true }, 'Sort', 'Cancel');
if (confirmation !== 'Sort') {
this.output_channel.appendLine('Sort cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Execute sort
this.output_channel.appendLine('Sorting methods...\n');
const result = await this.execute_sort(relative_path);
// Display result
this.output_channel.appendLine('\n=== Sort Output ===\n');
this.output_channel.appendLine(result);
this.output_channel.appendLine('\n=== Sort Complete ===');
// Reload the file if it's open
const document = await vscode.workspace.openTextDocument(file_path);
const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.fsPath === file_path);
if (editors.length > 0) {
// Wait 3.5 seconds, close panel, then reload
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 500ms for filesystem changes
setTimeout(async () => {
for (const editor of editors) {
const position = editor.selection.active;
const view_column = editor.viewColumn;
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
const new_document = await vscode.workspace.openTextDocument(file_path);
const new_editor = await vscode.window.showTextDocument(new_document, view_column);
// Restore cursor position
new_editor.selection = new vscode.Selection(position, position);
new_editor.revealRange(new vscode.Range(position, position));
}
}, 500);
}, 3500);
}
else {
// File not open, just close panel
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
}, 3500);
}
vscode.window.showInformationMessage(`Successfully sorted methods in ${path.basename(file_path)}`);
}
catch (error) {
const error_message = error.message || String(error);
this.output_channel.appendLine(`\nERROR: ${error_message}`);
vscode.window.showErrorMessage(`Sort failed: ${error_message}`);
}
}
/**
* Execute the sort command via IDE service
*/
async execute_sort(file_path) {
// Ensure authentication
await this.formatting_provider.ensure_auth();
// Prepare request data
const request_data = {
command: 'rsx:refactor:sort_php_class_functions',
arguments: [file_path]
};
this.output_channel.appendLine('Sending sort request to server...');
this.output_channel.appendLine(`Command: ${request_data.command}`);
this.output_channel.appendLine(`Arguments: ${JSON.stringify(request_data.arguments)}\n`);
// Make authenticated request
const response = await this.formatting_provider.make_authenticated_request('/command', request_data);
if (!response.success) {
throw new Error(response.error || 'Sort command failed');
}
return response.output || 'Sort completed successfully (no output)';
}
}
exports.RspadeSortClassMethodsProvider = RspadeSortClassMethodsProvider;
//# sourceMappingURL=sort_class_methods_provider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sort_class_methods_provider.js","sourceRoot":"","sources":["../src/sort_class_methods_provider.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,+CAAiC;AACjC,2CAA6B;AAG7B,MAAa,8BAA8B;IAIvC,YAAY,mBAA6C;QACrD,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,qBAAqB,CAAC,CAAC;IACnF,CAAC;IAED;;OAEG;IACI,QAAQ,CAAC,OAAgC;QAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,eAAe,CAC3C,yBAAyB,EACzB,KAAK,EAAE,GAAgB,EAAE,EAAE,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CACjE,CAAC;QACF,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,kBAAkB,CAAC,GAAgB;QAC7C,sBAAsB;QACtB,IAAI,SAAiB,CAAC;QAEtB,IAAI,GAAG,EAAE;YACL,oCAAoC;YACpC,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC;SAC1B;aAAM;YACH,kDAAkD;YAClD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC;YAC9C,IAAI,CAAC,MAAM,EAAE;gBACT,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,wBAAwB,CAAC,CAAC;gBACzD,OAAO;aACV;YACD,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;SAC1C;QAED,2BAA2B;QAC3B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;YAC7B,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,+BAA+B,CAAC,CAAC;YAChE,OAAO;SACV;QAED,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;QAC5B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,qCAAqC,CAAC,CAAC;QACtE,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,SAAS,SAAS,IAAI,CAAC,CAAC;QAEvD,IAAI;YACA,2CAA2C;YAC3C,MAAM,gBAAgB,GAAG,MAAM,CAAC,SAAS,CAAC,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YACzF,IAAI,CAAC,gBAAgB,EAAE;gBACnB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;aAC/C;YAED,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YAC5E,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,kBAAkB,aAAa,IAAI,CAAC,CAAC;YAEpE,kBAAkB;YAClB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,kBAAkB,CACvD,mBAAmB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO;gBAClD,mEAAmE,EACnE,EAAE,KAAK,EAAE,IAAI,EAAE,EACf,MAAM,EACN,QAAQ,CACX,CAAC;YAEF,IAAI,YAAY,KAAK,MAAM,EAAE;gBACzB,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,wBAAwB,CAAC,CAAC;gBACzD,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,6BAA6B,CAAC,CAAC;gBACpE,OAAO;aACV;YAED,eAAe;YACf,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC;YACvD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;YAEtD,iBAAiB;YACjB,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,yBAAyB,CAAC,CAAC;YAC1D,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YACvC,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,yBAAyB,CAAC,CAAC;YAE1D,+BAA+B;YAC/B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;YACpE,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,MAAM,CACnD,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,CACrD,CAAC;YAEF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE;gBACpB,6CAA6C;gBAC7C,UAAU,CAAC,KAAK,IAAI,EAAE;oBAClB,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,6BAA6B,CAAC,CAAC;oBAEpE,oCAAoC;oBACpC,UAAU,CAAC,KAAK,IAAI,EAAE;wBAClB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;4BAC1B,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;4BACzC,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC;4BAEtC,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,oCAAoC,CAAC,CAAC;4BAC3E,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;4BACxE,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;4BAEnF,0BAA0B;4BAC1B,UAAU,CAAC,SAAS,GAAG,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;4BAChE,UAAU,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;yBAChE;oBACL,CAAC,EAAE,GAAG,CAAC,CAAC;gBACZ,CAAC,EAAE,IAAI,CAAC,CAAC;aACZ;iBAAM;gBACH,kCAAkC;gBAClC,UAAU,CAAC,KAAK,IAAI,EAAE;oBAClB,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,6BAA6B,CAAC,CAAC;gBACxE,CAAC,EAAE,IAAI,CAAC,CAAC;aACZ;YAED,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,kCAAkC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;SAEtG;QAAC,OAAO,KAAU,EAAE;YACjB,MAAM,aAAa,GAAG,KAAK,CAAC,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;YACrD,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,YAAY,aAAa,EAAE,CAAC,CAAC;YAC5D,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,gBAAgB,aAAa,EAAE,CAAC,CAAC;SACnE;IACL,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,YAAY,CAAC,SAAiB;QACxC,wBAAwB;QACxB,MAAM,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,CAAC;QAE7C,uBAAuB;QACvB,MAAM,YAAY,GAAG;YACjB,OAAO,EAAE,uCAAuC;YAChD,SAAS,EAAE,CAAC,SAAS,CAAC;SACzB,CAAC;QAEF,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,mCAAmC,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,YAAY,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAEzF,6BAA6B;QAC7B,MAAM,QAAQ,GAAG,MAAO,IAAI,CAAC,mBAA2B,CAAC,0BAA0B,CAC/E,UAAU,EACV,YAAY,CACf,CAAC;QAEF,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE;YACnB,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,IAAI,qBAAqB,CAAC,CAAC;SAC5D;QAED,OAAO,QAAQ,CAAC,MAAM,IAAI,yCAAyC,CAAC;IACxE,CAAC;CACJ;AA7JD,wEA6JC"}

View File

@@ -0,0 +1,156 @@
{
"name": "rspade-framework",
"displayName": "RSpade Framework Support",
"description": "VS Code extension for RSpade framework with code folding, formatting, and namespace management",
"version": "0.1.170",
"publisher": "rspade",
"engines": {
"vscode": "^1.74.0"
},
"categories": [
"Programming Languages",
"Formatters",
"Other"
],
"activationEvents": [
"workspaceContains:**/app/RSpade/**"
],
"main": "./out/extension.js",
"contributes": {
"configuration": {
"title": "RSpade Framework",
"properties": {
"rspade.enableCodeFolding": {
"type": "boolean",
"default": true,
"description": "Enable automatic folding of LLMDIRECTIVE blocks"
},
"rspade.enableReadOnlyRegions": {
"type": "boolean",
"default": true,
"description": "Show visual indicators for RSX:USE sections"
},
"rspade.enableFormatOnMove": {
"type": "boolean",
"default": true,
"description": "Automatically update namespaces when moving PHP files"
},
"rspade.pythonPath": {
"type": "string",
"default": "python",
"description": "Path to Python executable (python or python3)"
},
"rspade.projectType": {
"type": "string",
"default": "",
"description": "Set to 'rspade' to enable RSpade framework features"
},
"rspade.enableBladeAutoSpacing": {
"type": "boolean",
"default": true,
"description": "Automatically add spaces inside Blade tags when typing"
}
}
},
"commands": [
{
"command": "rspade.toggleFolding",
"title": "RSpade: Toggle LLMDIRECTIVE Folding"
},
{
"command": "rspade.formatPhpFile",
"title": "RSpade: Format PHP File"
},
{
"command": "rspade.updateNamespace",
"title": "RSpade: Update Namespace for Current File"
},
{
"command": "rspade.copyRelativePathFromRoot",
"title": "RSpade: Copy Relative Path from Project Root"
},
{
"command": "rspade.refactorStaticMethod",
"title": "$(symbol-method) Rsx: Global Rename Method"
},
{
"command": "rspade.refactorClass",
"title": "$(symbol-class) Rsx: Global Rename Class"
},
{
"command": "rspade.sortClassMethods",
"title": "$(list-ordered) Rsx: Sort Class Methods"
}
],
"menus": {
"explorer/context": [
{
"command": "rspade.sortClassMethods",
"when": "resourceExtname == .php",
"group": "2_workspace"
}
]
},
"keybindings": [
{
"command": "rspade.copyRelativePathFromRoot",
"key": "ctrl+shift+alt+c",
"mac": "cmd+shift+alt+c",
"when": "editorFocus"
}
],
"languages": [
{
"id": "php",
"extensions": [
".php"
]
}
],
"grammars": [],
"semanticTokenTypes": [
{
"id": "conventionMethod",
"superType": "method",
"description": "Convention method automatically called by RSX framework"
},
{
"id": "jqhtmlTagAttribute",
"superType": "parameter",
"description": "The tag attribute on jqhtml components"
}
],
"semanticTokenScopes": [
{
"scopes": {
"conventionMethod": ["entity.name.function.convention.rspade"],
"jqhtmlTagAttribute": ["entity.other.attribute-name.jqhtml.tag"]
}
}
],
"configurationDefaults": {
"editor.semanticTokenColorCustomizations": {
"rules": {
"conventionMethod": "#FFA500",
"jqhtmlTagAttribute": "#FFA500"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./ && echo '\\n⚠ IMPORTANT: To complete the VS Code extension update, run: ./build.sh\\n\\nThe build.sh script will:\\n- Install all prerequisite npm dependencies\\n- Compile TypeScript to JavaScript\\n- Package the extension\\n- Install it in VS Code for immediate use\\n\\n⛔ FOR DEVELOPERS/LLM AGENTS: Do NOT run \"npm install\" or \"npm run compile\" directly.\\nAlways use ./build.sh which handles everything automatically.\\n'",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.74.0",
"@types/node": "16.x",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"typescript": "^4.9.3"
}
}

Binary file not shown.

View File

@@ -0,0 +1,518 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
/**
* Provides automatic file renaming based on RSX naming conventions
*
* This provider watches for file saves in ./rsx directory and automatically
* renames files to match their class names, @rsx_id, or <Define:> tags
* according to RSX framework conventions.
*
* Only runs if auto_rename_files is set to true in config/rsx.php or rsx/resource/config/rsx.php
* (user config takes precedence over framework config).
* Files containing @FILENAME-CONVENTION-EXCEPTION are skipped.
*/
export class AutoRenameProvider {
private config_enabled: boolean = false;
private workspace_root: string = '';
private is_checking = false;
constructor() {
this.init();
}
private find_rspade_root(): string | undefined {
const workspace_folders = vscode.workspace.workspaceFolders;
if (!workspace_folders) {
return undefined;
}
// Check each workspace folder for app/RSpade/
for (const folder of workspace_folders) {
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
private async init() {
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
return;
}
this.workspace_root = rspade_root;
await this.load_config();
}
private async load_config() {
const framework_config_path = path.join(this.workspace_root, 'config', 'rsx.php');
const user_config_path = path.join(this.workspace_root, 'rsx', 'resource', 'config', 'rsx.php');
let framework_enabled = false;
let user_enabled: boolean | null = null;
// Load framework config
if (fs.existsSync(framework_config_path)) {
try {
const content = fs.readFileSync(framework_config_path, 'utf8');
const value = this.extract_auto_rename_value(content);
if (value !== null) {
framework_enabled = value;
console.log('[AutoRename] Framework config - auto_rename_files:', framework_enabled);
}
} catch (error) {
console.error('[AutoRename] Failed to load framework config:', error);
}
} else {
console.log('[AutoRename] Framework config file not found:', framework_config_path);
}
// Load user config (takes precedence)
if (fs.existsSync(user_config_path)) {
try {
const content = fs.readFileSync(user_config_path, 'utf8');
const value = this.extract_auto_rename_value(content);
if (value !== null) {
user_enabled = value;
console.log('[AutoRename] User config - auto_rename_files:', user_enabled);
}
} catch (error) {
console.error('[AutoRename] Failed to load user config:', error);
}
} else {
console.log('[AutoRename] User config file not found:', user_config_path);
}
// User config takes precedence over framework config
this.config_enabled = user_enabled !== null ? user_enabled : framework_enabled;
console.log('[AutoRename] Final config - auto_rename_files:', this.config_enabled);
}
private extract_auto_rename_value(content: string): boolean | null {
// Look for development.auto_rename_files setting
// Match pattern: 'development' => [ ... 'auto_rename_files' => true/false ... ]
const development_section_match = content.match(/'development'\s*=>\s*\[([\s\S]*?)\],\s*\/\*/);
if (development_section_match) {
const development_content = development_section_match[1];
const auto_rename_match = development_content.match(/'auto_rename_files'\s*=>\s*(true|false)/);
if (auto_rename_match) {
return auto_rename_match[1] === 'true';
}
}
return null;
}
activate(context: vscode.ExtensionContext) {
console.log('[AutoRename] Provider activated');
// Watch for completed file saves (not before save)
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument(async (document) => {
if (this.is_checking) {
console.log('[AutoRename] Already checking, skipping');
return; // Prevent recursive calls
}
// Only process if this document is currently active in the editor
const active_editor = vscode.window.activeTextEditor;
if (!active_editor || active_editor.document !== document) {
console.log('[AutoRename] Document not active, skipping (likely bulk save/replace)');
return;
}
await this.load_config(); // Reload config on each save
if (!this.config_enabled) {
console.log('[AutoRename] Feature disabled in config');
return;
}
const file_path = document.uri.fsPath;
console.log('[AutoRename] File saved:', file_path);
// Only process files in ./rsx directory
const relative_path = path.relative(this.workspace_root, file_path);
console.log('[AutoRename] Relative path:', relative_path);
if (!relative_path.startsWith('rsx/') && !relative_path.startsWith('rsx\\')) {
console.log('[AutoRename] Not in rsx/ directory, skipping');
return;
}
// Check for exception marker
const content = document.getText();
if (content.includes('@FILENAME-CONVENTION-EXCEPTION')) {
console.log('[AutoRename] File contains @FILENAME-CONVENTION-EXCEPTION, skipping');
return;
}
this.is_checking = true;
try {
console.log('[AutoRename] Starting rename check...');
await this.check_and_rename(document);
} finally {
this.is_checking = false;
}
})
);
}
public async check_and_rename(document: vscode.TextDocument) {
const file_path = document.uri.fsPath;
const extension = this.get_extension(file_path);
const content = document.getText();
console.log('[AutoRename] Extension detected:', extension);
let identifier: string | null = null;
let suggested_filename: string | null = null;
// Determine identifier based on file type
if (extension === 'php') {
identifier = this.extract_php_class(content);
console.log('[AutoRename] PHP class extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_php_filename(file_path, identifier);
}
} else if (extension === 'js') {
identifier = this.extract_js_class(content);
console.log('[AutoRename] JS class extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_js_filename(file_path, identifier, content);
}
} else if (extension === 'blade.php') {
identifier = this.extract_rsx_id(content);
console.log('[AutoRename] Blade @rsx_id extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_blade_filename(file_path, identifier);
}
} else if (extension === 'jqhtml') {
identifier = this.extract_jqhtml_component(content);
console.log('[AutoRename] Jqhtml component extracted:', identifier);
if (identifier) {
suggested_filename = await this.get_suggested_jqhtml_filename(file_path, identifier);
}
}
console.log('[AutoRename] Identifier:', identifier);
console.log('[AutoRename] Suggested filename:', suggested_filename);
if (!identifier || !suggested_filename) {
console.log('[AutoRename] No identifier or suggested filename, skipping');
return;
}
const current_filename = path.basename(file_path);
console.log('[AutoRename] Current filename:', current_filename);
if (current_filename.toLowerCase() === suggested_filename.toLowerCase()) {
console.log('[AutoRename] Filename already correct (case-insensitive match)');
return; // Already correct
}
// Check if suggested filename already exists
const dir = path.dirname(file_path);
const new_path = path.join(dir, suggested_filename);
if (fs.existsSync(new_path)) {
console.log('[AutoRename] Cannot rename: ${suggested_filename} already exists at', new_path);
return;
}
// Perform rename
console.log('[AutoRename] Performing rename to:', suggested_filename);
await this.rename_file(file_path, new_path);
}
private async rename_file(old_path: string, new_path: string) {
const old_uri = vscode.Uri.file(old_path);
const new_uri = vscode.Uri.file(new_path);
console.log('[AutoRename] Renaming from:', old_path);
console.log('[AutoRename] Renaming to:', new_path);
try {
// Capture cursor position and view column before closing
let cursor_position: vscode.Position | undefined;
let view_column: vscode.ViewColumn | undefined;
const old_doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === old_path);
if (old_doc) {
// Find the editor for this document to capture cursor position
const old_editor = vscode.window.visibleTextEditors.find(
editor => editor.document.uri.fsPath === old_path
);
if (old_editor) {
cursor_position = old_editor.selection.active;
view_column = old_editor.viewColumn;
console.log('[AutoRename] Captured cursor position:', cursor_position.line, cursor_position.character);
console.log('[AutoRename] Captured view column:', view_column);
}
console.log('[AutoRename] Closing old document');
await vscode.window.showTextDocument(old_doc, { preview: false, preserveFocus: false });
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
}
// Use VS Code's rename API for proper integration
const edit = new vscode.WorkspaceEdit();
edit.renameFile(old_uri, new_uri, { overwrite: false });
const success = await vscode.workspace.applyEdit(edit);
console.log('[AutoRename] Rename result:', success);
if (success) {
console.log(`[AutoRename] ✅ Auto-renamed: ${path.basename(old_path)}${path.basename(new_path)}`);
// Wait a bit for the file system to settle
await new Promise(resolve => setTimeout(resolve, 100));
// Open the renamed file
const new_document = await vscode.workspace.openTextDocument(new_uri);
console.log('[AutoRename] Opened renamed document');
// Show the document in the editor, restoring view column if we had one
const show_options: vscode.TextDocumentShowOptions = {
preview: false,
viewColumn: view_column
};
const editor = await vscode.window.showTextDocument(new_document, show_options);
console.log('[AutoRename] Showing renamed document in editor');
// Restore cursor position if we captured one
if (cursor_position) {
editor.selection = new vscode.Selection(cursor_position, cursor_position);
editor.revealRange(new vscode.Range(cursor_position, cursor_position));
console.log('[AutoRename] Restored cursor position');
}
// Format the document
console.log('[AutoRename] Formatting document...');
await vscode.commands.executeCommand('editor.action.formatDocument');
console.log('[AutoRename] Format command executed');
// Save the formatted document
await new_document.save();
console.log('[AutoRename] Saved formatted document');
}
} catch (error) {
console.error('[AutoRename] ❌ Failed to rename file:', error);
}
}
private get_extension(file_path: string): string {
if (file_path.endsWith('.blade.php')) {
return 'blade.php';
}
if (file_path.endsWith('.jqhtml')) {
return 'jqhtml';
}
return path.extname(file_path).substring(1);
}
private extract_php_class(content: string): string | null {
// Match: class ClassName
const match = content.match(/^\s*class\s+([A-Za-z0-9_]+)/m);
return match ? match[1] : null;
}
private extract_js_class(content: string): string | null {
// Match: class ClassName
const match = content.match(/^\s*class\s+([A-Za-z0-9_]+)/m);
return match ? match[1] : null;
}
private extract_rsx_id(content: string): string | null {
// Match: @rsx_id('identifier')
const match = content.match(/@rsx_id\s*\(\s*['"]([^'"]+)['"]\s*\)/);
return match ? match[1] : null;
}
private extract_jqhtml_component(content: string): string | null {
// Match: <Define:ComponentName>
const match = content.match(/<Define:([A-Za-z0-9_]+)>/);
return match ? match[1] : null;
}
private async get_suggested_php_filename(file_path: string, class_name: string): Promise<string> {
// rsx/ files use lowercase convention
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
const short_name = this.extract_short_name(class_name, relative_dir);
if (short_name) {
return short_name.toLowerCase() + '.php';
}
return class_name.toLowerCase() + '.php';
}
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/);
console.log('[AutoRename] JS - Is Jqhtml_Component:', is_jqhtml);
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
console.log('[AutoRename] JS - Relative directory:', relative_dir);
if (is_jqhtml) {
// For Jqhtml components, use snake_case convention
const snake_case = this.pascal_to_snake_case(class_name);
console.log('[AutoRename] JS - PascalCase to snake_case:', class_name, '→', snake_case);
const short_name = this.extract_short_name(class_name, relative_dir);
console.log('[AutoRename] JS - Short name extracted:', short_name);
if (short_name) {
const short_snake = this.pascal_to_snake_case(short_name);
console.log('[AutoRename] JS - Short name to snake_case:', short_name, '→', short_snake);
return short_snake.toLowerCase() + '.js';
}
return snake_case.toLowerCase() + '.js';
} else {
// Regular JS classes use lowercase
const short_name = this.extract_short_name(class_name, relative_dir);
console.log('[AutoRename] JS - Short name extracted:', short_name);
if (short_name) {
return short_name.toLowerCase() + '.js';
}
return class_name.toLowerCase() + '.js';
}
}
private async get_suggested_blade_filename(file_path: string, rsx_id: string): Promise<string> {
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
const short_name = this.extract_short_name(rsx_id, relative_dir);
if (short_name) {
return short_name.toLowerCase() + '.blade.php';
}
return rsx_id.toLowerCase() + '.blade.php';
}
private async get_suggested_jqhtml_filename(file_path: string, component_name: string): Promise<string> {
// Jqhtml components use snake_case convention
const snake_case = this.pascal_to_snake_case(component_name);
console.log('[AutoRename] JQHTML - PascalCase to snake_case:', component_name, '→', snake_case);
const dir = path.dirname(file_path);
const relative_dir = path.relative(this.workspace_root, dir);
console.log('[AutoRename] JQHTML - Relative directory:', relative_dir);
const short_name = this.extract_short_name(component_name, relative_dir);
console.log('[AutoRename] JQHTML - Short name extracted:', short_name);
if (short_name) {
const short_snake = this.pascal_to_snake_case(short_name);
console.log('[AutoRename] JQHTML - Short name to snake_case:', short_name, '→', short_snake);
return short_snake.toLowerCase() + '.jqhtml';
}
return snake_case.toLowerCase() + '.jqhtml';
}
/**
* Convert PascalCase to snake_case
* Inserts underscores before uppercase letters and before first digit in number sequences
* Example: TestComponent1 -> Test_Component_1
*/
private pascal_to_snake_case(name: string): string {
// Insert underscore before uppercase letters (except first character)
let result = name.replace(/(?<!^)([A-Z])/g, '_$1');
// Insert underscore before first digit in a run of digits
result = result.replace(/(?<!^)(?<![0-9])([0-9])/g, '_$1');
// Replace multiple consecutive underscores with single underscore
result = result.replace(/_+/g, '_');
return result;
}
/**
* Extract short name from class/id if directory structure matches prefix
* Returns null if directory structure doesn't match or if short name rules aren't met
*
* Rules:
* - Short names only allowed in ./rsx directory (NOT in /app/RSpade)
* - Original name must have 3+ segments for short name to be allowed
* - Short name must have 2+ segments
*/
private extract_short_name(full_name: string, dir_path: string): string | null {
// Short names only allowed in ./rsx directory, not in framework code (/app/RSpade)
if (dir_path.includes('/app/RSpade') || dir_path.includes('\\app\\RSpade')) {
return null;
}
// Split the full name by underscores
const name_parts = full_name.split('_');
const original_segment_count = name_parts.length;
// If original name has exactly 2 segments, short name is NOT allowed
if (original_segment_count === 2) {
return null;
}
// If only 1 segment, no prefix to match
if (original_segment_count === 1) {
return null;
}
// Split directory path into parts (handle both / and \ separators)
const dir_parts = dir_path.split(/[/\\]/).filter(p => p.length > 0);
// Find the maximum number of consecutive matches between end of dir_parts and start of name_parts
let matched_parts = 0;
const max_possible = Math.min(dir_parts.length, name_parts.length - 1);
// Try to match last N dir parts with first N name parts
for (let num_to_check = max_possible; num_to_check > 0; num_to_check--) {
let all_match = true;
for (let i = 0; i < num_to_check; i++) {
const dir_idx = dir_parts.length - num_to_check + i;
if (dir_parts[dir_idx].toLowerCase() !== name_parts[i].toLowerCase()) {
all_match = false;
break;
}
}
if (all_match) {
matched_parts = num_to_check;
break;
}
}
if (matched_parts === 0) {
return null; // No match
}
// Calculate the short name
const short_parts = name_parts.slice(matched_parts);
const short_segment_count = short_parts.length;
// Validate short name segment count
// Short name must have 2+ segments
if (short_segment_count < 2) {
return null; // Short name would be too short
}
return short_parts.join('_');
}
dispose() {
// Cleanup if needed
}
}

View File

@@ -0,0 +1,58 @@
import * as vscode from 'vscode';
export const init_blade_language_config = () => {
// HTML empty elements that don't require closing tags
const EMPTY_ELEMENTS: string[] = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr',
];
// Configure Blade language indentation and auto-closing behavior
vscode.languages.setLanguageConfiguration('blade', {
indentationRules: {
increaseIndentPattern:
/<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
decreaseIndentPattern:
/^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/,
},
wordPattern:
/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
onEnterRules: [
{
// When pressing Enter between opening and closing tags, auto-indent
beforeText: new RegExp(
`<(?!(?:${EMPTY_ELEMENTS.join(
'|'
)}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`,
'i'
),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: vscode.IndentAction.IndentOutdent },
},
{
// When pressing Enter after opening tag, auto-indent
beforeText: new RegExp(
`<(?!(?:${EMPTY_ELEMENTS.join(
'|'
)}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,
'i'
),
action: { indentAction: vscode.IndentAction.Indent },
},
],
});
};

View File

@@ -0,0 +1,66 @@
import * as vscode from 'vscode';
/**
* Provides semantic tokens for uppercase component tags in Blade files
* Highlights component tag names in light green
* Highlights tag="" attribute in orange on jqhtml components
*/
export class BladeComponentSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'blade') {
return tokens_builder.build();
}
const text = document.getText();
// Match opening tags that start with uppercase letter to find jqhtml components
// Matches: <ComponentName ...>, captures the entire tag up to >
const component_tag_regex = /<([A-Z][a-zA-Z0-9_]*)([^>]*?)>/g;
let component_match;
while ((component_match = component_tag_regex.exec(text)) !== null) {
const tag_name = component_match[1];
const tag_attributes = component_match[2];
const tag_start = component_match.index + component_match[0].indexOf(tag_name);
const tag_position = document.positionAt(tag_start);
// Push token for the component tag name
// Token type 0 maps to 'class' which VS Code themes style as entity.name.class (turquoise/cyan)
tokens_builder.push(tag_position.line, tag_position.character, tag_name.length, 0, 0);
// Now look for tag="" attribute within this component's attributes
// Matches: tag="..." or tag='...'
const tag_attr_regex = /\btag\s*=/g;
let attr_match;
while ((attr_match = tag_attr_regex.exec(tag_attributes)) !== null) {
// Calculate the position of 'tag' within the document
const attr_start = component_match.index + component_match[0].indexOf(tag_attributes) + attr_match.index;
const attr_position = document.positionAt(attr_start);
// Push token for 'tag' attribute name
// Token type 1 maps to 'jqhtmlTagAttribute' which we'll define to be orange
tokens_builder.push(attr_position.line, attr_position.character, 3, 1, 0);
}
}
// Also match closing tags that start with uppercase letter
// Matches: </ComponentName>
const closing_tag_regex = /<\/([A-Z][a-zA-Z0-9_]*)/g;
let closing_match;
while ((closing_match = closing_tag_regex.exec(text)) !== null) {
const tag_name = closing_match[1];
const tag_start = closing_match.index + closing_match[0].indexOf(tag_name);
const position = document.positionAt(tag_start);
// Push token for the tag name
// Token type 0 maps to 'class' which VS Code themes style as entity.name.class (turquoise/cyan)
tokens_builder.push(position.line, position.character, tag_name.length, 0, 0);
}
return tokens_builder.build();
}
}

View File

@@ -0,0 +1,122 @@
import * as vscode from 'vscode';
import { get_config } from './config';
const TAG_DOUBLE = 0;
const TAG_UNESCAPED = 1;
const TAG_COMMENT = 2;
const snippets: Record<number, string> = {
[TAG_DOUBLE]: '{{ ${1:${TM_SELECTED_TEXT/[{}]//g}} }}$0',
[TAG_UNESCAPED]: '{!! ${1:${TM_SELECTED_TEXT/[{} !]//g}} !!}$0',
[TAG_COMMENT]: '{{-- ${1:${TM_SELECTED_TEXT/(--)|[{} ]//g}} --}}$0',
};
const triggers = ['{}', '!', '-', '{'];
const regexes = [
/({{(?!\s|-))(.*?)(}})/,
/({!!(?!\s))(.*?)?(}?)/,
/({{[\s]?--)(.*?)?(}})/,
];
const translate = (position: vscode.Position, offset: number): vscode.Position => {
try {
return position.translate(0, offset);
} catch (error) {
// VS Code doesn't like negative numbers passed
// to translate (even though it works fine), so
// this block prevents debug console errors
}
return position;
};
const chars_for_change = (
doc: vscode.TextDocument,
change: vscode.TextDocumentContentChangeEvent
): number => {
if (change.text === '!') {
return 2;
}
if (change.text !== '-') {
return 1;
}
const start = translate(change.range.start, -2);
const end = translate(change.range.start, -1);
return doc.getText(new vscode.Range(start, end)) === ' ' ? 4 : 3;
};
export const blade_spacer = async (
e: vscode.TextDocumentChangeEvent,
editor?: vscode.TextEditor
) => {
const config = get_config();
if (
!config.get('enableBladeAutoSpacing', true) ||
!editor ||
editor.document.fileName.indexOf('.blade.php') === -1
) {
return;
}
let tag_type: number = -1;
let ranges: vscode.Range[] = [];
let offsets: number[] = [];
// Changes (per line) come in right-to-left when we need them left-to-right
const changes = e.contentChanges.slice().reverse();
changes.forEach((change) => {
if (triggers.indexOf(change.text) === -1) {
return;
}
if (!offsets[change.range.start.line]) {
offsets[change.range.start.line] = 0;
}
const start_offset =
offsets[change.range.start.line] -
chars_for_change(e.document, change);
const start = translate(change.range.start, start_offset);
const line_end = e.document.lineAt(start.line).range.end;
for (let i = 0; i < regexes.length; i++) {
// If we typed a - or a !, don't consider the "double" tag type
if (i === TAG_DOUBLE && ['-', '!'].indexOf(change.text) !== -1) {
continue;
}
// Only look at unescaped tags if we need to
if (i === TAG_UNESCAPED && change.text !== '!') {
continue;
}
// Only look at comment tags if we need to
if (i === TAG_COMMENT && change.text !== '-') {
continue;
}
const tag = regexes[i].exec(
e.document.getText(new vscode.Range(start, line_end))
);
if (tag) {
tag_type = i;
ranges.push(
new vscode.Range(start, start.translate(0, tag[0].length))
);
offsets[start.line] += tag[1].length;
}
}
});
if (ranges.length > 0 && snippets[tag_type]) {
editor.insertSnippet(new vscode.SnippetString(snippets[tag_type]), ranges);
}
};

View File

@@ -0,0 +1,59 @@
/**
* RSpade Class Refactor Code Actions Provider
*
* Provides refactoring actions that appear in the "Refactor..." menu
* when the cursor is on a class definition line.
*/
import * as vscode from 'vscode';
import { RspadeClassRefactorProvider } from './class_refactor_provider';
export class RspadeClassRefactorCodeActionsProvider implements vscode.CodeActionProvider {
private refactor_provider: RspadeClassRefactorProvider;
constructor(refactor_provider: RspadeClassRefactorProvider) {
this.refactor_provider = refactor_provider;
}
public provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): vscode.CodeAction[] | undefined {
// Only provide actions for PHP files in ./rsx or ./app/RSpade
if (document.languageId !== 'php') {
return undefined;
}
const file_path = document.uri.fsPath;
if (!file_path.includes('/rsx/') && !file_path.includes('\\rsx\\') &&
!file_path.includes('/app/RSpade') && !file_path.includes('\\app\\RSpade')) {
return undefined;
}
// Check if line contains a class definition at indent level 0
const position = range.start;
const line = document.lineAt(position.line).text;
// Must be class definition at start of line (indent level 0)
const class_definition_match = line.match(/^(?:abstract\s+|final\s+)?class\s+([A-Z][a-zA-Z0-9_]*)/);
if (class_definition_match) {
return this.create_refactor_actions();
}
return undefined;
}
private create_refactor_actions(): vscode.CodeAction[] {
const action = new vscode.CodeAction(
'Global Rename Class',
vscode.CodeActionKind.Refactor
);
action.command = {
command: 'rspade.refactorClass',
title: 'Global Rename Class'
};
return [action];
}
}

View File

@@ -0,0 +1,272 @@
/**
* RSpade Class Refactor Provider
*
* Provides context menu refactoring options for PHP class definitions.
* Communicates with the server-side refactor commands via the IDE service.
*/
import * as vscode from 'vscode';
import { RspadeFormattingProvider } from './formatting_provider';
import { AutoRenameProvider } from './auto_rename_provider';
export class RspadeClassRefactorProvider {
private formatting_provider: RspadeFormattingProvider;
private auto_rename_provider: AutoRenameProvider;
private output_channel: vscode.OutputChannel;
constructor(formatting_provider: RspadeFormattingProvider, auto_rename_provider: AutoRenameProvider) {
this.formatting_provider = formatting_provider;
this.auto_rename_provider = auto_rename_provider;
this.output_channel = vscode.window.createOutputChannel('RSpade Refactor');
}
/**
* Register the refactor command
*/
public register(context: vscode.ExtensionContext): void {
const command = vscode.commands.registerCommand(
'rspade.refactorClass',
async () => await this.refactor_class()
);
context.subscriptions.push(command);
}
/**
* Main refactor method
*/
private async refactor_class(): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
const document = editor.document;
const position = editor.selection.active;
this.output_channel.clear();
this.output_channel.show(true);
this.output_channel.appendLine('=== RSpade Class Refactor ===\n');
try {
// Extract class name from cursor position
const class_info = await this.extract_class_info(document, position);
if (!class_info) {
vscode.window.showErrorMessage('Could not identify class at cursor position');
return;
}
this.output_channel.appendLine(`Class: ${class_info.class_name}\n`);
// Show input dialog for new class name
const new_class_name = await vscode.window.showInputBox({
title: `Global Rename Class: ${class_info.class_name}`,
prompt: 'Enter new class name:',
placeHolder: 'New_Class_Name',
value: class_info.class_name,
ignoreFocusOut: true,
validateInput: (value: string) => {
if (!value) {
return 'Class name cannot be empty';
}
if (!/^[A-Z][a-zA-Z0-9_]*$/.test(value)) {
return 'Class name must be PascalCase (uppercase first letter)';
}
if (value === class_info.class_name) {
return 'New class name must be different from current name';
}
return null;
}
});
if (!new_class_name) {
this.output_channel.appendLine('Refactor cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Confirm refactoring
const confirmation = await vscode.window.showWarningMessage(
`Global Rename: ${class_info.class_name}${new_class_name}\n\n` +
'This will rename the class across all usages in all files.',
{ modal: true },
'Rename',
'Cancel'
);
if (confirmation !== 'Rename') {
this.output_channel.appendLine('Global rename cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Save all dirty files first
this.output_channel.appendLine('Checking for unsaved files...');
const dirty_documents = vscode.workspace.textDocuments.filter(doc => doc.isDirty);
if (dirty_documents.length > 0) {
this.output_channel.appendLine(`Found ${dirty_documents.length} unsaved file(s):`);
for (const doc of dirty_documents) {
this.output_channel.appendLine(` - ${doc.fileName}`);
}
this.output_channel.appendLine('\nSaving all files...');
const save_result = await vscode.workspace.saveAll(false);
if (!save_result) {
const error_msg = 'Failed to save all files. Refactor operation aborted.';
this.output_channel.appendLine(`\nERROR: ${error_msg}`);
vscode.window.showErrorMessage(error_msg);
return;
}
this.output_channel.appendLine('All files saved successfully\n');
} else {
this.output_channel.appendLine('No unsaved files\n');
}
// Show output channel
this.output_channel.show(true);
// Show terminal and execute refactoring
this.output_channel.appendLine(`Refactoring ${class_info.class_name} to ${new_class_name}...`);
this.output_channel.appendLine('');
const result = await this.execute_refactor(
class_info.class_name,
new_class_name
);
// Display result in terminal
this.output_channel.appendLine('\n=== Refactor Output ===\n');
this.output_channel.appendLine(result);
this.output_channel.appendLine('\n=== Refactor Complete ===');
// Check if refactor was successful
if (result.includes('=== Refactor Complete ===') || result.trim().length > 0) {
// Wait 3.5 seconds total (2s + 1.5s), hide panel, then reload files and auto-rename
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 500ms for filesystem changes to propagate (Samba/network issues)
setTimeout(async () => {
await this.reload_all_open_files();
// Wait another 500ms then check if current file needs renaming
setTimeout(async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
const file_path = editor.document.uri.fsPath;
// Only auto-rename if file is in ./rsx
if (file_path.includes('/rsx/') || file_path.includes('\\rsx\\')) {
await this.auto_rename_provider.check_and_rename(editor.document);
}
}
}, 500);
}, 500);
}, 3500);
vscode.window.showInformationMessage(
`Successfully refactored ${class_info.class_name} to ${new_class_name}`
);
}
} catch (error: any) {
const error_message = error.message || String(error);
this.output_channel.appendLine(`\nERROR: ${error_message}`);
vscode.window.showErrorMessage(`Refactor failed: ${error_message}`);
}
}
/**
* Extract class name from cursor position
*/
private async extract_class_info(
document: vscode.TextDocument,
position: vscode.Position
): Promise<{ class_name: string } | null> {
const line = document.lineAt(position.line).text;
// Check for class definition at indent level 0: class ClassName or class ClassName extends Parent
// Must be at start of line (indent level 0)
const class_match = line.match(/^(?:abstract\s+|final\s+)?class\s+([A-Z][a-zA-Z0-9_]*)/);
if (class_match) {
const class_name = class_match[1];
return { class_name };
}
return null;
}
/**
* Reload all open text documents
*/
private async reload_all_open_files(): Promise<void> {
const text_documents = vscode.workspace.textDocuments;
for (const document of text_documents) {
// Skip untitled documents
if (document.uri.scheme === 'untitled') {
continue;
}
// Skip non-file schemes (git, output channels, etc)
if (document.uri.scheme !== 'file') {
continue;
}
// Get the text editor for this document
const editors = vscode.window.visibleTextEditors.filter(
editor => editor.document.uri.toString() === document.uri.toString()
);
if (editors.length > 0) {
// Document is currently visible, reload it
const position = editors[0].selection.active;
const view_column = editors[0].viewColumn;
// Close and reopen to force reload
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
const new_document = await vscode.workspace.openTextDocument(document.uri);
const editor = await vscode.window.showTextDocument(new_document, view_column);
// Restore cursor position
editor.selection = new vscode.Selection(position, position);
editor.revealRange(new vscode.Range(position, position));
}
}
}
/**
* Execute the refactor command via IDE service
*/
private async execute_refactor(
old_class: string,
new_class: string
): Promise<string> {
// Ensure authentication
await this.formatting_provider.ensure_auth();
// Prepare request data
const request_data = {
command: 'rsx:refactor:rename_php_class',
arguments: [old_class, new_class, '--skip-rename-file']
};
this.output_channel.appendLine('Sending refactor request to server...');
this.output_channel.appendLine(`Command: ${request_data.command}`);
this.output_channel.appendLine(`Arguments: ${JSON.stringify(request_data.arguments)}\n`);
// Make authenticated request
const response = await (this.formatting_provider as any).make_authenticated_request(
'/command',
request_data
);
if (!response.success) {
throw new Error(response.error || 'Refactor command failed');
}
return response.output || 'Refactor completed successfully (no output)';
}
}

View File

@@ -0,0 +1,15 @@
import * as vscode from 'vscode';
export function get_config() {
return vscode.workspace.getConfiguration('rspade');
}
export function get_python_command(): string {
const custom_path = get_config().get<string>('pythonPath');
if (custom_path && custom_path.trim() !== '') {
return custom_path;
}
// Default based on platform
return process.platform === 'win32' ? 'python' : 'python3';
}

View File

@@ -0,0 +1,257 @@
import * as vscode from 'vscode';
/**
* Convention methods that are called automatically by the RSX framework
* These methods are invoked by name at runtime, not through direct references
*/
const CONVENTION_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready',
'on_jqhtml_ready'
];
/**
* Check if a method is static by examining the line text
*/
function is_static_method(line_text: string): boolean {
return line_text.trim().startsWith('static ');
}
/**
* Check if position is inside a comment
*/
function is_in_comment(document: vscode.TextDocument, position: vscode.Position): boolean {
const line_text = document.lineAt(position.line).text;
const char_pos = position.character;
// Check for single-line comment
const single_comment_idx = line_text.indexOf('//');
if (single_comment_idx !== -1 && single_comment_idx < char_pos) {
return true;
}
// Check for multi-line comment by looking at text before position
const text_before = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
let in_block_comment = false;
let i = 0;
while (i < text_before.length) {
if (text_before.substring(i, i + 2) === '/*') {
in_block_comment = true;
i += 2;
} else if (text_before.substring(i, i + 2) === '*/') {
in_block_comment = false;
i += 2;
} else {
i++;
}
}
return in_block_comment;
}
/**
* Provides semantic tokens for convention methods (amber color)
*/
export class ConventionMethodSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return tokens_builder.build();
}
const text = document.getText();
// Find all static method definitions matching convention methods
for (const method_name of CONVENTION_METHODS) {
// Match: static method_name(...)
const regex = new RegExp(`\\bstatic\\s+(${method_name})\\s*\\(`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
const method_start = match.index + 'static '.length;
const position = document.positionAt(method_start);
// Skip if this is inside a comment
if (is_in_comment(document, position)) {
continue;
}
tokens_builder.push(
position.line,
position.character,
method_name.length,
0, // token type index for 'conventionMethod'
0 // token modifiers
);
}
}
return tokens_builder.build();
}
}
/**
* Provides hover information for convention methods
*/
export class ConventionMethodHoverProvider implements vscode.HoverProvider {
provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.Hover | undefined {
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return undefined;
}
const word = document.getText(word_range);
if (!CONVENTION_METHODS.includes(word)) {
return undefined;
}
// Check if this is a method definition
const line = document.lineAt(position.line).text;
if (!line.includes('(')) {
return undefined;
}
const markdown = new vscode.MarkdownString();
markdown.isTrusted = true;
if (is_static_method(line)) {
markdown.appendMarkdown(`**Convention Method**\n\n`);
markdown.appendMarkdown(`This method is automatically called by \`Rsx.js\` during initialization of the client-side RSpade runtime.\n\n`);
markdown.appendMarkdown(`Convention methods are invoked by name and do not appear as direct references in the codebase.`);
} else {
markdown.appendMarkdown(`**⚠️ Non-Static Convention Method**\n\n`);
markdown.appendMarkdown(`This method name is reserved for framework convention methods, but it is not declared as \`static\`.\n\n`);
markdown.appendMarkdown(`Convention methods must be \`static\` to be called by the RSX framework.`);
}
return new vscode.Hover(markdown, word_range);
}
}
/**
* Provides diagnostics for non-static convention methods
*/
export class ConventionMethodDiagnosticProvider {
private diagnostics_collection: vscode.DiagnosticCollection;
constructor() {
this.diagnostics_collection = vscode.languages.createDiagnosticCollection('rspade-convention');
}
activate(context: vscode.ExtensionContext) {
// Update diagnostics on document change
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
this.update_diagnostics(event.document);
})
);
// Update diagnostics for all open documents
vscode.workspace.textDocuments.forEach(document => {
this.update_diagnostics(document);
});
// Update diagnostics when opening a document
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument(document => {
this.update_diagnostics(document);
})
);
}
private update_diagnostics(document: vscode.TextDocument) {
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return;
}
const diagnostics: vscode.Diagnostic[] = [];
const text = document.getText();
for (const method_name of CONVENTION_METHODS) {
// Match non-static methods with convention names: method_name(...) without 'static' before it
const regex = new RegExp(`^\\s*(?!static\\s)(${method_name})\\s*\\(`, 'gm');
let match;
while ((match = regex.exec(text)) !== null) {
const method_start = match.index + match[0].indexOf(method_name);
const start_pos = document.positionAt(method_start);
const end_pos = document.positionAt(method_start + method_name.length);
const range = new vscode.Range(start_pos, end_pos);
const diagnostic = new vscode.Diagnostic(
range,
`Convention method '${method_name}' must be declared as 'static' to be called by the RSX framework`,
vscode.DiagnosticSeverity.Error
);
diagnostic.source = 'RSpade';
diagnostics.push(diagnostic);
}
}
this.diagnostics_collection.set(document.uri, diagnostics);
}
dispose() {
this.diagnostics_collection.dispose();
}
}
/**
* Provides go-to-definition for convention methods
*/
export class ConventionMethodDefinitionProvider implements vscode.DefinitionProvider {
async provideDefinition(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Location | undefined> {
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return undefined;
}
const word = document.getText(word_range);
if (!CONVENTION_METHODS.includes(word)) {
return undefined;
}
// Check if this is a method definition
const line = document.lineAt(position.line).text;
if (!line.includes('(') || !is_static_method(line)) {
return undefined;
}
// Find Rsx.js in the workspace
const files = await vscode.workspace.findFiles('**/Rsx.js', '**/node_modules/**');
if (files.length === 0) {
return undefined;
}
// Use the first match (should only be one Rsx.js)
const rsx_file = files[0];
const rsx_document = await vscode.workspace.openTextDocument(rsx_file);
const rsx_text = rsx_document.getText();
// Find _rsx_core_boot method
const boot_regex = /static\s+async\s+_rsx_core_boot\s*\(/;
const match = boot_regex.exec(rsx_text);
if (!match) {
return undefined;
}
const boot_position = rsx_document.positionAt(match.index);
return new vscode.Location(rsx_file, boot_position);
}
}

View File

@@ -0,0 +1,233 @@
import * as vscode from 'vscode';
import * as crypto from 'crypto';
import { RspadeFormattingProvider } from './formatting_provider';
// Use the global WebSocket available in VS Code extension host
declare const WebSocket: any;
interface WebSocketMessage {
type: string;
data?: any;
timestamp?: number;
}
export class DebugClient {
private ws: any = null; // WebSocket instance
private outputChannel: vscode.OutputChannel;
private formattingProvider: RspadeFormattingProvider;
private isConnecting: boolean = false;
private reconnectTimer: NodeJS.Timer | null = null;
private pingTimer: NodeJS.Timer | null = null;
private sessionId: string | null = null;
private serverKey: string | null = null;
constructor(formattingProvider: RspadeFormattingProvider) {
this.formattingProvider = formattingProvider;
this.outputChannel = vscode.window.createOutputChannel('RSPade Debug Proxy');
this.outputChannel.show();
this.log('Debug client initialized');
}
public async start(): Promise<void> {
this.log('Starting debug client...');
await this.connect();
}
private async connect(): Promise<void> {
if (this.isConnecting || this.ws?.readyState === WebSocket.OPEN) {
return;
}
this.isConnecting = true;
try {
// Get authentication from formatting provider
await this.ensureAuthenticated();
const serverUrl = await this.formattingProvider.get_server_url();
if (!serverUrl) {
throw new Error('No server URL configured');
}
// Parse URL and construct WebSocket URL
const url = new URL(serverUrl);
const wsProtocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${url.host}/_ide/debug/ws`;
this.log(`Connecting to WebSocket: ${wsUrl}`);
// Create WebSocket (standard API doesn't support headers in constructor)
// We'll send auth after connection
this.ws = new WebSocket(wsUrl);
this.setupEventHandlers();
} catch (error: any) {
this.log(`Connection failed: ${error.message}`);
this.isConnecting = false;
this.scheduleReconnect();
}
}
private setupEventHandlers(): void {
if (!this.ws) return;
this.ws.onopen = () => {
this.isConnecting = false;
this.log('WebSocket connected, sending authentication...');
// Send authentication as first message
const signature = crypto
.createHmac('sha256', this.serverKey!)
.update(this.sessionId!)
.digest('hex');
this.sendMessage({
type: 'auth',
data: {
sessionId: this.sessionId,
signature: signature
}
});
// Send initial hello message after auth
setTimeout(() => {
this.sendMessage({
type: 'hello',
data: { name: 'VS Code Debug Client' }
});
// Start ping timer
this.startPingTimer();
}, 100);
};
this.ws.onmessage = (event: any) => {
try {
const message = JSON.parse(event.data) as WebSocketMessage;
this.handleMessage(message);
} catch (error) {
this.log(`Failed to parse message: ${error}`);
}
};
this.ws.onclose = () => {
this.log('WebSocket disconnected');
this.ws = null;
this.stopPingTimer();
this.scheduleReconnect();
};
this.ws.onerror = (error: any) => {
this.log(`WebSocket error: ${error}`);
};
}
private handleMessage(message: WebSocketMessage): void {
this.log(`Received: ${message.type}`, message.data);
switch (message.type) {
case 'welcome':
this.log('✅ Authentication successful! Connected to debug proxy');
this.log(`Session ID: ${message.data?.sessionId}`);
break;
case 'pong':
this.log(`PONG received! Server responded to ping`);
break;
case 'hello_response':
this.log(`Server says: ${message.data?.message}`);
break;
case 'error':
this.log(`❌ Error: ${message.data?.message}`);
break;
default:
this.log(`Unknown message type: ${message.type}`);
}
}
private sendMessage(message: WebSocketMessage): void {
if (this.ws?.readyState === 1) { // 1 = OPEN in standard WebSocket API
this.ws.send(JSON.stringify(message));
this.log(`Sent: ${message.type}`, message.data);
}
}
private startPingTimer(): void {
this.stopPingTimer();
// Send ping every 5 seconds
this.pingTimer = setInterval(() => {
this.sendMessage({
type: 'ping',
data: { timestamp: Date.now() }
});
this.log('PING sent to server');
}, 5000);
}
private stopPingTimer(): void {
if (this.pingTimer) {
clearInterval(this.pingTimer);
this.pingTimer = null;
}
}
private scheduleReconnect(): void {
if (this.reconnectTimer) {
return;
}
this.log('Scheduling reconnection in 5 seconds...');
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, 5000);
}
private async ensureAuthenticated(): Promise<void> {
// Get auth data from formatting provider
const authData = await this.formattingProvider.ensure_auth();
if (!authData) {
throw new Error('Failed to authenticate');
}
// Extract session ID and server key
this.sessionId = authData.session_id;
this.serverKey = authData.server_key;
if (!this.sessionId || !this.serverKey) {
throw new Error('Invalid auth data');
}
}
private log(message: string, data?: any): void {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}`;
if (data) {
this.outputChannel.appendLine(`${logMessage}\n${JSON.stringify(data, null, 2)}`);
} else {
this.outputChannel.appendLine(logMessage);
}
}
public dispose(): void {
this.stopPingTimer();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.outputChannel.dispose();
}
}

View File

@@ -0,0 +1,134 @@
import * as vscode from 'vscode';
export class RspadeDecorationProvider {
// RSX markers are no longer used - keeping class for potential future use
private static readonly RSX_USE_START = '// [RSX:USE:START]'; // Deprecated
private static readonly RSX_USE_END = '// [RSX:USE:END]'; // Deprecated
private decoration_type: vscode.TextEditorDecorationType;
private decorations = new Map<string, vscode.DecorationOptions[]>();
constructor() {
// Create decoration type for read-only sections
this.decoration_type = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(100, 100, 100, 0.1)', // Subtle gray instead of yellow
borderWidth: '0px', // Remove border
isWholeLine: true,
overviewRulerColor: 'rgba(100, 100, 100, 0.3)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
before: {
contentText: 'ⓘ ', // Info symbol instead of warning
color: 'rgba(100, 150, 200, 0.7)', // Soft blue
margin: '0 4px 0 0'
}
});
}
activate(context: vscode.ExtensionContext) {
// RSX markers are no longer used - this functionality is disabled
return;
/* Original implementation preserved for reference
// Update decorations for active editor
if (vscode.window.activeTextEditor) {
this.update_decorations(vscode.window.activeTextEditor);
}
// Update decorations when active editor changes
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) {
this.update_decorations(editor);
}
})
);
// Update decorations when document changes
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument(event => {
const editor = vscode.window.activeTextEditor;
if (editor && event.document === editor.document) {
this.update_decorations(editor);
}
})
);
// Show warning when trying to edit protected region
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument(event => {
if (event.contentChanges.length === 0) return;
const editor = vscode.window.activeTextEditor;
if (!editor || event.document !== editor.document) return;
for (const change of event.contentChanges) {
if (this.is_in_protected_region(event.document, change.range)) {
vscode.window.showWarningMessage(
'You are editing an auto-generated RSX:USE section. These changes may be overwritten.',
'Understood'
);
break;
}
}
})
);
*/
}
private update_decorations(editor: vscode.TextEditor) {
if (editor.document.languageId !== 'php') return;
const decorations: vscode.DecorationOptions[] = [];
const document = editor.document;
let in_protected_region = false;
let start_line: number | null = null;
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
const text = line.text;
if (text.includes(RspadeDecorationProvider.RSX_USE_START)) {
in_protected_region = true;
start_line = i;
} else if (text.includes(RspadeDecorationProvider.RSX_USE_END)) {
if (in_protected_region && start_line !== null) {
// Create decoration for the entire region
const start_pos = new vscode.Position(start_line, 0);
const end_pos = new vscode.Position(i, line.text.length);
const decoration: vscode.DecorationOptions = {
range: new vscode.Range(start_pos, end_pos),
hoverMessage: new vscode.MarkdownString(
'ⓘ **Deprecated RSX:USE section**\n\n' +
'These markers are no longer used by the RSpade formatter.'
)
};
decorations.push(decoration);
}
in_protected_region = false;
start_line = null;
}
}
// Apply decorations
editor.setDecorations(this.decoration_type, decorations);
this.decorations.set(editor.document.uri.toString(), decorations);
}
private is_in_protected_region(document: vscode.TextDocument, range: vscode.Range): boolean {
const decorations = this.decorations.get(document.uri.toString()) || [];
for (const decoration of decorations) {
if (decoration.range.contains(range)) {
return true;
}
}
return false;
}
dispose() {
this.decoration_type.dispose();
this.decorations.clear();
}
}

View File

@@ -0,0 +1,651 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { IdeBridgeClient } from './ide_bridge_client';
interface JqhtmlExtensionAPI {
findComponent(name: string): {
uri: vscode.Uri;
position: vscode.Position;
name: string;
line: string;
} | undefined;
getAllComponentNames(): string[];
reindexWorkspace(): Promise<void>;
}
export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
private readonly ide_bridge: IdeBridgeClient;
private status_bar_item: vscode.StatusBarItem | undefined;
private readonly jqhtml_api: JqhtmlExtensionAPI | undefined;
constructor(jqhtml_api: JqhtmlExtensionAPI | undefined) {
// Create output channel and IDE bridge client
const output_channel = vscode.window.createOutputChannel('RSpade Framework');
this.ide_bridge = new IdeBridgeClient(output_channel);
this.jqhtml_api = jqhtml_api;
}
/**
* Find the RSpade project root folder (contains app/RSpade/)
* Works in both single-folder and multi-root workspace modes
*/
private find_rspade_root(): string | undefined {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
// Check each workspace folder for app/RSpade/
for (const folder of vscode.workspace.workspaceFolders) {
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
private show_error_status(message: string) {
// 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);
}
public clear_status_bar() {
if (this.status_bar_item) {
this.status_bar_item.hide();
}
}
async provideDefinition(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Definition | undefined> {
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;
}
// 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)
*/
private async handleRoutePattern(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
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 "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
*/
private async handleThisReference(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
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: 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
// 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>
*/
private async handleJqhtmlComponent(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
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);
}
private async handleJavaScriptDefinition(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
// 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: string | undefined;
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;
}
private async handlePhpBladeDefinition(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
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
try {
const result = await this.queryIdeHelper(stringContent, undefined, 'view');
return this.createLocationFromResult(result);
} catch (error) {
console.error('Error querying IDE helper for view:', error);
}
}
}
// 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;
}
private createLocationFromResult(result: any): vscode.Location | undefined {
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
*/
private async handleFilePathInString(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.Definition | undefined> {
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;
}
private async queryIdeHelper(identifier: string, methodName?: string, type?: string): Promise<any> {
const params: any = { identifier };
if (methodName) {
params.method = methodName;
}
if (type) {
params.type = type;
}
try {
const result = await this.ide_bridge.request('/_idehelper', params);
return result;
} catch (error: any) {
this.show_error_status('IDE helper request failed');
throw error;
}
}
}

View File

@@ -0,0 +1,569 @@
import * as vscode from 'vscode';
import { RspadeFoldingProvider } from './folding_provider';
import { RspadeDecorationProvider } from './decoration_provider';
import { RspadeFileWatcher } from './file_watcher';
import { RspadeFormattingProvider } from './formatting_provider';
import { RspadeDefinitionProvider } from './definition_provider';
import { DebugClient } from './debug_client';
import { get_config } from './config';
import { LaravelCompletionProvider } from './laravel_completion_provider';
import { blade_spacer } from './blade_spacer';
import { init_blade_language_config } from './blade_client';
import { ConventionMethodSemanticTokensProvider, ConventionMethodHoverProvider, ConventionMethodDiagnosticProvider, ConventionMethodDefinitionProvider } from './convention_method_provider';
import { JqhtmlLifecycleSemanticTokensProvider, JqhtmlLifecycleHoverProvider, JqhtmlLifecycleDiagnosticProvider } from './jqhtml_lifecycle_provider';
import { PhpAttributeSemanticTokensProvider } from './php_attribute_provider';
import { BladeComponentSemanticTokensProvider } from './blade_component_provider';
import { AutoRenameProvider } from './auto_rename_provider';
import { FolderColorProvider } from './folder_color_provider';
import { GitStatusProvider } from './git_status_provider';
import { GitDiffProvider } from './git_diff_provider';
import { RspadeRefactorProvider } from './refactor_provider';
import { RspadeRefactorCodeActionsProvider } from './refactor_code_actions';
import { RspadeClassRefactorProvider } from './class_refactor_provider';
import { RspadeClassRefactorCodeActionsProvider } from './class_refactor_code_actions';
import { RspadeSortClassMethodsProvider } from './sort_class_methods_provider';
import * as fs from 'fs';
import * as path from 'path';
let folding_provider: RspadeFoldingProvider;
let decoration_provider: RspadeDecorationProvider;
let file_watcher: RspadeFileWatcher;
let formatting_provider: RspadeFormattingProvider;
let definition_provider: RspadeDefinitionProvider;
let debug_client: DebugClient;
let laravel_completion_provider: LaravelCompletionProvider;
let auto_rename_provider: AutoRenameProvider;
/**
* Check for conflicting PHP extensions and prompt user to disable them
*/
async function check_conflicting_extensions() {
const intelephense = vscode.extensions.getExtension('bmewburn.vscode-intelephense-client');
const php_intellisense = vscode.extensions.getExtension('zobo.php-intellisense');
// Only warn if both Intelephense and PHP IntelliSense are installed
if (intelephense && php_intellisense) {
const action = await vscode.window.showWarningMessage(
`Both "Intelephense" and "PHP IntelliSense" are installed. ` +
`It is recommended to disable "PHP IntelliSense" to avoid conflicts.`,
'Disable PHP IntelliSense',
'Ignore'
);
if (action === 'Disable PHP IntelliSense') {
await vscode.commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [['zobo.php-intellisense']]);
}
}
}
/**
* Find the RSpade project root folder (contains rsx/ and system/app/RSpade/)
* Works in both single-folder and multi-root workspace modes
*/
function find_rspade_root(): string | undefined {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
// Check each workspace folder for rsx/ and system/app/RSpade/ (new structure)
// or app/RSpade/ (legacy structure)
for (const folder of vscode.workspace.workspaceFolders) {
const rsx_dir = path.join(folder.uri.fsPath, 'rsx');
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
// New structure: requires both rsx/ and system/app/RSpade/
if (fs.existsSync(rsx_dir) && fs.existsSync(system_app_rspade)) {
console.log(`[RSpade] Found project root (new structure): ${folder.uri.fsPath}`);
return folder.uri.fsPath;
}
// Legacy structure: just app/RSpade/
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
console.log(`[RSpade] Found project root (legacy structure): ${folder.uri.fsPath}`);
return folder.uri.fsPath;
}
}
return undefined;
}
export async function activate(context: vscode.ExtensionContext) {
console.log('RSpade Framework extension is now active');
// Find RSpade project root
const rspade_root = find_rspade_root();
if (!rspade_root) {
console.log('Not an RSpade project (no rsx/ and system/app/RSpade/ found), extension features disabled');
return;
}
console.log(`[RSpade] Project root: ${rspade_root}`);
// Get config scoped to RSpade root for multi-root workspace support
const config = get_config();
// Get JQHTML extension API for component navigation
// Try both possible extension IDs
let jqhtml_api = undefined;
const possible_jqhtml_ids = [
'jqhtml.jqhtml-vscode-extension',
'jqhtml.@jqhtml/vscode-extension',
'jqhtml.jqhtml-language'
];
console.log('[RSpade] Searching for JQHTML extension...');
console.log('[RSpade] All installed extensions:', vscode.extensions.all.map(e => e.id).filter(id => id.includes('jqhtml')));
let jqhtml_extension = null;
for (const ext_id of possible_jqhtml_ids) {
console.log(`[RSpade] Trying extension ID: ${ext_id}`);
jqhtml_extension = vscode.extensions.getExtension(ext_id);
if (jqhtml_extension) {
console.log(`[RSpade] JQHTML extension found with ID: ${ext_id}`);
console.log(`[RSpade] Extension isActive: ${jqhtml_extension.isActive}`);
break;
} else {
console.log(`[RSpade] Extension ID not found: ${ext_id}`);
}
}
if (!jqhtml_extension) {
console.warn('[RSpade] JQHTML extension not found - component navigation in Blade files will be unavailable');
} else {
try {
console.log('[RSpade] JQHTML extension isActive before activate():', jqhtml_extension.isActive);
console.log('[RSpade] Calling activate() on JQHTML extension...');
// Always call activate() - it returns the API or exports if already active
jqhtml_api = await jqhtml_extension.activate();
console.log('[RSpade] JQHTML extension isActive after activate():', jqhtml_extension.isActive);
console.log('[RSpade] JQHTML extension API loaded successfully');
console.log('[RSpade] API type:', typeof jqhtml_api);
console.log('[RSpade] API value:', jqhtml_api);
console.log('[RSpade] API methods:', Object.keys(jqhtml_api || {}));
console.log('[RSpade] findComponent exists:', typeof (jqhtml_api && jqhtml_api.findComponent));
console.log('[RSpade] getAllComponentNames exists:', typeof (jqhtml_api && jqhtml_api.getAllComponentNames));
console.log('[RSpade] reindexWorkspace exists:', typeof (jqhtml_api && jqhtml_api.reindexWorkspace));
} catch (error) {
console.warn('[RSpade] JQHTML extension found but API could not be loaded:', error);
}
}
// Initialize providers
folding_provider = new RspadeFoldingProvider();
decoration_provider = new RspadeDecorationProvider();
file_watcher = new RspadeFileWatcher();
formatting_provider = new RspadeFormattingProvider();
definition_provider = new RspadeDefinitionProvider(jqhtml_api);
laravel_completion_provider = new LaravelCompletionProvider();
// Register folder color provider
const folder_color_provider = new FolderColorProvider();
context.subscriptions.push(
vscode.window.registerFileDecorationProvider(folder_color_provider)
);
// Register git status provider
const git_status_provider = new GitStatusProvider(rspade_root);
context.subscriptions.push(
vscode.window.registerFileDecorationProvider(git_status_provider)
);
// Register git diff provider
const git_diff_provider = new GitDiffProvider(rspade_root);
git_diff_provider.activate(context);
// Register refactor provider
const refactor_provider = new RspadeRefactorProvider(formatting_provider);
refactor_provider.register(context);
// Register refactor code actions provider
const refactor_code_actions = new RspadeRefactorCodeActionsProvider(refactor_provider);
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
{ language: 'php' },
refactor_code_actions,
{
providedCodeActionKinds: [vscode.CodeActionKind.Refactor]
}
)
);
// Register auto-rename provider early (needed by class refactor provider)
auto_rename_provider = new AutoRenameProvider();
auto_rename_provider.activate(context);
console.log('Auto-rename provider registered for rsx/ files');
// Register class refactor provider
const class_refactor_provider = new RspadeClassRefactorProvider(formatting_provider, auto_rename_provider);
class_refactor_provider.register(context);
// Register class refactor code actions provider
const class_refactor_code_actions = new RspadeClassRefactorCodeActionsProvider(class_refactor_provider);
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
{ language: 'php' },
class_refactor_code_actions,
{
providedCodeActionKinds: [vscode.CodeActionKind.Refactor]
}
)
);
// Register sort class methods provider
const sort_methods_provider = new RspadeSortClassMethodsProvider(formatting_provider);
sort_methods_provider.register(context);
// Register folding provider
if (config.get<boolean>('enableCodeFolding', true)) {
context.subscriptions.push(
vscode.languages.registerFoldingRangeProvider(
{ language: 'php' },
folding_provider
)
);
}
// Activate decoration provider
if (get_config().get<boolean>('enableReadOnlyRegions', true)) {
decoration_provider.activate(context);
}
// Activate file watcher
if (get_config().get<boolean>('enableFormatOnMove', true)) {
file_watcher.activate(context);
}
// Register formatting provider
context.subscriptions.push(
vscode.languages.registerDocumentFormattingEditProvider(
{ language: 'php' },
formatting_provider
)
);
console.log('RSpade formatter registered for PHP files');
// Initialize Blade language configuration (indentation, auto-closing)
init_blade_language_config();
console.log('Blade language configuration initialized');
// Register Blade auto-spacing on text change
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
blade_spacer(event, vscode.window.activeTextEditor);
})
);
console.log('Blade auto-spacing enabled');
// Register definition provider for JavaScript/TypeScript and PHP/Blade/jqhtml files
context.subscriptions.push(
vscode.languages.registerDefinitionProvider(
[
{ language: 'javascript' },
{ language: 'typescript' },
{ language: 'php' },
{ language: 'blade' },
{ language: 'html' },
{ pattern: '**/*.jqhtml' },
{ pattern: '**/*.blade.php' }
],
definition_provider
)
);
console.log('RSpade definition provider registered for JavaScript/TypeScript/PHP/Blade/jqhtml files');
// Register Laravel completion provider for PHP files
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
{ language: 'php' },
laravel_completion_provider
)
);
console.log('Laravel completion provider registered for PHP files');
// Register convention method providers for JavaScript/TypeScript
// Note: Semantic tokens are handled by JqhtmlLifecycleSemanticTokensProvider to avoid duplicate registration
const convention_hover_provider = new ConventionMethodHoverProvider();
const convention_diagnostic_provider = new ConventionMethodDiagnosticProvider();
const convention_definition_provider = new ConventionMethodDefinitionProvider();
context.subscriptions.push(
vscode.languages.registerHoverProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
convention_hover_provider
)
);
context.subscriptions.push(
vscode.languages.registerDefinitionProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
convention_definition_provider
)
);
convention_diagnostic_provider.activate(context);
console.log('Convention method providers registered for JavaScript/TypeScript');
// Register JQHTML lifecycle method providers for JavaScript/TypeScript
const jqhtml_semantic_provider = new JqhtmlLifecycleSemanticTokensProvider();
const jqhtml_hover_provider = new JqhtmlLifecycleHoverProvider();
const jqhtml_diagnostic_provider = new JqhtmlLifecycleDiagnosticProvider();
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
jqhtml_semantic_provider,
new vscode.SemanticTokensLegend(['conventionMethod'])
)
);
context.subscriptions.push(
vscode.languages.registerHoverProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
jqhtml_hover_provider
)
);
jqhtml_diagnostic_provider.activate(context);
console.log('JQHTML lifecycle providers registered for JavaScript/TypeScript');
// Register PHP attribute provider
const php_attribute_provider = new PhpAttributeSemanticTokensProvider();
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
[{ language: 'php' }],
php_attribute_provider,
new vscode.SemanticTokensLegend(['conventionMethod'])
)
);
console.log('PHP attribute provider registered for PHP files');
// Register Blade component provider for uppercase component tags
const blade_component_provider = new BladeComponentSemanticTokensProvider();
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
[{ language: 'blade' }, { pattern: '**/*.blade.php' }],
blade_component_provider,
new vscode.SemanticTokensLegend(['class', 'jqhtmlTagAttribute'])
)
);
console.log('Blade component provider registered for Blade files');
// Debug client disabled
// debug_client = new DebugClient(formatting_provider as any);
// debug_client.start().catch(error => {
// console.error('Failed to start debug client:', error);
// });
// console.log('RSpade debug client started (WebSocket test)');
// Clear status bar on document save
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument(() => {
definition_provider.clear_status_bar();
})
);
// Register commands
context.subscriptions.push(
vscode.commands.registerCommand('rspade.toggleFolding', () => {
const config = get_config();
const current = config.get<boolean>('enableCodeFolding', true);
config.update('enableCodeFolding', !current, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`RSpade code folding ${!current ? 'enabled' : 'disabled'}`);
})
);
context.subscriptions.push(
vscode.commands.registerCommand('rspade.formatPhpFile', async () => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.languageId === 'php') {
await vscode.commands.executeCommand('editor.action.formatDocument');
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('rspade.updateNamespace', async () => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.languageId === 'php') {
await formatting_provider.update_namespace_only(editor.document);
}
})
);
// Override built-in copyRelativePath commands to use project root
const copy_relative_path_handler = async (uri?: vscode.Uri) => {
const rspade_root = find_rspade_root();
if (!rspade_root) {
vscode.window.showErrorMessage('Could not find RSpade project root');
return;
}
// Get URI from context menu click or active editor
const file_uri = uri || vscode.window.activeTextEditor?.document.uri;
if (!file_uri) {
return;
}
// Get path relative to project root
const relative_path = path.relative(rspade_root, file_uri.fsPath);
// Copy to clipboard
await vscode.env.clipboard.writeText(relative_path);
vscode.window.showInformationMessage(`Copied: ${relative_path}`);
};
// Register our custom command
context.subscriptions.push(
vscode.commands.registerCommand('rspade.copyRelativePathFromRoot', copy_relative_path_handler)
);
// Override built-in commands
context.subscriptions.push(
vscode.commands.registerCommand('copyRelativePath', copy_relative_path_handler)
);
context.subscriptions.push(
vscode.commands.registerCommand('copyRelativeFilePath', copy_relative_path_handler)
);
// Watch for configuration changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('rspade')) {
vscode.window.showInformationMessage('RSpade configuration changed. Restart VS Code for some changes to take effect.');
}
})
);
// Watch for extension update marker file
watch_for_self_update(context);
// Watch for terminal close marker file
watch_for_terminal_close(context);
}
function watch_for_self_update(context: vscode.ExtensionContext) {
// Check for update marker file every 2 seconds
const rspade_root = find_rspade_root();
if (!rspade_root) {
return;
}
const marker_file = path.join(rspade_root, '.vscode', '.rspade-extension-updated');
const check_interval = setInterval(() => {
if (fs.existsSync(marker_file)) {
console.log('[RSpade] Extension update marker detected, reloading window in 2 seconds...');
// Clear the interval immediately
clearInterval(check_interval);
// Wait 2 seconds before reloading to allow other VS Code instances to see the marker
setTimeout(async () => {
// Try to delete the marker file (may already be deleted by another instance)
try {
if (fs.existsSync(marker_file)) {
fs.unlinkSync(marker_file);
console.log('[RSpade] Deleted marker file');
}
} catch (error) {
console.error('[RSpade] Failed to delete marker file:', error);
}
// Close the terminal panel
console.log('[RSpade] Closing terminal panel');
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 200ms for panel to close
await new Promise(resolve => setTimeout(resolve, 200));
// Check for conflicting extensions after panel closes
await check_conflicting_extensions();
// Auto-reload VS Code
console.log('[RSpade] Reloading window now');
vscode.commands.executeCommand('workbench.action.reloadWindow');
}, 2000);
}
}, 2000); // Check every 2 seconds
// Clean up interval on deactivate
context.subscriptions.push({
dispose: () => clearInterval(check_interval)
});
}
function watch_for_terminal_close(context: vscode.ExtensionContext) {
// Check for terminal close marker file every second
const rspade_root = find_rspade_root();
if (!rspade_root) {
return;
}
const marker_file = path.join(rspade_root, '.vscode', '.rspade-close-terminal');
const check_interval = setInterval(() => {
if (fs.existsSync(marker_file)) {
console.log('[RSpade] Terminal close marker detected, hiding panel in 2 seconds...');
// Clear the interval immediately
clearInterval(check_interval);
// Wait 2 seconds to allow other VS Code instances to see the marker
setTimeout(async () => {
// Try to delete the marker file (may already be deleted by another instance)
try {
if (fs.existsSync(marker_file)) {
fs.unlinkSync(marker_file);
console.log('[RSpade] Deleted terminal close marker');
}
} catch (error) {
console.error('[RSpade] Failed to delete terminal close marker:', error);
}
// Close all terminals
console.log('[RSpade] Closing all terminals');
vscode.window.terminals.forEach(terminal => terminal.dispose());
// Close the terminal panel
console.log('[RSpade] Closing terminal panel');
await vscode.commands.executeCommand('workbench.action.closePanel');
}, 2000);
}
}, 1000); // Check every second
// Clean up interval on deactivate
context.subscriptions.push({
dispose: () => clearInterval(check_interval)
});
}
export function deactivate() {
// Cleanup
if (decoration_provider) {
decoration_provider.dispose();
}
if (file_watcher) {
file_watcher.dispose();
}
if (auto_rename_provider) {
auto_rename_provider.dispose();
}
// if (debug_client) {
// debug_client.dispose();
// }
}

View File

@@ -0,0 +1,105 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { get_python_command } from './config';
const exec_async = promisify(exec);
export class RspadeFileWatcher {
private disposables: vscode.Disposable[] = [];
activate(context: vscode.ExtensionContext) {
// Watch for file rename/move events
const watcher = vscode.workspace.createFileSystemWatcher('**/*.php');
// Track file renames
const file_tracker = new Map<string, string>();
// Before delete, store the content
watcher.onDidDelete(uri => {
if (this.is_php_file_in_rsx(uri)) {
// Store file path for potential rename detection
file_tracker.set(uri.fsPath, uri.fsPath);
// Clean up old entries after 1 second
setTimeout(() => {
file_tracker.delete(uri.fsPath);
}, 1000);
}
});
// On create, check if it's a rename
watcher.onDidCreate(async uri => {
if (this.is_php_file_in_rsx(uri)) {
// Check if this might be a rename
let old_path: string | undefined;
// Look for recently deleted files
for (const [deleted_path, _] of file_tracker) {
// If the file was deleted within the last second, it might be a rename
if (path.basename(deleted_path) !== path.basename(uri.fsPath)) {
old_path = deleted_path;
break;
}
}
// Format the file to update namespace
await this.format_php_file(uri.fsPath);
if (old_path) {
// Silent update - no notification needed
console.log(`Updated namespace for moved file ${path.basename(uri.fsPath)}`);
file_tracker.delete(old_path);
}
}
});
this.disposables.push(watcher);
context.subscriptions.push(...this.disposables);
// Also handle explicit rename commands
context.subscriptions.push(
vscode.workspace.onDidRenameFiles(async e => {
for (const file of e.files) {
if (this.is_php_file_in_rsx(file.newUri)) {
await this.format_php_file(file.newUri.fsPath);
// Silent update - no notification needed
console.log(`Updated namespace for renamed file ${path.basename(file.newUri.fsPath)}`);
}
}
})
);
}
private is_php_file_in_rsx(uri: vscode.Uri): boolean {
const file_path = uri.fsPath;
return file_path.endsWith('.php') &&
(file_path.includes(path.sep + 'rsx' + path.sep) ||
file_path.includes('/rsx/'));
}
private async format_php_file(file_path: string): Promise<void> {
try {
const workspace_folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(file_path));
if (!workspace_folder) return;
const workspace_root = workspace_folder.uri.fsPath;
const orchestrator_path = path.join(workspace_root, '.vscode', 'formatters', 'orchestrator.py');
const python_cmd = get_python_command();
const command = `${python_cmd} "${orchestrator_path}" "${file_path}"`;
await exec_async(command, { cwd: workspace_root });
} catch (error) {
console.error('Failed to format PHP file:', error);
}
}
dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
this.disposables = [];
}
}

View File

@@ -0,0 +1,121 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
/**
* Provides folder coloring for the RSpade framework
*
* Colors:
* - rsx/ - Blue (highlight important directory)
* - system/ - Muted gray
* - app/ - Muted gray (legacy structure)
* - routes/ - Muted gray (legacy structure)
*/
export class FolderColorProvider implements vscode.FileDecorationProvider {
private readonly _onDidChangeFileDecorations: vscode.EventEmitter<vscode.Uri | vscode.Uri[] | undefined> =
new vscode.EventEmitter<vscode.Uri | vscode.Uri[] | undefined>();
public readonly onDidChangeFileDecorations: vscode.Event<vscode.Uri | vscode.Uri[] | undefined> =
this._onDidChangeFileDecorations.event;
/**
* Find the RSpade project root folder (contains rsx/ and system/app/RSpade/)
* Works in both single-folder and multi-root workspace modes
*/
private find_rspade_root(): string | undefined {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
// Check each workspace folder for rsx/ and system/app/RSpade/ (new structure)
// or app/RSpade/ (legacy structure)
for (const folder of vscode.workspace.workspaceFolders) {
const rsx_dir = path.join(folder.uri.fsPath, 'rsx');
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
// New structure: requires both rsx/ and system/app/RSpade/
if (fs.existsSync(rsx_dir) && fs.existsSync(system_app_rspade)) {
return folder.uri.fsPath;
}
// Legacy structure: just app/RSpade/
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
provideFileDecoration(
uri: vscode.Uri,
token: vscode.CancellationToken
): vscode.ProviderResult<vscode.FileDecoration> {
if (!vscode.workspace.workspaceFolders) {
console.log('[FolderColor] No workspace folders');
return undefined;
}
const uriPath = uri.fsPath.replace(/\\/g, '/');
// Check if this URI is a workspace folder root (for multi-root workspaces)
const workspaceFolder = vscode.workspace.workspaceFolders.find(
folder => folder.uri.fsPath.replace(/\\/g, '/') === uriPath
);
if (workspaceFolder) {
const folderName = workspaceFolder.name.toLowerCase();
console.log('[FolderColor] Workspace folder:', folderName);
// Color workspace folders based on name
if (folderName.includes('rsx')) {
return new vscode.FileDecoration(
undefined,
undefined,
new vscode.ThemeColor('charts.blue')
);
}
// docs, database, public - no coloring (default)
if (folderName.includes('system')) {
return new vscode.FileDecoration(
undefined,
undefined,
new vscode.ThemeColor('descriptionForeground')
);
}
}
// For single-folder workspaces, color top-level directories
const workspaceRoot = this.find_rspade_root();
if (!workspaceRoot) {
return undefined;
}
const relativePath = uriPath.replace(workspaceRoot.replace(/\\/g, '/') + '/', '');
// Only color top-level directories (no subdirectories)
if (relativePath.includes('/')) {
return undefined;
}
// Color blue for rsx
if (relativePath === 'rsx') {
return new vscode.FileDecoration(
undefined,
undefined,
new vscode.ThemeColor('charts.blue')
);
}
// Color muted gray for system, app, and routes
if (relativePath === 'system' || relativePath === 'app' || relativePath === 'routes') {
return new vscode.FileDecoration(
undefined,
undefined,
new vscode.ThemeColor('descriptionForeground')
);
}
return undefined;
}
}

View File

@@ -0,0 +1,41 @@
import * as vscode from 'vscode';
export class RspadeFoldingProvider implements vscode.FoldingRangeProvider {
// RSX markers are no longer used - keeping class for potential future use
private static readonly LLMDIRECTIVE_START = '// [RSX:LLMDIRECTIVE:START]'; // Deprecated
private static readonly LLMDIRECTIVE_END = '// [RSX:LLMDIRECTIVE:END]'; // Deprecated
provideFoldingRanges(
document: vscode.TextDocument,
_context: vscode.FoldingContext,
_token: vscode.CancellationToken
): vscode.ProviderResult<vscode.FoldingRange[]> {
// RSX markers are no longer used - returning empty array
return [];
/* Original implementation preserved for reference
const folding_ranges: vscode.FoldingRange[] = [];
let start_line: number | null = null;
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
const text = line.text.trim();
if (text.includes(RspadeFoldingProvider.LLMDIRECTIVE_START)) {
start_line = i;
} else if (text.includes(RspadeFoldingProvider.LLMDIRECTIVE_END) && start_line !== null) {
// Create folding range from start to end
folding_ranges.push(new vscode.FoldingRange(
start_line,
i,
vscode.FoldingRangeKind.Region
));
start_line = null;
}
}
return folding_ranges;
*/
}
}

View File

@@ -0,0 +1,562 @@
/**
* RSpade Formatting Provider
*
* Handles code formatting via remote IDE service endpoints.
* All formatting is performed on the server - no local PHP execution.
*
* Authentication Flow:
* 1. Reads domain from storage/rsx-ide-bridge/domain.txt (auto-discovered)
* 2. Creates session with auth tokens on first use
* 3. Signs all requests with SHA1(body + client_key)
* 4. Validates server responses with SHA1(body + server_key)
*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as https from 'https';
import * as http from 'http';
import * as crypto from 'crypto';
import { promisify } from 'util';
const read_file = promisify(fs.readFile);
const write_file = promisify(fs.writeFile);
const exists = promisify(fs.exists);
interface AuthData {
session: string;
client_key: string;
server_key: string;
}
export class RspadeFormattingProvider implements vscode.DocumentFormattingEditProvider {
private auth_data: AuthData | null = null;
private server_url: string | null = null;
private output_channel: vscode.OutputChannel;
constructor() {
this.output_channel = vscode.window.createOutputChannel('RSpade Formatter');
console.log('[RSpade Formatter] Provider initialized');
this.output_channel.appendLine('=== RSpade Formatter Initialized ===');
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
this.output_channel.appendLine('Ready to format PHP files via remote server');
}
private find_rspade_root(): string | undefined {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
// Check each workspace folder for system/app/RSpade/ (new structure) or app/RSpade/ (legacy)
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first (both rsx/ and system/ must exist)
const rsx_dir = path.join(folder.uri.fsPath, 'rsx');
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
if (fs.existsSync(rsx_dir) && fs.existsSync(system_app_rspade)) {
// Return the project root (not system/)
return folder.uri.fsPath;
}
// Fall back to legacy structure
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
async provideDocumentFormattingEdits(
document: vscode.TextDocument,
_options: vscode.FormattingOptions,
_token: vscode.CancellationToken
): Promise<vscode.TextEdit[]> {
console.log(`[RSpade Formatter] Format request: ${path.basename(document.fileName)}`);
this.output_channel.appendLine(`\n=== FORMAT REQUEST ===`);
this.output_channel.appendLine(`File: ${document.fileName}`);
this.output_channel.appendLine(`Language: ${document.languageId}`);
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
if (document.languageId !== 'php') {
this.output_channel.appendLine('Result: Skipped (not a PHP file)');
return [];
}
try {
// Get the current document text
const original_text = document.getText();
this.output_channel.appendLine(`Original text length: ${original_text.length} chars`);
// Find RSpade project root
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
throw new Error('RSpade project root not found');
}
// Get relative path from project root
let relative_path = path.relative(rspade_root, document.uri.fsPath);
relative_path = relative_path.replace(/\\/g, '/'); // Convert Windows paths to Unix
this.output_channel.appendLine(`Relative path: ${relative_path}`);
this.output_channel.appendLine(`Project root: ${rspade_root}`);
// Format via server
this.output_channel.appendLine('Calling format_via_server...');
const formatted_text = await this.format_via_server(relative_path, original_text);
// If content changed, return a TextEdit to replace entire document
if (formatted_text !== original_text) {
const full_range = new vscode.Range(
document.positionAt(0),
document.positionAt(original_text.length)
);
this.output_channel.appendLine(`Result: SUCCESS - Content changed`);
this.output_channel.appendLine(`Formatted text length: ${formatted_text.length} chars`);
return [vscode.TextEdit.replace(full_range, formatted_text)];
}
this.output_channel.appendLine('Result: SUCCESS - Content unchanged');
return [];
} catch (error: any) {
console.error('[RSpade Formatter] Format failed:', error.message);
this.output_channel.appendLine(`Result: ERROR`);
this.output_channel.appendLine(`Error message: ${error.message}`);
this.output_channel.appendLine(`Error stack: ${error.stack}`);
vscode.window.showErrorMessage(`RSpade formatting failed: ${error.message || error}`);
return [];
}
}
async update_namespace_only(document: vscode.TextDocument): Promise<void> {
this.output_channel.appendLine(`\n=== NAMESPACE UPDATE ===`);
this.output_channel.appendLine(`File: ${document.fileName}`);
this.output_channel.appendLine(`Language: ${document.languageId}`);
if (document.languageId !== 'php') {
this.output_channel.appendLine('Skipped: Not a PHP file');
return;
}
try {
// Save the document first if it has unsaved changes
if (document.isDirty) {
this.output_channel.appendLine('Saving unsaved changes...');
await document.save();
this.output_channel.appendLine('Document saved');
}
// Get relative path
const workspace_folder = vscode.workspace.getWorkspaceFolder(document.uri);
if (!workspace_folder) {
this.output_channel.appendLine('ERROR: No workspace folder found');
throw new Error('No workspace folder found');
}
let relative_path = path.relative(workspace_folder.uri.fsPath, document.uri.fsPath);
relative_path = relative_path.replace(/\\/g, '/');
this.output_channel.appendLine(`Relative path: ${relative_path}`);
// Read current content
const content = await read_file(document.uri.fsPath, 'utf8');
this.output_channel.appendLine(`File content length: ${content.length} chars`);
// Format via server
this.output_channel.appendLine('Calling format_via_server for namespace update...');
await this.format_via_server(relative_path, content);
this.output_channel.appendLine('Namespace update completed successfully');
// Silent update - no notification needed
} catch (error: any) {
console.error('[RSpade Formatter] Namespace update failed:', error.message);
this.output_channel.appendLine(`ERROR: ${error.message}`);
this.output_channel.appendLine(`Stack: ${error.stack}`);
vscode.window.showErrorMessage(`RSpade namespace update failed: ${error.message || error}`);
}
}
private async format_via_server(relative_path: string, content: string): Promise<string> {
this.output_channel.appendLine('\n--- SERVER FORMATTING ---');
this.output_channel.appendLine(`File path: ${relative_path}`);
this.output_channel.appendLine(`Content length: ${content.length} chars`);
// Ensure we have authentication
await this.ensure_auth();
// Find RSpade project root for temp file creation
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
this.output_channel.appendLine('ERROR: RSpade project root not found');
throw new Error('RSpade project root not found');
}
const temp_file_path = relative_path + '.formatting.tmp';
const full_temp_path = path.join(rspade_root, temp_file_path);
this.output_channel.appendLine(`Creating temp file: ${temp_file_path}`);
// Write content to temp file
await write_file(full_temp_path, content, 'utf8');
this.output_channel.appendLine('Temp file written successfully');
try {
// Prepare request data
const request_data = {
file: temp_file_path,
return_content: true
};
this.output_channel.appendLine(`Request data: ${JSON.stringify(request_data)}`);
const response = await this.make_authenticated_request('/format', request_data);
if (!response.success) {
this.output_channel.appendLine(`ERROR: Server formatting failed - ${response.error}`);
throw new Error(response.error || 'Formatting failed');
}
// Clean up temp file
try {
fs.unlinkSync(full_temp_path);
this.output_channel.appendLine('Temp file cleaned up');
} catch (e: any) {
this.output_channel.appendLine(`Warning: Failed to clean up temp file - ${e.message}`);
}
const formatted_content = response.content || content;
this.output_channel.appendLine(`Formatted content length: ${formatted_content.length} chars`);
this.output_channel.appendLine(`Content changed: ${formatted_content !== content}`);
if (formatted_content !== content) {
this.output_channel.appendLine(`First 100 chars of original: ${content.substring(0, 100)}`);
this.output_channel.appendLine(`First 100 chars of formatted: ${formatted_content.substring(0, 100)}`);
}
return formatted_content;
} catch (error: any) {
this.output_channel.appendLine(`ERROR during formatting: ${error.message}`);
// Clean up temp file on error
try {
fs.unlinkSync(full_temp_path);
this.output_channel.appendLine('Temp file cleaned up after error');
} catch (e: any) {
this.output_channel.appendLine(`Warning: Failed to clean up temp file - ${e.message}`);
}
throw error;
}
}
public async ensure_auth(force_new: boolean = false): Promise<any> {
if (this.auth_data && !force_new) {
console.log('[RSpade Formatter] Reusing session:', this.auth_data.session.substring(0, 8) + '...');
this.output_channel.appendLine(`Using existing auth session: ${this.auth_data.session}`);
return {
session_id: this.auth_data.session,
server_key: this.auth_data.server_key
};
}
console.log('[RSpade Formatter] Creating new auth session');
this.output_channel.appendLine('\n--- AUTHENTICATION ---');
this.output_channel.appendLine('No existing auth session, creating new...');
// Get server URL (force refresh if this is a retry)
await this.get_server_url(force_new);
// Create new auth session
this.output_channel.appendLine(`Creating auth session at: ${this.server_url}/_ide/service/auth/create`);
const response = await this.make_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
console.log('[RSpade Formatter] Auth created:', this.auth_data.session.substring(0, 8) + '...');
this.output_channel.appendLine(`Auth session created successfully!`);
this.output_channel.appendLine(` Session ID: ${this.auth_data.session}`);
this.output_channel.appendLine(` Client Key: ${this.auth_data.client_key.substring(0, 8)}...`);
this.output_channel.appendLine(` Server Key: ${this.auth_data.server_key.substring(0, 8)}...`);
return {
session_id: this.auth_data.session,
server_key: this.auth_data.server_key
};
}
public async get_server_url(force_refresh: boolean = false): Promise<string | null> {
// Only re-check if forced (due to connection failure) or no URL cached
if (this.server_url && !force_refresh) {
this.output_channel.appendLine(`Using cached server URL: ${this.server_url}`);
return this.server_url;
}
this.output_channel.appendLine('\n--- SERVER URL DISCOVERY ---');
if (force_refresh) {
this.output_channel.appendLine('Re-checking server URL due to connection failure...');
}
// First check VS Code settings
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get<string>('serverUrl');
this.output_channel.appendLine(`Checking VS Code settings: ${configured_url ? 'found' : 'not found'}`);
if (configured_url) {
this.server_url = configured_url;
console.log('[RSpade Formatter] Using configured URL:', this.server_url);
this.output_channel.appendLine(`Using configured server URL: ${this.server_url}`);
return this.server_url;
}
// Try to auto-discover from file
// Find RSpade project root (works in both single-folder and multi-root workspace)
let rspade_root = null;
if (vscode.workspace.workspaceFolders) {
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
if (fs.existsSync(system_app_rspade)) {
rspade_root = path.join(folder.uri.fsPath, 'system');
break;
}
// Fall back to legacy structure
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
rspade_root = folder.uri.fsPath;
break;
}
}
}
if (!rspade_root) {
this.output_channel.appendLine('ERROR: RSpade project root not found');
throw new Error('RSpade project root not found');
}
const domain_file = path.join(rspade_root, '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');
this.server_url = domain.trim();
console.log('[RSpade Formatter] Auto-discovered:', this.server_url);
this.output_channel.appendLine(`Auto-discovered server URL: ${this.server_url}`);
return this.server_url;
}
// Provide helpful error message
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
this.output_channel.appendLine('ERROR: RSpade Extension cannot connect to server');
this.output_channel.appendLine('════════════════════════════════════════════════════════');
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('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');
this.output_channel.appendLine('3. Enable IDE integration in Laravel config');
this.output_channel.appendLine(' In config/rsx.php, set ide_integration.enabled = true');
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
throw new Error('RSpade: storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
}
private async make_authenticated_request(
endpoint: string,
data: any,
retry_count: number = 0
): Promise<any> {
this.output_channel.appendLine(`\n--- AUTHENTICATED REQUEST ${retry_count > 0 ? '(RETRY)' : ''} ---`);
this.output_channel.appendLine(`Endpoint: ${endpoint}`);
// If no auth data or this is a retry, create new session
if (!this.auth_data || retry_count > 0) {
if (retry_count === 0) {
this.output_channel.appendLine('No auth session exists, creating new one...');
} else {
this.output_channel.appendLine('Session lost, creating new session...');
}
try {
await this.ensure_auth();
} catch (error: any) {
this.output_channel.appendLine(`ERROR: Failed to create auth session: ${error.message}`);
throw error;
}
}
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data!.client_key).digest('hex');
console.log('[RSpade Formatter] Request sig:', signature.substring(0, 8) + '...');
this.output_channel.appendLine(`Body length: ${body.length} bytes`);
this.output_channel.appendLine(`Session: ${this.auth_data!.session}`);
this.output_channel.appendLine(`Client key (first 8 chars): ${this.auth_data!.client_key.substring(0, 8)}...`);
this.output_channel.appendLine(`Generated signature: ${signature}`);
try {
const response = await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data!.session,
'X-Signature': signature
});
return response;
} catch (error: any) {
// Check if this is a recoverable error and we haven't retried yet
if (retry_count === 0) {
const error_msg = error.message || '';
// Session expired or not found
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.output_channel.appendLine('Session/signature error, attempting recovery...');
this.auth_data = null;
// Force URL refresh in case server changed
this.server_url = null;
return this.make_authenticated_request(endpoint, data, retry_count + 1);
}
// Connection failure - maybe server URL changed
if (error_msg.includes('ECONNREFUSED') || error_msg.includes('ENOTFOUND') ||
error_msg.includes('ETIMEDOUT') || error_msg.includes('getaddrinfo')) {
this.output_channel.appendLine('Connection failed, refreshing server URL...');
this.auth_data = null;
// Force URL refresh
this.server_url = null;
return this.make_authenticated_request(endpoint, data, retry_count + 1);
}
}
// Not a recoverable error or already retried
throw error;
}
}
private async make_request(
endpoint: string,
data: any,
method: string = 'POST',
validate_signature: boolean = false,
extra_headers: any = {}
): Promise<any> {
return new Promise((resolve, reject) => {
this.output_channel.appendLine('\n--- HTTP REQUEST ---');
this.output_channel.appendLine(`Method: ${method}`);
this.output_channel.appendLine(`Validate signature: ${validate_signature}`);
if (!this.server_url) {
this.output_channel.appendLine('ERROR: Server URL not configured');
reject(new Error('Server URL not configured'));
return;
}
const url = new URL(this.server_url);
const is_https = url.protocol === 'https:';
const http_module = is_https ? https : http;
const body = JSON.stringify(data);
const options: https.RequestOptions = {
hostname: url.hostname,
port: url.port || (is_https ? 443 : 80),
path: '/_ide/service' + endpoint,
method: method,
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
...extra_headers
},
timeout: 30000, // 30 second timeout
rejectUnauthorized: false // Allow self-signed certificates
};
this.output_channel.appendLine(`Full URL: ${is_https ? 'https' : 'http'}://${options.hostname}:${options.port}${options.path}`);
this.output_channel.appendLine(`Request body size: ${Buffer.byteLength(body)} bytes`);
const start_time = Date.now();
const req = http_module.request(options, (res) => {
let response_data = '';
let total_bytes = 0;
this.output_channel.appendLine(`\n--- HTTP RESPONSE ---`);
this.output_channel.appendLine(`Status: ${res.statusCode}`);
res.on('data', (chunk) => {
response_data += chunk;
total_bytes += chunk.length;
});
res.on('end', () => {
const elapsed = Date.now() - start_time;
console.log(`[RSpade Formatter] Response: ${res.statusCode} in ${elapsed}ms (${total_bytes} bytes)`);
this.output_channel.appendLine(`Response time: ${elapsed}ms`);
this.output_channel.appendLine(`Response body size: ${response_data.length} bytes`);
try {
// Validate signature if required
if (validate_signature && this.auth_data) {
const response_signature = res.headers['x-signature'] as string;
this.output_channel.appendLine(`\n--- SIGNATURE VALIDATION ---`);
this.output_channel.appendLine(`Response signature: ${response_signature || 'missing'}`);
if (response_signature) {
const expected_signature = crypto.createHash('sha1')
.update(response_data + this.auth_data.server_key)
.digest('hex');
console.log('[RSpade Formatter] Sig match:', response_signature.substring(0, 8) + '... == ' + expected_signature.substring(0, 8) + '...');
this.output_channel.appendLine(`Server key (first 8 chars): ${this.auth_data.server_key.substring(0, 8)}...`);
this.output_channel.appendLine(`Expected signature: ${expected_signature}`);
this.output_channel.appendLine(`Validation: ${response_signature === expected_signature ? 'PASSED' : 'FAILED'}`);
if (response_signature !== expected_signature) {
console.error('[RSpade Formatter] Signature mismatch!');
this.output_channel.appendLine('ERROR: Invalid server signature');
reject(new Error('Invalid server signature'));
return;
}
}
}
const response = JSON.parse(response_data);
if (res.statusCode !== 200) {
this.output_channel.appendLine(`ERROR: ${response.error || `HTTP ${res.statusCode}`}`);
reject(new Error(response.error || `HTTP ${res.statusCode}`));
} else {
this.output_channel.appendLine('Result: SUCCESS');
resolve(response);
}
} catch (e: any) {
this.output_channel.appendLine(`ERROR: Failed to parse response - ${e.message}`);
reject(new Error('Invalid response from server'));
}
});
});
req.on('error', (error) => {
const elapsed = Date.now() - start_time;
console.error(`[RSpade Formatter] Request failed after ${elapsed}ms:`, error.message);
this.output_channel.appendLine(`\nERROR after ${elapsed}ms: ${error.message}`);
reject(error);
});
req.on('timeout', () => {
this.output_channel.appendLine('\nERROR: Request timed out after 30 seconds');
req.destroy();
reject(new Error('Request timed out'));
});
req.write(body);
req.end();
});
}
}

View File

@@ -0,0 +1,543 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as http from 'http';
import * as https from 'https';
import * as crypto from 'crypto';
import { URL } from 'url';
interface AuthData {
session: string;
client_key: string;
server_key: string;
}
interface DiffResponse {
success: boolean;
added: number[][];
modified: number[][];
deleted: number[][];
}
/**
* Git diff provider - shows line-level change indicators in gutter
*/
export class GitDiffProvider {
private rspade_root: string | undefined;
private auth_data: AuthData | null = null;
private server_url: string | null = null;
private cached_protocol: 'https:' | 'http:' | null = null;
private retry_timer: NodeJS.Timeout | null = null;
private retry_interval: number = 60000; // 60 seconds
private domain_file_watcher: fs.FSWatcher | null = null;
private added_decoration: vscode.TextEditorDecorationType;
private modified_decoration: vscode.TextEditorDecorationType;
private deleted_decoration: vscode.TextEditorDecorationType;
// Track git diff state and local modifications per document
private git_state: Map<string, DiffResponse> = new Map();
private original_content: Map<string, string> = new Map();
constructor(rspade_root: string | undefined) {
this.rspade_root = rspade_root;
// Watch domain.txt for changes
this.setup_domain_file_watcher();
// Create decoration types for different change types
this.added_decoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
overviewRulerColor: 'rgba(0, 255, 0, 0.6)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
gutterIconPath: this.create_colored_bar('#28a745'),
gutterIconSize: 'contain'
});
this.modified_decoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
overviewRulerColor: 'rgba(33, 150, 243, 0.6)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
gutterIconPath: this.create_colored_bar('#2196f3'),
gutterIconSize: 'contain'
});
this.deleted_decoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
overviewRulerColor: 'rgba(244, 67, 54, 0.6)',
overviewRulerLane: vscode.OverviewRulerLane.Left,
gutterIconPath: this.create_colored_bar('#f44336'),
gutterIconSize: 'contain'
});
}
activate(context: vscode.ExtensionContext) {
// Track document changes for real-time line marking
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument(e => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document === e.document) {
this.update_decorations_for_changes(editor);
}
})
);
// Refresh git diff on file save
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument(async document => {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document === document) {
await this.fetch_git_diff(editor);
this.original_content.set(document.uri.toString(), document.getText());
this.update_decorations_for_changes(editor);
}
})
);
// Refresh on active editor change (only if clean)
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(async editor => {
if (editor) {
const doc_key = editor.document.uri.toString();
// Only fetch git diff if document is clean
if (!editor.document.isDirty) {
await this.fetch_git_diff(editor);
this.original_content.set(doc_key, editor.document.getText());
}
this.update_decorations_for_changes(editor);
}
})
);
// Initial refresh for current editor
const editor = vscode.window.activeTextEditor;
if (editor) {
this.fetch_git_diff(editor).then(() => {
this.original_content.set(editor.document.uri.toString(), editor.document.getText());
this.update_decorations_for_changes(editor);
});
}
}
private create_colored_bar(color: string): vscode.Uri {
// Create a simple SVG colored bar for the gutter
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="3" height="16"><rect width="3" height="16" fill="${color}"/></svg>`;
const data_uri = 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
return vscode.Uri.parse(data_uri);
}
private async fetch_git_diff(editor: vscode.TextEditor) {
if (!this.rspade_root) {
return;
}
try {
const relative_path = path.relative(this.rspade_root, editor.document.uri.fsPath).replace(/\\/g, '/');
const response = await this.make_authenticated_request('/git/diff', { file: relative_path });
if (response.success) {
this.git_state.set(editor.document.uri.toString(), response);
}
} catch (error) {
console.error('[GitDiff] Failed to fetch git diff:', error);
}
}
private update_decorations_for_changes(editor: vscode.TextEditor) {
const doc_key = editor.document.uri.toString();
const git_diff = this.git_state.get(doc_key);
const original_text = this.original_content.get(doc_key);
if (!git_diff) {
return;
}
// Start with git diff changes
const added_lines = new Set<number>();
const modified_lines = new Set<number>();
const deleted_lines = new Set<number>();
// Add git diff lines
for (const [start, end] of git_diff.added) {
for (let line = start; line <= end; line++) {
added_lines.add(line);
}
}
for (const [start, end] of git_diff.modified) {
for (let line = start; line <= end; line++) {
modified_lines.add(line);
}
}
for (const [start, end] of git_diff.deleted) {
for (let line = start; line <= end; line++) {
deleted_lines.add(line);
}
}
// If document is dirty and we have original content, compute proper diff
if (editor.document.isDirty && original_text) {
const local_changes = this.compute_diff(original_text, editor.document.getText());
// Overlay local changes on top of git changes
for (const line of local_changes.added) {
added_lines.add(line);
}
for (const line of local_changes.modified) {
modified_lines.add(line);
}
for (const line of local_changes.deleted) {
deleted_lines.add(line);
}
}
// Convert to ranges and apply decorations
const added_ranges = Array.from(added_lines).map(line => new vscode.Range(line - 1, 0, line - 1, 0));
const modified_ranges = Array.from(modified_lines).map(line => new vscode.Range(line - 1, 0, line - 1, 0));
const deleted_ranges = Array.from(deleted_lines).map(line => new vscode.Range(line - 1, 0, line - 1, 0));
editor.setDecorations(this.added_decoration, added_ranges);
editor.setDecorations(this.modified_decoration, modified_ranges);
editor.setDecorations(this.deleted_decoration, deleted_ranges);
}
private compute_diff(original: string, current: string): { added: number[], modified: number[], deleted: number[] } {
const original_lines = original.split('\n');
const current_lines = current.split('\n');
// Build LCS table for longest common subsequence
const m = original_lines.length;
const n = current_lines.length;
const lcs: number[][] = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (original_lines[i - 1] === current_lines[j - 1]) {
lcs[i][j] = lcs[i - 1][j - 1] + 1;
} else {
lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]);
}
}
}
// Backtrack to find which lines changed
const added: number[] = [];
const modified: number[] = [];
const deleted: number[] = [];
let i = m;
let j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && original_lines[i - 1] === current_lines[j - 1]) {
// Line unchanged
i--;
j--;
} else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
// Line added in current
added.push(j); // 1-indexed
j--;
} else if (i > 0) {
// Line deleted from original
deleted.push(j + 1); // Mark position where it was deleted (1-indexed)
i--;
}
}
return { added, modified, deleted };
}
private async get_server_url(): Promise<string> {
if (this.server_url) {
return this.server_url;
}
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get<string>('serverUrl');
if (configured_url) {
this.server_url = await this.negotiate_protocol(configured_url);
return this.server_url;
}
const domain_file = path.join(this.rspade_root!, 'storage', 'rsx-ide-bridge', 'domain.txt');
if (fs.existsSync(domain_file)) {
const domain = fs.readFileSync(domain_file, 'utf8').trim();
this.server_url = await this.negotiate_protocol(domain);
return this.server_url;
}
// domain.txt doesn't exist yet - schedule retry
this.schedule_retry();
throw new Error('Server URL not configured - waiting for domain.txt');
}
private async negotiate_protocol(url_or_hostname: string): Promise<string> {
// Parse the input to extract hostname
let hostname: string;
// If it looks like a URL, parse it
if (url_or_hostname.includes('://')) {
const parsed = new URL(url_or_hostname);
hostname = parsed.hostname;
// If we have a cached protocol from previous successful connection, use it
if (this.cached_protocol) {
console.log(`[GitDiff] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
} else {
hostname = url_or_hostname;
}
// If we have a cached protocol, use it
if (this.cached_protocol) {
console.log(`[GitDiff] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
// Try HTTPS first
console.log(`[GitDiff] Negotiating protocol for: ${hostname}`);
const https_url = `https://${hostname}`;
if (await this.test_connection(https_url)) {
console.log(`[GitDiff] HTTPS connection successful`);
this.cached_protocol = 'https:';
return https_url;
}
console.log(`[GitDiff] HTTPS failed, trying HTTP...`);
const http_url = `http://${hostname}`;
if (await this.test_connection(http_url)) {
console.log(`[GitDiff] HTTP connection successful`);
this.cached_protocol = 'http:';
return http_url;
}
throw new Error(`Unable to connect to ${hostname} via HTTPS or HTTP`);
}
private async test_connection(base_url: string): Promise<boolean> {
try {
const parsed_url = new URL(base_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const test_path = '/_ide/service/auth/create';
return new Promise<boolean>((resolve) => {
const options = {
hostname: parsed_url.hostname,
port: port,
path: test_path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': '2'
},
timeout: 5000
};
const client = is_https ? https : http;
const req = client.request(options, (res) => {
// 200 OK or 301/302 redirect means server is responding
// We'll handle redirects by just noting protocol works
const success = res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302;
// If we got 301/302, it means we should probably use the other protocol
// But we'll return false to try the other one
if (res.statusCode === 301 || res.statusCode === 302) {
console.log(`[GitDiff] Got redirect ${res.statusCode} from ${base_url}`);
resolve(false);
} else {
resolve(success);
}
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.write('{}');
req.end();
});
} catch (error) {
return false;
}
}
private setup_domain_file_watcher() {
if (!this.rspade_root) {
return;
}
const domain_file = path.join(this.rspade_root, '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)) {
try {
this.domain_file_watcher = fs.watch(domain_dir, (eventType, filename) => {
if (filename === 'domain.txt' && eventType === 'change') {
console.log('[GitDiff] domain.txt changed, clearing cached URL and protocol');
this.server_url = null;
this.cached_protocol = null;
this.auth_data = null;
// Cancel any pending retries
if (this.retry_timer) {
clearTimeout(this.retry_timer);
this.retry_timer = null;
}
// Refresh git diff for active editor
const editor = vscode.window.activeTextEditor;
if (editor) {
this.fetch_git_diff(editor);
}
}
});
} catch (error) {
console.error('[GitDiff] Failed to set up domain file watcher:', error);
}
}
}
private schedule_retry() {
// Don't schedule multiple retries
if (this.retry_timer) {
return;
}
console.log(`[GitDiff] Scheduling retry in ${this.retry_interval / 1000} seconds...`);
this.retry_timer = setTimeout(() => {
console.log('[GitDiff] Retrying git diff fetch...');
this.retry_timer = null;
const editor = vscode.window.activeTextEditor;
if (editor) {
this.fetch_git_diff(editor);
}
}, this.retry_interval);
}
private async ensure_auth(): Promise<void> {
if (this.auth_data) {
return;
}
await this.get_server_url();
const response = await this.make_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
}
private async make_authenticated_request(endpoint: string, data: any): Promise<any> {
if (!this.auth_data) {
await this.ensure_auth();
}
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data!.client_key).digest('hex');
try {
const response = await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data!.session,
'X-Signature': signature
});
return response;
} catch (error: any) {
const error_msg = error.message || '';
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.auth_data = null;
await this.ensure_auth();
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data!.client_key).digest('hex');
return await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data!.session,
'X-Signature': signature
});
}
throw error;
}
}
private make_request(endpoint: string, data: any, method: string, signed: boolean, extra_headers: any = {}): Promise<any> {
return new Promise(async (resolve, reject) => {
const server_url = await this.get_server_url();
const parsed_url = new URL(server_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const full_path = `/_ide/service${endpoint}`;
const body_str = JSON.stringify(data);
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body_str),
...extra_headers
};
const options = {
hostname: parsed_url.hostname,
port: port,
path: full_path,
method: method,
headers: headers
};
console.log(`[GitDiff] Making request to: ${parsed_url.protocol}//${parsed_url.hostname}:${port}${full_path}`);
console.log(`[GitDiff] Server URL from config/file: ${server_url}`);
const client = is_https ? https : http;
const req = client.request(options, (res) => {
console.log(`[GitDiff] Response status: ${res.statusCode}`);
let response_data = '';
res.on('data', chunk => response_data += chunk);
res.on('end', () => {
console.log(`[GitDiff] Response body: ${response_data.substring(0, 200)}`);
try {
const parsed = JSON.parse(response_data);
resolve(parsed);
} catch (e) {
console.error(`[GitDiff] JSON parse error. Full response: ${response_data}`);
reject(new Error('Invalid JSON response'));
}
});
});
req.on('error', (err) => {
console.error(`[GitDiff] Request error:`, err);
reject(err);
});
req.write(body_str);
req.end();
});
}
dispose() {
this.added_decoration.dispose();
this.modified_decoration.dispose();
this.deleted_decoration.dispose();
}
}

View File

@@ -0,0 +1,431 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as http from 'http';
import * as https from 'https';
import * as crypto from 'crypto';
import { URL } from 'url';
interface AuthData {
session: string;
client_key: string;
server_key: string;
}
/**
* Git status provider that fetches status from IDE service
* Colors files based on git status without using local git
*/
export class GitStatusProvider implements vscode.FileDecorationProvider {
private _onDidChangeFileDecorations: vscode.EventEmitter<vscode.Uri | vscode.Uri[] | undefined> =
new vscode.EventEmitter<vscode.Uri | vscode.Uri[] | undefined>();
public readonly onDidChangeFileDecorations: vscode.Event<vscode.Uri | vscode.Uri[] | undefined> =
this._onDidChangeFileDecorations.event;
private git_status: Map<string, 'modified' | 'added' | 'conflict'> = new Map();
private rspade_root: string | undefined;
private auth_data: AuthData | null = null;
private server_url: string | null = null;
private cached_protocol: 'https:' | 'http:' | null = null;
private retry_timer: NodeJS.Timeout | null = null;
private retry_interval: number = 60000; // 60 seconds
private domain_file_watcher: fs.FSWatcher | null = null;
constructor(rspade_root: string | undefined) {
this.rspade_root = rspade_root;
// Initial fetch
this.refresh_git_status();
// Fetch on file save
vscode.workspace.onDidSaveTextDocument(() => {
this.refresh_git_status();
});
// Fetch on window focus
vscode.window.onDidChangeWindowState(e => {
if (e.focused) {
this.refresh_git_status();
}
});
// Watch domain.txt for changes
this.setup_domain_file_watcher();
}
async refresh_git_status() {
if (!this.rspade_root) {
return;
}
try {
const response = await this.make_authenticated_request('/git', {});
if (response.success) {
// Build new status map from response
const new_status = new Map<string, 'modified' | 'added' | 'conflict'>();
for (const file of response.modified || []) {
new_status.set(file, 'modified');
}
for (const file of response.added || []) {
new_status.set(file, 'added');
}
for (const file of response.conflicts || []) {
new_status.set(file, 'conflict');
}
// Collect URIs that need decoration updates
const changed_uris: vscode.Uri[] = [];
// First pass: update or remove existing tracked files
for (const [file_path, old_status] of this.git_status.entries()) {
const new_file_status = new_status.get(file_path);
// File changed status or was removed from git status
if (new_file_status !== old_status) {
const file_uri = vscode.Uri.file(path.join(this.rspade_root!, file_path));
changed_uris.push(file_uri);
if (new_file_status === undefined) {
// File no longer in git status - remove it
this.git_status.delete(file_path);
} else {
// File changed status - update it
this.git_status.set(file_path, new_file_status);
}
}
}
// Second pass: add newly tracked files
for (const [file_path, status] of new_status.entries()) {
if (!this.git_status.has(file_path)) {
this.git_status.set(file_path, status);
const file_uri = vscode.Uri.file(path.join(this.rspade_root!, file_path));
changed_uris.push(file_uri);
}
}
// Only fire decoration updates for files that actually changed
if (changed_uris.length > 0) {
this._onDidChangeFileDecorations.fire(changed_uris);
}
}
} catch (error) {
console.error('[GitStatus] Failed to fetch status:', error);
}
}
private async get_server_url(): Promise<string> {
if (this.server_url) {
return this.server_url;
}
// Check VS Code settings
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get<string>('serverUrl');
if (configured_url) {
this.server_url = await this.negotiate_protocol(configured_url);
return this.server_url;
}
// Try to auto-discover from domain.txt
const domain_file = path.join(this.rspade_root!, 'storage', 'rsx-ide-bridge', 'domain.txt');
if (fs.existsSync(domain_file)) {
const domain = fs.readFileSync(domain_file, 'utf8').trim();
this.server_url = await this.negotiate_protocol(domain);
return this.server_url;
}
// domain.txt doesn't exist yet - schedule retry
this.schedule_retry();
throw new Error('Server URL not configured - waiting for domain.txt');
}
private async negotiate_protocol(url_or_hostname: string): Promise<string> {
// Parse the input to extract hostname
let hostname: string;
// If it looks like a URL, parse it
if (url_or_hostname.includes('://')) {
const parsed = new URL(url_or_hostname);
hostname = parsed.hostname;
// If we have a cached protocol from previous successful connection, use it
if (this.cached_protocol) {
console.log(`[GitStatus] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
} else {
hostname = url_or_hostname;
}
// If we have a cached protocol, use it
if (this.cached_protocol) {
console.log(`[GitStatus] Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
// Try HTTPS first
console.log(`[GitStatus] Negotiating protocol for: ${hostname}`);
const https_url = `https://${hostname}`;
if (await this.test_connection(https_url)) {
console.log(`[GitStatus] HTTPS connection successful`);
this.cached_protocol = 'https:';
return https_url;
}
console.log(`[GitStatus] HTTPS failed, trying HTTP...`);
const http_url = `http://${hostname}`;
if (await this.test_connection(http_url)) {
console.log(`[GitStatus] HTTP connection successful`);
this.cached_protocol = 'http:';
return http_url;
}
throw new Error(`Unable to connect to ${hostname} via HTTPS or HTTP`);
}
private async test_connection(base_url: string): Promise<boolean> {
try {
const parsed_url = new URL(base_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const test_path = '/_ide/service/auth/create';
return new Promise<boolean>((resolve) => {
const options = {
hostname: parsed_url.hostname,
port: port,
path: test_path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': '2'
},
timeout: 5000
};
const client = is_https ? https : http;
const req = client.request(options, (res) => {
// 200 OK or 301/302 redirect means server is responding
// We'll handle redirects by just noting protocol works
const success = res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302;
// If we got 301/302, it means we should probably use the other protocol
// But we'll return false to try the other one
if (res.statusCode === 301 || res.statusCode === 302) {
console.log(`[GitStatus] Got redirect ${res.statusCode} from ${base_url}`);
resolve(false);
} else {
resolve(success);
}
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.write('{}');
req.end();
});
} catch (error) {
return false;
}
}
private setup_domain_file_watcher() {
if (!this.rspade_root) {
return;
}
const domain_file = path.join(this.rspade_root, '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)) {
try {
this.domain_file_watcher = fs.watch(domain_dir, (eventType, filename) => {
if (filename === 'domain.txt' && eventType === 'change') {
console.log('[GitStatus] domain.txt changed, clearing cached URL and protocol');
this.server_url = null;
this.cached_protocol = null;
this.auth_data = null;
// Cancel any pending retries
if (this.retry_timer) {
clearTimeout(this.retry_timer);
this.retry_timer = null;
}
// Refresh git status with new URL
this.refresh_git_status();
}
});
} catch (error) {
console.error('[GitStatus] Failed to set up domain file watcher:', error);
}
}
}
private schedule_retry() {
// Don't schedule multiple retries
if (this.retry_timer) {
return;
}
console.log(`[GitStatus] Scheduling retry in ${this.retry_interval / 1000} seconds...`);
this.retry_timer = setTimeout(() => {
console.log('[GitStatus] Retrying git status fetch...');
this.retry_timer = null;
this.refresh_git_status();
}, this.retry_interval);
}
private async ensure_auth(): Promise<void> {
if (this.auth_data) {
return;
}
// Get server URL
await this.get_server_url();
// Create new auth session
const response = await this.make_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
}
private async make_authenticated_request(endpoint: string, data: any): Promise<any> {
// Ensure we have auth
if (!this.auth_data) {
await this.ensure_auth();
}
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data!.client_key).digest('hex');
try {
const response = await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data!.session,
'X-Signature': signature
});
return response;
} catch (error: any) {
// If session lost, retry once with new auth
const error_msg = error.message || '';
if (error_msg.includes('Session not found') || error_msg.includes('Invalid signature')) {
this.auth_data = null;
await this.ensure_auth();
const body = JSON.stringify(data);
const signature = crypto.createHash('sha1').update(body + this.auth_data!.client_key).digest('hex');
return await this.make_request(endpoint, data, 'POST', true, {
'X-Session': this.auth_data!.session,
'X-Signature': signature
});
}
throw error;
}
}
private make_request(endpoint: string, data: any, method: string, signed: boolean, extra_headers: any = {}): Promise<any> {
return new Promise(async (resolve, reject) => {
const server_url = await this.get_server_url();
const parsed_url = new URL(server_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const full_path = `/_ide/service${endpoint}`;
const body_str = JSON.stringify(data);
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body_str),
...extra_headers
};
const options = {
hostname: parsed_url.hostname,
port: port,
path: full_path,
method: method,
headers: headers
};
console.log(`[GitStatus] Making request to: ${parsed_url.protocol}//${parsed_url.hostname}:${port}${full_path}`);
console.log(`[GitStatus] Server URL from config/file: ${server_url}`);
const client = is_https ? https : http;
const req = client.request(options, (res) => {
console.log(`[GitStatus] Response status: ${res.statusCode}`);
let response_data = '';
res.on('data', chunk => response_data += chunk);
res.on('end', () => {
console.log(`[GitStatus] Response body: ${response_data.substring(0, 200)}`);
try {
const parsed = JSON.parse(response_data);
resolve(parsed);
} catch (e) {
console.error(`[GitStatus] JSON parse error. Full response: ${response_data}`);
reject(new Error('Invalid JSON response'));
}
});
});
req.on('error', (err) => {
console.error(`[GitStatus] Request error:`, err);
reject(err);
});
req.write(body_str);
req.end();
});
}
provideFileDecoration(uri: vscode.Uri): vscode.ProviderResult<vscode.FileDecoration> {
if (!this.rspade_root) {
return undefined;
}
// Get path relative to project root
const relative_path = path.relative(this.rspade_root, uri.fsPath).replace(/\\/g, '/');
const status = this.git_status.get(relative_path);
if (!status) {
return undefined;
}
// Return decoration based on status
if (status === 'conflict') {
const decoration = new vscode.FileDecoration();
decoration.color = new vscode.ThemeColor('charts.red');
return decoration;
} else if (status === 'modified' || status === 'added') {
const decoration = new vscode.FileDecoration();
decoration.color = new vscode.ThemeColor('gitDecoration.modifiedResourceForeground');
return decoration;
}
return undefined;
}
}

View File

@@ -0,0 +1,577 @@
/**
* RSpade IDE Bridge Client
*
* 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
* 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
*
* AUTHENTICATION FLOW:
* 1. Client requests session from /_idehelper/auth/create
* 2. Server generates session ID, client_key, server_key
* 3. Client signs requests: SHA1(body + client_key)
* 4. Server validates signature and signs response: SHA1(body + server_key)
* 5. Client validates server response signature
*
* ERROR HANDLING:
* - ALWAYS fails loud with descriptive errors
* - NO silent fallbacks
* - Logs all activity to output channel
* - Shows status bar notifications for user visibility
*
* USAGE FOR NEW IDE HELPER INTEGRATIONS:
*
* ```typescript
* import { IdeBridgeClient } from './ide_bridge_client';
*
* // Create client instance (pass output channel for logging)
* const client = new IdeBridgeClient(output_channel);
*
* // Make request to IDE helper endpoint
* const response = await client.request('/_idehelper/your_endpoint', {
* param1: 'value1',
* param2: 'value2'
* });
*
* // Client handles:
* // - Auto-discovery of server URL
* // - Authentication session creation
* // - Request signing
* // - Response validation
* // - Auto-retry on connection/auth failure
* // - Comprehensive error reporting
* ```
*
* ADDING NEW IDE HELPER ENDPOINTS:
*
* Backend (PHP):
* 1. Add method to /app/RSpade/Ide/Helper/Ide_Helper_Controller.php
* 2. Register route in /routes/web.php:
* Route::get('/_idehelper/your_endpoint', [Ide_Helper_Controller::class, 'your_method']);
* 3. Return JsonResponse with data
*
* Frontend (TypeScript):
* 1. Use IdeBridgeClient.request() to call endpoint
* 2. NO hardcoded URLs
* 3. NO silent error handling
* 4. Let client handle all auth/retry/discovery logic
*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as https from 'https';
import * as http from 'http';
import * as crypto from 'crypto';
import { promisify } from 'util';
const read_file = promisify(fs.readFile);
const exists = promisify(fs.exists);
interface AuthData {
session: string;
client_key: string;
server_key: string;
}
export class IdeBridgeClient {
private auth_data: AuthData | null = null;
private server_url: string | null = null;
private cached_protocol: 'https:' | 'http:' | null = null;
private retry_timer: NodeJS.Timeout | null = null;
private retry_interval: number = 60000; // 60 seconds
private domain_file_watcher: fs.FSWatcher | null = null;
private output_channel: vscode.OutputChannel;
private status_bar_item: vscode.StatusBarItem | undefined;
constructor(output_channel?: vscode.OutputChannel) {
this.output_channel = output_channel || vscode.window.createOutputChannel('RSpade IDE Bridge');
this.output_channel.appendLine('=== RSpade IDE Bridge Client Initialized ===');
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
// Watch domain.txt for changes
this.setup_domain_file_watcher();
}
/**
* Make authenticated request to IDE helper endpoint
* Handles auto-discovery, authentication, signing, and retry logic
*/
public async request(endpoint: string, data: any = {}, method: string = 'GET'): Promise<any> {
this.output_channel.appendLine(`\n=== IDE BRIDGE REQUEST ===`);
this.output_channel.appendLine(`Endpoint: ${endpoint}`);
this.output_channel.appendLine(`Method: ${method}`);
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
return this.make_request_with_retry(endpoint, data, method, 0);
}
private async make_request_with_retry(
endpoint: string,
data: any,
method: string,
retry_count: number
): Promise<any> {
if (retry_count > 0) {
this.output_channel.appendLine(`\n--- RETRY ATTEMPT ${retry_count} ---`);
}
// Ensure we have server URL (force refresh on retry)
await this.discover_server_url(retry_count > 0);
// For authenticated endpoints, ensure we have auth session
const needs_auth = !endpoint.includes('/auth/create');
if (needs_auth) {
await this.ensure_auth(retry_count > 0);
}
try {
return await this.make_http_request(endpoint, data, method, needs_auth);
} catch (error: any) {
// 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...');
this.auth_data = null;
return this.make_request_with_retry(endpoint, data, method, retry_count + 1);
}
// Connection failure - server URL might have changed
if (error_msg.includes('ECONNREFUSED') || error_msg.includes('ENOTFOUND') ||
error_msg.includes('ETIMEDOUT') || error_msg.includes('getaddrinfo')) {
this.output_channel.appendLine('Connection failed, refreshing server URL...');
this.auth_data = null;
this.server_url = null;
return this.make_request_with_retry(endpoint, data, method, retry_count + 1);
}
}
// Not recoverable or already retried - fail loud
this.show_error_status('IDE Bridge request failed');
throw error;
}
}
private async make_http_request(
endpoint: string,
data: any,
method: string,
needs_auth: boolean
): Promise<any> {
return new Promise((resolve, reject) => {
if (!this.server_url) {
const error = new Error('Server URL not configured');
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
return;
}
const url = new URL(this.server_url);
const is_https = url.protocol === 'https:';
const http_module = is_https ? https : http;
// For GET requests, encode data as query string
let body = '';
let full_path = endpoint;
if (method === 'GET' && Object.keys(data).length > 0) {
const params = new URLSearchParams(data);
full_path += (endpoint.includes('?') ? '&' : '?') + params.toString();
} else if (method === 'POST') {
body = JSON.stringify(data);
}
const headers: any = {
'Content-Type': 'application/json'
};
if (body) {
headers['Content-Length'] = Buffer.byteLength(body);
}
// Add authentication headers if needed
if (needs_auth && this.auth_data) {
const signature = crypto.createHash('sha1')
.update(body + this.auth_data.client_key)
.digest('hex');
headers['X-Session'] = this.auth_data.session;
headers['X-Signature'] = signature;
this.output_channel.appendLine(`Session: ${this.auth_data.session}`);
this.output_channel.appendLine(`Signature: ${signature}`);
}
const options: https.RequestOptions = {
hostname: url.hostname,
port: url.port || (is_https ? 443 : 80),
path: full_path,
method: method,
headers: headers,
timeout: 30000,
rejectUnauthorized: false
};
this.output_channel.appendLine(`Full URL: ${is_https ? 'https' : 'http'}://${options.hostname}:${options.port}${options.path}`);
const start_time = Date.now();
const req = http_module.request(options, (res) => {
let response_data = '';
this.output_channel.appendLine(`\n--- HTTP RESPONSE ---`);
this.output_channel.appendLine(`Status: ${res.statusCode}`);
res.on('data', (chunk) => {
response_data += chunk;
});
res.on('end', () => {
const elapsed = Date.now() - start_time;
this.output_channel.appendLine(`Response time: ${elapsed}ms`);
this.output_channel.appendLine(`Response size: ${response_data.length} bytes`);
try {
// Validate server signature if authenticated request
if (needs_auth && this.auth_data) {
const response_signature = res.headers['x-signature'] as string;
this.output_channel.appendLine(`\n--- SIGNATURE VALIDATION ---`);
this.output_channel.appendLine(`Response signature: ${response_signature || 'MISSING'}`);
if (response_signature) {
const expected_signature = crypto.createHash('sha1')
.update(response_data + this.auth_data.server_key)
.digest('hex');
this.output_channel.appendLine(`Expected signature: ${expected_signature}`);
this.output_channel.appendLine(`Validation: ${response_signature === expected_signature ? 'PASSED' : 'FAILED'}`);
if (response_signature !== expected_signature) {
const error = new Error('Invalid server signature');
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
return;
}
}
}
const response = JSON.parse(response_data);
if (res.statusCode !== 200) {
const error = new Error(response.error || `HTTP ${res.statusCode}`);
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
} else {
this.output_channel.appendLine('Result: SUCCESS');
this.clear_status_bar();
resolve(response);
}
} catch (e: any) {
const error = new Error(`Failed to parse response: ${e.message}`);
this.output_channel.appendLine(`ERROR: ${error.message}`);
reject(error);
}
});
});
req.on('error', (error) => {
const elapsed = Date.now() - start_time;
this.output_channel.appendLine(`\nERROR after ${elapsed}ms: ${error.message}`);
reject(error);
});
req.on('timeout', () => {
this.output_channel.appendLine('\nERROR: Request timed out after 30 seconds');
req.destroy();
reject(new Error('Request timed out'));
});
if (body) {
req.write(body);
}
req.end();
});
}
private async ensure_auth(force_new: boolean = false): Promise<void> {
if (this.auth_data && !force_new) {
this.output_channel.appendLine(`Using existing auth session: ${this.auth_data.session}`);
return;
}
this.output_channel.appendLine('\n--- AUTHENTICATION ---');
this.output_channel.appendLine('Creating new auth session...');
// Request new session (this endpoint doesn't require auth)
const response = await this.make_http_request('/auth/create', {}, 'POST', false);
if (!response.success) {
throw new Error('Failed to create auth session');
}
this.auth_data = {
session: response.session,
client_key: response.client_key,
server_key: response.server_key
};
this.output_channel.appendLine(`✅ Auth session created successfully!`);
this.output_channel.appendLine(` Server: ${this.server_url}`);
this.output_channel.appendLine(` Session ID: ${this.auth_data.session}`);
this.output_channel.appendLine(` Client Key: ${this.auth_data.client_key.substring(0, 8)}...`);
this.output_channel.appendLine(` Server Key: ${this.auth_data.server_key.substring(0, 8)}...`);
}
private async discover_server_url(force_refresh: boolean = false): Promise<void> {
if (this.server_url && !force_refresh) {
this.output_channel.appendLine(`Using cached server URL: ${this.server_url}`);
return;
}
this.output_channel.appendLine('\n--- SERVER URL DISCOVERY ---');
if (force_refresh) {
this.output_channel.appendLine('Re-checking server URL due to connection failure...');
}
// Check VS Code settings first
const config = vscode.workspace.getConfiguration('rspade');
const configured_url = config.get<string>('serverUrl');
this.output_channel.appendLine(`VS Code setting: ${configured_url ? 'found' : 'not found'}`);
if (configured_url) {
this.server_url = await this.negotiate_protocol(configured_url);
this.output_channel.appendLine(`✅ Using configured server URL: ${this.server_url}`);
return;
}
// Auto-discover from domain file
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
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');
this.output_channel.appendLine(`Checking for domain file: ${domain_file}`);
if (await exists(domain_file)) {
const domain = (await read_file(domain_file, 'utf8')).trim();
this.server_url = await this.negotiate_protocol(domain);
this.output_channel.appendLine(`✅ Auto-discovered server URL from domain.txt: ${this.server_url}`);
return;
}
// 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.');
}
private async negotiate_protocol(url_or_hostname: string): Promise<string> {
// Parse the input to extract hostname
let hostname: string;
// If it looks like a URL, parse it
if (url_or_hostname.includes('://')) {
const parsed = new URL(url_or_hostname);
hostname = parsed.hostname;
// If we have a cached protocol from previous successful connection, use it
if (this.cached_protocol) {
this.output_channel.appendLine(`Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
} else {
hostname = url_or_hostname;
}
// If we have a cached protocol, use it
if (this.cached_protocol) {
this.output_channel.appendLine(`Using cached protocol: ${this.cached_protocol}`);
return `${this.cached_protocol}//${hostname}`;
}
// Try HTTPS first
this.output_channel.appendLine(`Negotiating protocol for: ${hostname}`);
const https_url = `https://${hostname}`;
if (await this.test_connection(https_url)) {
this.output_channel.appendLine(`✅ HTTPS connection successful`);
this.cached_protocol = 'https:';
return https_url;
}
this.output_channel.appendLine(`HTTPS failed, trying HTTP...`);
const http_url = `http://${hostname}`;
if (await this.test_connection(http_url)) {
this.output_channel.appendLine(`✅ HTTP connection successful`);
this.cached_protocol = 'http:';
return http_url;
}
throw new Error(`Unable to connect to ${hostname} via HTTPS or HTTP`);
}
private async test_connection(base_url: string): Promise<boolean> {
try {
const parsed_url = new URL(base_url);
const is_https = parsed_url.protocol === 'https:';
const port = parsed_url.port || (is_https ? 443 : 80);
const test_path = '/_ide/service/auth/create';
return new Promise<boolean>((resolve) => {
const options = {
hostname: parsed_url.hostname,
port: port,
path: test_path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': '2'
},
timeout: 5000,
rejectUnauthorized: false
};
const client = is_https ? https : http;
const req = client.request(options, (res) => {
// 200 OK means success
// 301/302 redirect means we should try the other protocol
if (res.statusCode === 301 || res.statusCode === 302) {
this.output_channel.appendLine(`Got redirect ${res.statusCode} from ${base_url}`);
resolve(false);
} else {
resolve(res.statusCode === 200);
}
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.write('{}');
req.end();
});
} catch (error) {
return false;
}
}
private setup_domain_file_watcher() {
const rspade_root = this.find_rspade_root();
if (!rspade_root) {
return;
}
const domain_file = path.join(rspade_root, '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)) {
try {
this.domain_file_watcher = fs.watch(domain_dir, (eventType, filename) => {
if (filename === 'domain.txt' && eventType === 'change') {
this.output_channel.appendLine('[IdeBridge] domain.txt changed, clearing cached URL and protocol');
this.server_url = null;
this.cached_protocol = null;
this.auth_data = null;
// Cancel any pending retries
if (this.retry_timer) {
clearTimeout(this.retry_timer);
this.retry_timer = null;
}
}
});
} catch (error) {
this.output_channel.appendLine(`Failed to set up domain file watcher: ${error}`);
}
}
}
private schedule_retry() {
// Don't schedule multiple retries
if (this.retry_timer) {
return;
}
this.output_channel.appendLine(`Scheduling retry in ${this.retry_interval / 1000} seconds...`);
this.retry_timer = setTimeout(() => {
this.output_channel.appendLine('Retrying server URL discovery...');
this.retry_timer = null;
this.server_url = null;
// Next request will trigger discovery
}, this.retry_interval);
}
private find_rspade_root(): string | undefined {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
for (const folder of vscode.workspace.workspaceFolders) {
// Try new structure first
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');
}
// Fall back to legacy structure
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
if (fs.existsSync(app_rspade)) {
return folder.uri.fsPath;
}
}
return undefined;
}
private show_error_status(message: string) {
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';
}
this.status_bar_item.text = `$(error) RSpade: ${message}`;
this.status_bar_item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
this.status_bar_item.show();
setTimeout(() => {
this.clear_status_bar();
}, 5000);
}
private clear_status_bar() {
if (this.status_bar_item) {
this.status_bar_item.hide();
}
}
private show_detailed_error() {
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
this.output_channel.appendLine('ERROR: RSpade Extension cannot connect to server');
this.output_channel.appendLine('════════════════════════════════════════════════════════');
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('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');
this.output_channel.appendLine('3. Enable IDE integration in Laravel config');
this.output_channel.appendLine(' In config/rsx.php, set ide_integration.enabled = true');
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
}
}

View File

@@ -0,0 +1,527 @@
import * as vscode from 'vscode';
import { IdeBridgeClient } from './ide_bridge_client';
/**
* JQHTML lifecycle methods that are called automatically by the framework
*/
const JQHTML_LIFECYCLE_METHODS = ['on_render', 'on_create', 'on_load', 'on_ready', 'on_destroy'];
/**
* Convention methods that are called automatically by the RSX framework
*/
const CONVENTION_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready',
'on_jqhtml_ready'
];
/**
* Lifecycle method documentation
*/
const LIFECYCLE_DOCS: { [key: string]: string } = {
on_render: 'Initial render phase - template DOM created. Parent completes before children. DOM manipulation allowed. MUST be synchronous.',
on_create: 'Quick setup after DOM exists. Children complete before parent. DOM manipulation allowed. MUST be synchronous.',
on_load: 'Data loading phase - fetch async data. NO DOM manipulation allowed, only update `this.data`. Template re-renders after load. MUST be async.',
on_ready: 'Final setup phase - all data loaded. Children complete before parent. DOM manipulation allowed. Can be sync or async.',
on_destroy: 'Component destruction phase - cleanup resources. Called before component is removed. MUST be synchronous.',
};
/**
* Cache for lineage lookups
*/
const lineage_cache = new Map<string, string[]>();
/**
* IDE Bridge client instance (shared across all providers)
*/
let ide_bridge_client: IdeBridgeClient | null = null;
/**
* Get JavaScript class lineage from backend via IDE bridge
*/
async function get_js_lineage(class_name: string): Promise<string[]> {
// Check cache first
if (lineage_cache.has(class_name)) {
return lineage_cache.get(class_name)!;
}
// Initialize IDE bridge client if needed
if (!ide_bridge_client) {
const output_channel = vscode.window.createOutputChannel('RSpade JQHTML Lifecycle');
ide_bridge_client = new IdeBridgeClient(output_channel);
}
try {
const response = await ide_bridge_client.request('/_idehelper/js_lineage', { class: class_name });
const lineage = response.lineage || [];
// Cache the result
lineage_cache.set(class_name, lineage);
return lineage;
} 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}`);
}
}
/**
* Extract class name from document text
*/
function extract_class_name(text: string): string | null {
const regex = /class\s+([A-Za-z0-9_]+)\s+extends/;
const match = regex.exec(text);
return match ? match[1] : null;
}
/**
* Check if class extends Jqhtml_Component
*/
function directly_extends_jqhtml(text: string): boolean {
const regex = /class\s+[A-Za-z0-9_]+\s+extends\s+Jqhtml_Component/;
return regex.test(text);
}
/**
* Check if class has extends clause
*/
function has_extends_clause(text: string): boolean {
const regex = /class\s+[A-Za-z0-9_]+\s+extends\s+[A-Za-z0-9_]+/;
return regex.test(text);
}
/**
* Check if a method is async
*/
function is_async_method(line_text: string): boolean {
return line_text.trim().startsWith('async ');
}
/**
* Check if position is inside a comment
*/
function is_in_comment(document: vscode.TextDocument, position: vscode.Position): boolean {
const line_text = document.lineAt(position.line).text;
const char_pos = position.character;
// Check for single-line comment
const single_comment_idx = line_text.indexOf('//');
if (single_comment_idx !== -1 && single_comment_idx < char_pos) {
return true;
}
// Check for multi-line comment by looking at text before position
const text_before = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
let in_block_comment = false;
let i = 0;
while (i < text_before.length) {
if (text_before.substring(i, i + 2) === '/*') {
in_block_comment = true;
i += 2;
} else if (text_before.substring(i, i + 2) === '*/') {
in_block_comment = false;
i += 2;
} else {
i++;
}
}
return in_block_comment;
}
/**
* Provides semantic tokens for JQHTML lifecycle methods (amber color)
*/
export class JqhtmlLifecycleSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
console.log(`[JQHTML] provideDocumentSemanticTokens called for: ${document.fileName}`);
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
console.log(`[JQHTML] Skipping - not JS/TS, language is: ${document.languageId}`);
return tokens_builder.build();
}
const text = document.getText();
// Quick check: does this file have an extends clause?
if (!has_extends_clause(text)) {
console.log(`[JQHTML] No extends clause found, checking for convention methods only`);
// Still process convention methods even without extends
} else {
console.log(`[JQHTML] Found extends clause`);
}
// 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
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}`);
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}`);
}
}
// Highlight lifecycle methods (only if extends Jqhtml_Component)
if (extends_jqhtml) {
console.log(`[JQHTML] Scanning for JQHTML lifecycle methods...`);
let lifecycle_count = 0;
for (const method_name of JQHTML_LIFECYCLE_METHODS) {
const regex = new RegExp(`\\b(async\\s+)?(${method_name})\\s*\\(`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
const method_start = match.index + (match[1] ? match[1].length : 0);
const position = document.positionAt(method_start);
// Skip if this is inside a comment
if (is_in_comment(document, position)) {
console.log(`[JQHTML] Skipping ${method_name} - inside comment`);
continue;
}
console.log(`[JQHTML] Found lifecycle method: ${method_name} at line ${position.line}`);
tokens_builder.push(position.line, position.character, method_name.length, 0, 0);
lifecycle_count++;
}
}
console.log(`[JQHTML] Total lifecycle methods highlighted: ${lifecycle_count}`);
}
// Highlight convention methods (for all classes)
console.log(`[JQHTML] Scanning for convention methods...`);
let convention_count = 0;
for (const method_name of CONVENTION_METHODS) {
const regex = new RegExp(`\\b(static\\s+)?(async\\s+)?(${method_name})\\s*\\(`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
const prefix_length = (match[1] ? match[1].length : 0) + (match[2] ? match[2].length : 0);
const method_start = match.index + prefix_length;
const position = document.positionAt(method_start);
// Skip if this is inside a comment
if (is_in_comment(document, position)) {
console.log(`[JQHTML] Skipping ${method_name} - inside comment`);
continue;
}
console.log(`[JQHTML] Found convention method: ${method_name} at line ${position.line}`);
tokens_builder.push(position.line, position.character, method_name.length, 0, 0);
convention_count++;
}
}
console.log(`[JQHTML] Total convention methods highlighted: ${convention_count}`);
// Highlight @Instantiatable in JSDoc comments
console.log(`[JQHTML] Scanning for @Instantiatable annotations...`);
let instantiatable_count = 0;
const instantiatable_regex = /@(Instantiatable)\b/g;
let instantiatable_match;
while ((instantiatable_match = instantiatable_regex.exec(text)) !== null) {
const annotation_start = instantiatable_match.index + 1; // Skip the @ symbol
const position = document.positionAt(annotation_start);
console.log(`[JQHTML] Found @Instantiatable at line ${position.line}`);
tokens_builder.push(position.line, position.character, 'Instantiatable'.length, 0, 0);
instantiatable_count++;
}
console.log(`[JQHTML] Total @Instantiatable annotations highlighted: ${instantiatable_count}`);
const result = tokens_builder.build();
console.log(`[JQHTML] Returning ${result.data.length} semantic tokens`);
return result;
}
}
/**
* Provides hover information for JQHTML lifecycle methods
*/
export class JqhtmlLifecycleHoverProvider implements vscode.HoverProvider {
async provideHover(document: vscode.TextDocument, position: vscode.Position): Promise<vscode.Hover | undefined> {
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return undefined;
}
const word = document.getText(word_range);
if (!JQHTML_LIFECYCLE_METHODS.includes(word)) {
return undefined;
}
// Check if this is a method definition
const line = document.lineAt(position.line).text;
if (!line.includes('(')) {
return undefined;
}
// Check if class extends Jqhtml_Component
const text = document.getText();
if (!has_extends_clause(text)) {
return undefined;
}
const is_jqhtml = directly_extends_jqhtml(text);
let extends_jqhtml = is_jqhtml;
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');
}
}
if (!extends_jqhtml) {
return undefined;
}
const markdown = new vscode.MarkdownString();
markdown.isTrusted = true;
const is_async = is_async_method(line);
// Determine if async is required, forbidden, or optional
const must_be_sync = ['on_create', 'on_render', 'on_destroy'].includes(word);
const must_be_async = word === 'on_load';
const can_be_either = word === 'on_ready';
let has_error = false;
if (must_be_sync && is_async) {
markdown.appendMarkdown(`**⚠️ Incorrect Async Declaration**\n\n`);
markdown.appendMarkdown(`\`${word}\` must be synchronous - remove 'async' keyword.\n\n`);
has_error = true;
} else if (must_be_async && !is_async) {
markdown.appendMarkdown(`**⚠️ Missing Async Declaration**\n\n`);
markdown.appendMarkdown(`\`${word}\` must be async - add 'async' keyword.\n\n`);
has_error = true;
}
if (!has_error) {
markdown.appendMarkdown(`**JQHTML Lifecycle Method**\n\n`);
}
markdown.appendMarkdown(`${LIFECYCLE_DOCS[word]}\n\n`);
markdown.appendMarkdown(`Run \`php artisan rsx:man jqhtml\` for more information.`);
return new vscode.Hover(markdown, word_range);
}
}
/**
* Diagnostic provider for non-async lifecycle methods
*/
export class JqhtmlLifecycleDiagnosticProvider {
private diagnostics_collection: vscode.DiagnosticCollection;
private document_cache = new Map<string, boolean>();
constructor() {
this.diagnostics_collection = vscode.languages.createDiagnosticCollection('rspade-jqhtml');
}
activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
this.update_diagnostics(event.document);
})
);
vscode.workspace.textDocuments.forEach((document) => {
this.update_diagnostics(document);
});
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => {
this.update_diagnostics(document);
})
);
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((document) => {
// Clear cache on save to force lineage re-check
this.document_cache.delete(document.uri.toString());
this.update_diagnostics(document);
})
);
}
private async update_diagnostics(document: vscode.TextDocument) {
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return;
}
const text = document.getText();
if (!has_extends_clause(text)) {
return;
}
// Check cache
const cache_key = document.uri.toString();
let extends_jqhtml = this.document_cache.get(cache_key);
if (extends_jqhtml === undefined) {
const is_jqhtml = directly_extends_jqhtml(text);
extends_jqhtml = is_jqhtml;
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');
}
}
this.document_cache.set(cache_key, extends_jqhtml);
}
if (!extends_jqhtml) {
return;
}
const diagnostics: vscode.Diagnostic[] = [];
const lines = text.split('\n');
// Find each lifecycle method and validate
for (const method_name of JQHTML_LIFECYCLE_METHODS) {
// Find method definition and extract its body
const method_regex = new RegExp(`^\\s*(static\\s+)?(async\\s+)?(${method_name})\\s*\\([^)]*\\)\\s*\\{`, 'gm');
let method_match;
while ((method_match = method_regex.exec(text)) !== null) {
const is_static = !!method_match[1];
const is_async = !!method_match[2];
const method_start_index = method_match.index + method_match[0].indexOf(method_name);
const method_start_pos = document.positionAt(method_start_index);
const method_end_pos = document.positionAt(method_start_index + method_name.length);
const method_name_range = new vscode.Range(method_start_pos, method_end_pos);
// Check if method is static (should not be)
if (is_static) {
diagnostics.push(
new vscode.Diagnostic(
method_name_range,
`JQHTML lifecycle method '${method_name}' should not be static`,
vscode.DiagnosticSeverity.Warning
)
);
}
// Check async requirements
if (method_name === 'on_create' && is_async) {
diagnostics.push(
new vscode.Diagnostic(
method_name_range,
`'on_create' must be synchronous - remove 'async' keyword`,
vscode.DiagnosticSeverity.Error
)
);
} else if (method_name === 'on_render' && is_async) {
diagnostics.push(
new vscode.Diagnostic(
method_name_range,
`'on_render' must be synchronous - remove 'async' keyword`,
vscode.DiagnosticSeverity.Error
)
);
} else if (method_name === 'on_destroy' && is_async) {
diagnostics.push(
new vscode.Diagnostic(
method_name_range,
`'on_destroy' must be synchronous - remove 'async' keyword`,
vscode.DiagnosticSeverity.Error
)
);
} else if (method_name === 'on_load' && !is_async) {
diagnostics.push(
new vscode.Diagnostic(
method_name_range,
`'on_load' must be async - add 'async' keyword`,
vscode.DiagnosticSeverity.Error
)
);
}
// on_ready can be either async or non-async - no requirement
// Find method body to check for violations
const method_body_start = method_match.index + method_match[0].length;
let brace_count = 1;
let body_end = method_body_start;
for (let i = method_body_start; i < text.length && brace_count > 0; i++) {
if (text[i] === '{') brace_count++;
if (text[i] === '}') brace_count--;
if (brace_count === 0) {
body_end = i;
break;
}
}
const method_body = text.substring(method_body_start, body_end);
// Check for violations in method body
if (method_name === 'on_create') {
// Check for this.data or that.data access
const data_access_regex = /(this|that)\.data/g;
let data_match;
while ((data_match = data_access_regex.exec(method_body)) !== null) {
const violation_pos = document.positionAt(method_body_start + data_match.index);
const violation_end = document.positionAt(method_body_start + data_match.index + data_match[0].length);
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(violation_pos, violation_end),
`'${data_match[0]}' is populated during on_load, which happens after on_create. Did you mean ${data_match[1]}.args?`,
vscode.DiagnosticSeverity.Warning
)
);
}
}
if (method_name === 'on_load') {
// Check for DOM access: this.$, this.$id, that.$, that.$id
const dom_access_regex = /(this|that)\.\$(?:id)?/g;
let dom_match;
while ((dom_match = dom_access_regex.exec(method_body)) !== null) {
const violation_pos = document.positionAt(method_body_start + dom_match.index);
const violation_end = document.positionAt(method_body_start + dom_match.index + dom_match[0].length);
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(violation_pos, violation_end),
`'on_load' should not access DOM elements. It should be headless, using only ${dom_match[1]}.args for inputs and setting ${dom_match[1]}.data for outputs`,
vscode.DiagnosticSeverity.Warning
)
);
}
}
}
}
this.diagnostics_collection.set(document.uri, diagnostics);
}
dispose() {
this.diagnostics_collection.dispose();
}
}

View File

@@ -0,0 +1,289 @@
import * as vscode from 'vscode';
export class LaravelCompletionProvider implements vscode.CompletionItemProvider {
private laravel_functions: Map<string, CompletionItemData> = new Map();
constructor() {
this.initialize_laravel_functions();
}
private initialize_laravel_functions() {
// Path Helpers
this.add_function('base_path', 'base_path(string $path = \'\'): string',
'Get the base path of the Laravel installation. Optionally append a path.');
this.add_function('app_path', 'app_path(string $path = \'\'): string',
'Get the path to the app folder. Optionally append a path.');
this.add_function('config_path', 'config_path(string $path = \'\'): string',
'Get the configuration path. Optionally append a path.');
this.add_function('database_path', 'database_path(string $path = \'\'): string',
'Get the database path. Optionally append a path.');
this.add_function('public_path', 'public_path(string $path = \'\'): string',
'Get the public path. Optionally append a path.');
this.add_function('resource_path', 'resource_path(string $path = \'\'): string',
'Get the path to the resources folder. Optionally append a path.');
this.add_function('storage_path', 'storage_path(string $path = \'\'): string',
'Get the path to the storage folder. Optionally append a path.');
// Environment & Config
this.add_function('env', 'env(string $key, mixed $default = null): mixed',
'Get the value of an environment variable. Supports a default value.');
this.add_function('config', 'config(string|array|null $key = null, mixed $default = null): mixed',
'Get/set a configuration value. Pass an array to set multiple values.');
this.add_function('app', 'app(string|null $abstract = null, array $parameters = []): mixed',
'Get the available container instance or resolve a type from the container.');
// URL & Asset Helpers
this.add_function('url', 'url(string|null $path = null, mixed $parameters = [], bool|null $secure = null): string',
'Generate a URL for the application.');
this.add_function('asset', 'asset(string $path, bool|null $secure = null): string',
'Generate an asset URL.');
this.add_function('secure_asset', 'secure_asset(string $path): string',
'Generate an asset URL using HTTPS.');
this.add_function('route', 'route(string $name, mixed $parameters = [], bool $absolute = true): string',
'Generate the URL to a named route.');
this.add_function('mix', 'mix(string $path, string $manifestDirectory = \'\'): string',
'Get the path to a versioned Mix file.');
// Request & Response
this.add_function('request', 'request(array|string|null $key = null, mixed $default = null): mixed',
'Get an instance of the current request or an input item from the request.');
this.add_function('response', 'response(mixed $content = \'\', int $status = 200, array $headers = []): mixed',
'Create a new response instance.');
this.add_function('redirect', 'redirect(string|null $to = null, int $status = 302, array $headers = [], bool|null $secure = null): mixed',
'Get an instance of the redirector or create a new redirect response.');
this.add_function('back', 'back(int $status = 302, array $headers = [], mixed $fallback = false): mixed',
'Create a new redirect response to the previous location.');
this.add_function('abort', 'abort(int $code, string $message = \'\', array $headers = []): never',
'Throw an HttpException with the given data.');
this.add_function('abort_if', 'abort_if(bool $boolean, int $code, string $message = \'\', array $headers = []): void',
'Throw an HttpException with the given data if the given condition is true.');
this.add_function('abort_unless', 'abort_unless(bool $boolean, int $code, string $message = \'\', array $headers = []): void',
'Throw an HttpException with the given data unless the given condition is true.');
// Authentication & Session
this.add_function('auth', 'auth(string|null $guard = null): mixed',
'Get the available auth instance or a specific guard.');
this.add_function('session', 'session(array|string|null $key = null, mixed $default = null): mixed',
'Get/set the specified session value.');
this.add_function('old', 'old(string|null $key = null, mixed $default = null): mixed',
'Retrieve an old input item.');
this.add_function('cookie', 'cookie(string|null $name = null, string|null $value = null, int $minutes = 0, ...): mixed',
'Create a new cookie instance.');
this.add_function('csrf_token', 'csrf_token(): string',
'Get the CSRF token value.');
this.add_function('csrf_field', 'csrf_field(): string',
'Generate a CSRF token form field.');
// Caching
this.add_function('cache', 'cache(mixed ...$arguments): mixed',
'Get/set the specified cache value.');
// Collections & Arrays
this.add_function('collect', 'collect(mixed $value = null): mixed',
'Create a collection from the given value.');
this.add_function('data_get', 'data_get(mixed $target, string|array|null $key, mixed $default = null): mixed',
'Get an item from an array or object using "dot" notation.');
this.add_function('data_set', 'data_set(mixed &$target, string|array $key, mixed $value, bool $overwrite = true): mixed',
'Set an item on an array or object using dot notation.');
this.add_function('data_forget', 'data_forget(mixed &$target, string|array $key): mixed',
'Remove an item from an array or object using "dot" notation.');
// Debugging
this.add_function('dd', 'dd(mixed ...$vars): never',
'Dump the given variables and end the script.');
this.add_function('dump', 'dump(mixed ...$vars): void',
'Dump the given variables.');
this.add_function('info', 'info(string $message, array $context = []): void',
'Write some information to the log.');
this.add_function('logger', 'logger(string|null $message = null, array $context = []): mixed',
'Log a debug message to the logs or get a logger instance.');
// Events & Jobs
this.add_function('event', 'event(mixed ...$args): mixed',
'Dispatch an event and call the listeners.');
this.add_function('dispatch', 'dispatch(mixed $job): mixed',
'Dispatch a job to its appropriate handler.');
this.add_function('dispatch_now', 'dispatch_now(mixed $job): mixed',
'Dispatch a job immediately (synchronously).');
this.add_function('dispatch_sync', 'dispatch_sync(mixed $job): mixed',
'Dispatch a job synchronously.');
// Encryption & Hashing
this.add_function('bcrypt', 'bcrypt(string $value, array $options = []): string',
'Hash the given value using Bcrypt.');
this.add_function('encrypt', 'encrypt(mixed $value, bool $serialize = true): string',
'Encrypt the given value.');
this.add_function('decrypt', 'decrypt(string $payload, bool $unserialize = true): mixed',
'Decrypt the given value.');
// Date & Time
this.add_function('now', 'now(mixed $tz = null): mixed',
'Create a new Carbon instance for the current time.');
this.add_function('today', 'today(mixed $tz = null): mixed',
'Create a new Carbon instance for the current date.');
// Translation
this.add_function('trans', 'trans(string|null $key = null, array $replace = [], string|null $locale = null): string',
'Translate the given message.');
this.add_function('trans_choice', 'trans_choice(string $key, int|array|\\Countable $number, array $replace = [], string|null $locale = null): string',
'Translate the given message with a count.');
this.add_function('__', '__(string|null $key = null, array $replace = [], string|null $locale = null): string',
'Translate the given message (alias for trans).');
// Validation
this.add_function('validator', 'validator(array $data = [], array $rules = [], array $messages = [], array $customAttributes = []): mixed',
'Create a new Validator instance.');
// Other Laravel Helpers
this.add_function('class_basename', 'class_basename(string|object $class): string',
'Get the class "basename" of the given object/class.');
this.add_function('class_uses_recursive', 'class_uses_recursive(string|object $class): array',
'Returns all traits used by a class, its parent classes and trait of their traits.');
this.add_function('trait_uses_recursive', 'trait_uses_recursive(string|object $trait): array',
'Returns all traits used by a trait and its traits.');
this.add_function('value', 'value(mixed $value, mixed ...$args): mixed',
'Return the default value of the given value.');
this.add_function('with', 'with(mixed $value, callable|null $callback = null): mixed',
'Return the given value, optionally passed through the given callback.');
this.add_function('tap', 'tap(mixed $value, callable|null $callback = null): mixed',
'Call the given Closure with the given value then return the value.');
this.add_function('blank', 'blank(mixed $value): bool',
'Determine if the given value is "blank".');
this.add_function('filled', 'filled(mixed $value): bool',
'Determine if a value is "filled".');
this.add_function('optional', 'optional(mixed $value, callable|null $callback = null): mixed',
'Provide access to optional objects.');
this.add_function('rescue', 'rescue(callable $callback, mixed $rescue = null, bool|callable $report = true): mixed',
'Catch a potential exception and return a default value.');
this.add_function('retry', 'retry(int $times, callable $callback, int|\\Closure $sleepMilliseconds = 0, callable|null $when = null): mixed',
'Retry an operation a given number of times.');
this.add_function('throw_if', 'throw_if(mixed $condition, \\Throwable|string $exception = \'RuntimeException\', mixed ...$parameters): mixed',
'Throw the given exception if the given condition is true.');
this.add_function('throw_unless', 'throw_unless(mixed $condition, \\Throwable|string $exception = \'RuntimeException\', mixed ...$parameters): mixed',
'Throw the given exception unless the given condition is true.');
this.add_function('windows_os', 'windows_os(): bool',
'Determine whether the current environment is Windows based.');
}
private add_function(name: string, signature: string, documentation: string) {
this.laravel_functions.set(name, {
name,
signature,
documentation
});
}
public provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext
): vscode.CompletionItem[] {
const line_text = document.lineAt(position).text;
const before_cursor = line_text.substring(0, position.character);
// Check if we're in a position where a function name might be typed
const function_pattern = /([a-z_][a-z0-9_]*)?$/i;
const match = before_cursor.match(function_pattern);
if (!match) {
return [];
}
const prefix = match[1] || '';
const completion_items: vscode.CompletionItem[] = [];
this.laravel_functions.forEach((func_data, func_name) => {
if (func_name.toLowerCase().startsWith(prefix.toLowerCase())) {
const item = new vscode.CompletionItem(
func_name,
vscode.CompletionItemKind.Function
);
item.detail = func_data.signature;
item.documentation = new vscode.MarkdownString(func_data.documentation);
// Create snippet for function with parentheses
const params = this.extract_parameters(func_data.signature);
if (params.length > 0) {
// Create snippet with placeholders for required parameters
const snippet_params = params
.filter(p => !p.optional)
.map((p, i) => `\${${i + 1}:${p.name}}`)
.join(', ');
item.insertText = new vscode.SnippetString(`${func_name}(${snippet_params})`);
} else {
item.insertText = new vscode.SnippetString(`${func_name}()`);
}
item.sortText = '0' + func_name; // Prioritize Laravel functions
completion_items.push(item);
}
});
return completion_items;
}
private extract_parameters(signature: string): Array<{name: string, optional: boolean}> {
const params: Array<{name: string, optional: boolean}> = [];
// Extract everything between parentheses
const match = signature.match(/\((.*?)\)/);
if (!match) {
return params;
}
const params_str = match[1];
if (!params_str.trim()) {
return params;
}
// Split by comma but handle nested parentheses/brackets
const param_parts = this.smart_split(params_str, ',');
for (const param of param_parts) {
const param_match = param.match(/\$([a-z_][a-z0-9_]*)/i);
if (param_match) {
const param_name = param_match[1];
const is_optional = param.includes('=');
params.push({ name: param_name, optional: is_optional });
}
}
return params;
}
private smart_split(str: string, delimiter: string): string[] {
const result: string[] = [];
let current = '';
let depth = 0;
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === '(' || char === '[' || char === '{') {
depth++;
} else if (char === ')' || char === ']' || char === '}') {
depth--;
} else if (char === delimiter && depth === 0) {
result.push(current.trim());
current = '';
continue;
}
current += char;
}
if (current.trim()) {
result.push(current.trim());
}
return result;
}
}
interface CompletionItemData {
name: string;
signature: string;
documentation: string;
}

View File

@@ -0,0 +1,52 @@
import * as vscode from 'vscode';
/**
* Framework PHP attributes that should be highlighted
*/
const FRAMEWORK_ATTRIBUTES = [
'Ajax_Endpoint',
'Route',
'Auth',
'Relationship',
'Monoprogenic',
'Instantiatable'
];
/**
* Provides semantic tokens for PHP attributes (amber color)
*/
export class PhpAttributeSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'php') {
return tokens_builder.build();
}
const text = document.getText();
// Find all PHP attributes: #[AttributeName] or #[\AttributeName]
for (const attribute_name of FRAMEWORK_ATTRIBUTES) {
// Match: #[AttributeName or #[\AttributeName with optional namespace prefix
// Captures the attribute name only (not brackets or backslash)
const regex = new RegExp(`#\\[\\\\?(${attribute_name})(?:\\s|\\(|\\])`, 'g');
let match;
while ((match = regex.exec(text)) !== null) {
// match[1] contains the attribute name without namespace prefix
const attr_start = match.index + match[0].indexOf(match[1]);
const position = document.positionAt(attr_start);
tokens_builder.push(
position.line,
position.character,
attribute_name.length,
0, // token type index for 'conventionMethod'
0 // token modifiers
);
}
}
return tokens_builder.build();
}
}

View File

@@ -0,0 +1,65 @@
/**
* RSpade Refactor Code Actions Provider
*
* Provides refactoring actions that appear in the "Refactor..." menu
* when the cursor is on a static method definition or call.
*/
import * as vscode from 'vscode';
import { RspadeRefactorProvider } from './refactor_provider';
export class RspadeRefactorCodeActionsProvider implements vscode.CodeActionProvider {
private refactor_provider: RspadeRefactorProvider;
constructor(refactor_provider: RspadeRefactorProvider) {
this.refactor_provider = refactor_provider;
}
public provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): vscode.CodeAction[] | undefined {
// Only provide actions for PHP files in ./rsx or ./app/RSpade
if (document.languageId !== 'php') {
return undefined;
}
const file_path = document.uri.fsPath;
if (!file_path.includes('/rsx/') && !file_path.includes('\\rsx\\') &&
!file_path.includes('/app/RSpade') && !file_path.includes('\\app\\RSpade')) {
return undefined;
}
// Check if line contains a static method (cursor can be anywhere on the line)
const position = range.start;
const line = document.lineAt(position.line).text;
// Check for static method definition: public/protected/private static function method_name
const static_definition_match = line.match(/\b(?:public|protected|private)\s+static\s+function\s+(\w+)/);
if (static_definition_match) {
return this.create_refactor_actions();
}
// Check for static method call: ClassName::method_name
const static_call_match = line.match(/(\w+)::(\w+)/);
if (static_call_match) {
return this.create_refactor_actions();
}
return undefined;
}
private create_refactor_actions(): vscode.CodeAction[] {
const action = new vscode.CodeAction(
'Global Rename Method',
vscode.CodeActionKind.Refactor
);
action.command = {
command: 'rspade.refactorStaticMethod',
title: 'Global Rename Method'
};
return [action];
}
}

View File

@@ -0,0 +1,336 @@
/**
* RSpade Refactor Provider
*
* Provides context menu refactoring options for PHP static methods.
* Communicates with the server-side refactor commands via the IDE service.
*/
import * as vscode from 'vscode';
import { RspadeFormattingProvider } from './formatting_provider';
export class RspadeRefactorProvider {
private formatting_provider: RspadeFormattingProvider;
private output_channel: vscode.OutputChannel;
constructor(formatting_provider: RspadeFormattingProvider) {
this.formatting_provider = formatting_provider;
this.output_channel = vscode.window.createOutputChannel('RSpade Refactor');
}
/**
* Register the refactor command
*/
public register(context: vscode.ExtensionContext): void {
const command = vscode.commands.registerCommand(
'rspade.refactorStaticMethod',
async () => await this.refactor_static_method()
);
context.subscriptions.push(command);
}
/**
* Check if the refactor command should be available
*/
public should_show_refactor_menu(): boolean {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return false;
}
const document = editor.document;
// Only PHP files
if (document.languageId !== 'php') {
return false;
}
// Must be in ./rsx or ./app/RSpade directory
const file_path = document.uri.fsPath;
if (!file_path.includes('/rsx/') && !file_path.includes('\\rsx\\') &&
!file_path.includes('/app/RSpade') && !file_path.includes('\\app\\RSpade')) {
return false;
}
// Check if cursor is on a static method definition or call
const position = editor.selection.active;
const word_range = document.getWordRangeAtPosition(position);
if (!word_range) {
return false;
}
const word = document.getText(word_range);
const line = document.lineAt(position.line).text;
// Check for static method definition: public/protected/private static function method_name
const static_definition_match = line.match(/\b(?:public|protected|private)\s+static\s+function\s+(\w+)/);
if (static_definition_match) {
const method_name = static_definition_match[1];
// Cursor must be on the method name itself
return word === method_name;
}
// Check for static method call: ClassName::method_name
const static_call_match = line.match(/(\w+)::(\w+)/);
if (static_call_match) {
const method_name = static_call_match[2];
// Cursor must be on the method name itself
return word === method_name;
}
return false;
}
/**
* Main refactor method
*/
private async refactor_static_method(): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
const document = editor.document;
const position = editor.selection.active;
this.output_channel.clear();
this.output_channel.show(true);
this.output_channel.appendLine('=== RSpade Method Refactor ===\n');
try {
// Extract method information from cursor position
const method_info = await this.extract_method_info(document, position);
if (!method_info) {
vscode.window.showErrorMessage('Could not identify static method at cursor position');
return;
}
this.output_channel.appendLine(`Class: ${method_info.class_name}`);
this.output_channel.appendLine(`Method: ${method_info.method_name}\n`);
// Show input dialog for new method name
const new_method_name = await vscode.window.showInputBox({
title: `Global Rename Method: ${method_info.class_name}::${method_info.method_name}`,
prompt: 'Enter new method name:',
placeHolder: 'new_method_name',
value: method_info.method_name,
ignoreFocusOut: true,
validateInput: (value: string) => {
if (!value) {
return 'Method name cannot be empty';
}
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
return 'Method name must be snake_case (lowercase with underscores)';
}
if (value === method_info.method_name) {
return 'New method name must be different from current name';
}
return null;
}
});
if (!new_method_name) {
this.output_channel.appendLine('Refactor cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Confirm refactoring
const confirmation = await vscode.window.showWarningMessage(
`Global Rename: ${method_info.class_name}::${method_info.method_name}${method_info.class_name}::${new_method_name}\n\n` +
'This will rename the method across all usages in all files.',
{ modal: true },
'Rename',
'Cancel'
);
if (confirmation !== 'Rename') {
this.output_channel.appendLine('Global rename cancelled by user');
return;
}
// Save all dirty files first
this.output_channel.appendLine('Checking for unsaved files...');
const dirty_documents = vscode.workspace.textDocuments.filter(doc => doc.isDirty);
if (dirty_documents.length > 0) {
this.output_channel.appendLine(`Found ${dirty_documents.length} unsaved file(s):`);
for (const doc of dirty_documents) {
this.output_channel.appendLine(` - ${doc.fileName}`);
}
this.output_channel.appendLine('\nSaving all files...');
const save_result = await vscode.workspace.saveAll(false);
if (!save_result) {
const error_msg = 'Failed to save all files. Refactor operation aborted.';
this.output_channel.appendLine(`\nERROR: ${error_msg}`);
vscode.window.showErrorMessage(error_msg);
return;
}
this.output_channel.appendLine('All files saved successfully\n');
} else {
this.output_channel.appendLine('No unsaved files\n');
}
// Show output channel
this.output_channel.show(true);
// Show terminal and execute refactoring
this.output_channel.appendLine(`Refactoring ${method_info.class_name}::${method_info.method_name} to ${method_info.class_name}::${new_method_name}...`);
this.output_channel.appendLine('');
const result = await this.execute_refactor(
method_info.class_name,
method_info.method_name,
new_method_name
);
// Display result in terminal
this.output_channel.appendLine('\n=== Refactor Output ===\n');
this.output_channel.appendLine(result);
this.output_channel.appendLine('\n=== Refactor Complete ===');
// Check if refactor was successful
if (result.includes('=== Refactor Complete ===') || result.trim().length > 0) {
// Wait 3.5 seconds total (2s + 1.5s), hide panel, then reload files
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 500ms for filesystem changes to propagate (Samba/network issues)
setTimeout(async () => {
await this.reload_all_open_files();
}, 500);
}, 3500);
vscode.window.showInformationMessage(
`Successfully refactored ${method_info.class_name}::${method_info.method_name} to ${new_method_name}`
);
}
} catch (error: any) {
const error_message = error.message || String(error);
this.output_channel.appendLine(`\nERROR: ${error_message}`);
vscode.window.showErrorMessage(`Refactor failed: ${error_message}`);
}
}
/**
* Extract method information from cursor position
*/
private async extract_method_info(
document: vscode.TextDocument,
position: vscode.Position
): Promise<{ class_name: string; method_name: string } | null> {
const line = document.lineAt(position.line).text;
// Check for static method definition: public/protected/private static function method_name
const definition_match = line.match(/\b(?:public|protected|private)\s+static\s+function\s+(\w+)/);
if (definition_match) {
const method_name = definition_match[1];
// Extract class name from the file
const class_name = await this.extract_class_name(document);
if (class_name) {
return { class_name, method_name };
}
}
// Check for static method call: ClassName::method_name
const static_call_match = line.match(/(\w+)::(\w+)/);
if (static_call_match) {
const class_name = static_call_match[1];
const method_name = static_call_match[2];
return { class_name, method_name };
}
return null;
}
/**
* Extract class name from PHP file
*/
private async extract_class_name(document: vscode.TextDocument): Promise<string | null> {
const text = document.getText();
// Match actual class declaration, not @class in comments
// Look for: class ClassName or abstract class ClassName or final class ClassName
const class_match = text.match(/^\s*(?:abstract\s+|final\s+)?class\s+(\w+)/m);
if (class_match) {
return class_match[1];
}
return null;
}
/**
* Reload all open text documents
*/
private async reload_all_open_files(): Promise<void> {
const text_documents = vscode.workspace.textDocuments;
for (const document of text_documents) {
// Skip untitled documents
if (document.uri.scheme === 'untitled') {
continue;
}
// Skip non-file schemes (git, output channels, etc)
if (document.uri.scheme !== 'file') {
continue;
}
// Get the text editor for this document
const editors = vscode.window.visibleTextEditors.filter(
editor => editor.document.uri.toString() === document.uri.toString()
);
if (editors.length > 0) {
// Document is currently visible, reload it
const position = editors[0].selection.active;
const view_column = editors[0].viewColumn;
// Close and reopen to force reload
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
const new_document = await vscode.workspace.openTextDocument(document.uri);
const editor = await vscode.window.showTextDocument(new_document, view_column);
// Restore cursor position
editor.selection = new vscode.Selection(position, position);
editor.revealRange(new vscode.Range(position, position));
}
}
}
/**
* Execute the refactor command via IDE service
*/
private async execute_refactor(
class_name: string,
old_method: string,
new_method: string
): Promise<string> {
// Ensure authentication
await this.formatting_provider.ensure_auth();
// Prepare request data
const request_data = {
command: 'rsx:refactor:rename_php_class_function',
arguments: [class_name, old_method, new_method]
};
this.output_channel.appendLine('Sending refactor request to server...');
this.output_channel.appendLine(`Command: ${request_data.command}`);
this.output_channel.appendLine(`Arguments: ${JSON.stringify(request_data.arguments)}\n`);
// Make authenticated request
const response = await (this.formatting_provider as any).make_authenticated_request(
'/command',
request_data
);
if (!response.success) {
throw new Error(response.error || 'Refactor command failed');
}
return response.output || 'Refactor completed successfully (no output)';
}
}

View File

@@ -0,0 +1,168 @@
/**
* RSpade Sort Class Methods Provider
*
* Reorganizes methods in PHP class files according to RSpade conventions
*/
import * as vscode from 'vscode';
import * as path from 'path';
import { RspadeFormattingProvider } from './formatting_provider';
export class RspadeSortClassMethodsProvider {
private formatting_provider: RspadeFormattingProvider;
private output_channel: vscode.OutputChannel;
constructor(formatting_provider: RspadeFormattingProvider) {
this.formatting_provider = formatting_provider;
this.output_channel = vscode.window.createOutputChannel('RSpade Sort Methods');
}
/**
* Register the sort command
*/
public register(context: vscode.ExtensionContext): void {
const command = vscode.commands.registerCommand(
'rspade.sortClassMethods',
async (uri?: vscode.Uri) => await this.sort_class_methods(uri)
);
context.subscriptions.push(command);
}
/**
* Main sort method
*/
private async sort_class_methods(uri?: vscode.Uri): Promise<void> {
// Determine file path
let file_path: string;
if (uri) {
// Called from explorer context menu
file_path = uri.fsPath;
} else {
// Called from command palette - use active editor
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active file to sort');
return;
}
file_path = editor.document.uri.fsPath;
}
// Validate it's a PHP file
if (!file_path.endsWith('.php')) {
vscode.window.showErrorMessage('Can only sort PHP class files');
return;
}
this.output_channel.clear();
this.output_channel.show(true);
this.output_channel.appendLine('=== RSpade Sort Class Methods ===\n');
this.output_channel.appendLine(`File: ${file_path}\n`);
try {
// Get workspace root to make path relative
const workspace_folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(file_path));
if (!workspace_folder) {
throw new Error('File is not in workspace');
}
const relative_path = path.relative(workspace_folder.uri.fsPath, file_path);
this.output_channel.appendLine(`Relative path: ${relative_path}\n`);
// Confirm sorting
const confirmation = await vscode.window.showWarningMessage(
`Sort methods in ${path.basename(file_path)}?\n\n` +
'This will reorganize all methods according to RSpade conventions.',
{ modal: true },
'Sort',
'Cancel'
);
if (confirmation !== 'Sort') {
this.output_channel.appendLine('Sort cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
// Execute sort
this.output_channel.appendLine('Sorting methods...\n');
const result = await this.execute_sort(relative_path);
// Display result
this.output_channel.appendLine('\n=== Sort Output ===\n');
this.output_channel.appendLine(result);
this.output_channel.appendLine('\n=== Sort Complete ===');
// Reload the file if it's open
const document = await vscode.workspace.openTextDocument(file_path);
const editors = vscode.window.visibleTextEditors.filter(
editor => editor.document.uri.fsPath === file_path
);
if (editors.length > 0) {
// Wait 3.5 seconds, close panel, then reload
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 500ms for filesystem changes
setTimeout(async () => {
for (const editor of editors) {
const position = editor.selection.active;
const view_column = editor.viewColumn;
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
const new_document = await vscode.workspace.openTextDocument(file_path);
const new_editor = await vscode.window.showTextDocument(new_document, view_column);
// Restore cursor position
new_editor.selection = new vscode.Selection(position, position);
new_editor.revealRange(new vscode.Range(position, position));
}
}, 500);
}, 3500);
} else {
// File not open, just close panel
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
}, 3500);
}
vscode.window.showInformationMessage(`Successfully sorted methods in ${path.basename(file_path)}`);
} catch (error: any) {
const error_message = error.message || String(error);
this.output_channel.appendLine(`\nERROR: ${error_message}`);
vscode.window.showErrorMessage(`Sort failed: ${error_message}`);
}
}
/**
* Execute the sort command via IDE service
*/
private async execute_sort(file_path: string): Promise<string> {
// Ensure authentication
await this.formatting_provider.ensure_auth();
// Prepare request data
const request_data = {
command: 'rsx:refactor:sort_php_class_functions',
arguments: [file_path]
};
this.output_channel.appendLine('Sending sort request to server...');
this.output_channel.appendLine(`Command: ${request_data.command}`);
this.output_channel.appendLine(`Arguments: ${JSON.stringify(request_data.arguments)}\n`);
// Make authenticated request
const response = await (this.formatting_provider as any).make_authenticated_request(
'/command',
request_data
);
if (!response.success) {
throw new Error(response.error || 'Sort command failed');
}
return response.output || 'Sort completed successfully (no output)';
}
}

View File

@@ -0,0 +1,20 @@
{
"name": "Blade JQHTML Components",
"scopeName": "source.jqhtml.injection",
"injectionSelector": "L:text.blade, L:text.html.php",
"patterns": [
{
"comment": "JQHTML Component Tags",
"name": "meta.tag.jqhtml.component",
"match": "(</?)(([A-Z][A-Za-z0-9_]*))(?=[\\s/>])",
"captures": {
"1": {
"name": "punctuation.definition.tag.html"
},
"2": {
"name": "entity.name.tag.component.jqhtml"
}
}
}
]
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "out",
"lib": [
"ES2020"
],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"exclude": [
"node_modules",
".vscode-test"
]
}