Files
rspade_system/app/RSpade/resource/vscode_extension/out/definition_provider.js
root f6fac6c4bc 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>
2025-10-21 02:08:33 +00:00

571 lines
25 KiB
JavaScript
Executable File

"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