Fix IDE service routing and path normalization Refactor IDE services and add session rotation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
763 lines
40 KiB
JavaScript
Executable File
763 lines
40 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 () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.JqhtmlHoverProvider = exports.JqhtmlDefinitionProvider = void 0;
|
|
const vscode = __importStar(require("vscode"));
|
|
const componentIndex_1 = require("./componentIndex");
|
|
/**
|
|
* JQHTML Definition Provider
|
|
*
|
|
* Provides "Go to Definition" functionality for JQHTML components.
|
|
* When a user Ctrl/Cmd+clicks on a component name or uses F12,
|
|
* this provider will locate the component definition.
|
|
*/
|
|
class JqhtmlDefinitionProvider {
|
|
constructor(componentIndex) {
|
|
this.componentIndex = componentIndex;
|
|
}
|
|
provideDefinition(document, position, token) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
console.log(`\n========== JQHTML: provideDefinition called ==========`);
|
|
console.log(`File: ${document.uri.fsPath}`);
|
|
console.log(`Position: Line ${position.line + 1}, Character ${position.character}`);
|
|
const line = document.lineAt(position.line).text;
|
|
console.log(`JQHTML: Line text: "${line}"`);
|
|
// Check if we're in a $ attribute with unquoted value
|
|
const dollarAttrResult = this.checkDollarAttributeContext(document, position, line);
|
|
if (dollarAttrResult) {
|
|
console.log(`JQHTML: In $ attribute context:`, dollarAttrResult);
|
|
return yield this.handleDollarAttributeDefinition(document, position, dollarAttrResult);
|
|
}
|
|
// IMPORTANT: Check for slot syntax BEFORE extracting word
|
|
// This prevents slot names from being treated as component names
|
|
// Check if we're in a slot tag by looking for <# or </# before cursor
|
|
const beforeCursor = line.substring(0, position.character);
|
|
if (beforeCursor.match(/<\/?#\s*[A-Z][A-Za-z0-9_]*$/)) {
|
|
// We're in a slot tag - extract the full slot name from the line
|
|
const slotNameMatch = line.match(/<\/?#\s*([A-Z][A-Za-z0-9_]*)/);
|
|
if (slotNameMatch) {
|
|
const slotName = slotNameMatch[1];
|
|
console.log(`JQHTML: Detected slot tag syntax for slot: ${slotName}`);
|
|
return yield this.handleSlotDefinition(document, position, slotName);
|
|
}
|
|
}
|
|
// Get the word at the cursor position
|
|
const wordRange = document.getWordRangeAtPosition(position, /[A-Z][A-Za-z0-9_]*/);
|
|
if (!wordRange) {
|
|
console.log(`JQHTML: No word range found at position`);
|
|
return undefined;
|
|
}
|
|
const word = document.getText(wordRange);
|
|
console.log(`JQHTML: Word at cursor: "${word}"`);
|
|
// Check if this looks like a component reference
|
|
if (!componentIndex_1.JqhtmlComponentIndex.isComponentReference(word)) {
|
|
console.log(`JQHTML: "${word}" is not a component reference (doesn't start with capital)`);
|
|
return undefined;
|
|
}
|
|
// line already declared at top of function
|
|
const charBefore = wordRange.start.character > 0 ?
|
|
line.charAt(wordRange.start.character - 1) : '';
|
|
// Check if this word is inside an extends="" attribute value
|
|
let beforeWord = line.substring(0, wordRange.start.character);
|
|
if (beforeWord.match(/extends\s*=\s*["']?\s*$/)) {
|
|
console.log(`JQHTML: "${word}" is in extends attribute, treating as component reference`);
|
|
// This is in extends="ComponentName", treat as component reference
|
|
const componentDef = this.componentIndex.findComponent(word);
|
|
if (!componentDef) {
|
|
console.log(`JQHTML: Component '${word}' not found in index`);
|
|
return undefined;
|
|
}
|
|
// Verify the file still exists
|
|
try {
|
|
yield vscode.workspace.fs.stat(componentDef.uri);
|
|
}
|
|
catch (error) {
|
|
console.log(`JQHTML: Component '${word}' definition file no longer exists`);
|
|
this.componentIndex.reindexWorkspace();
|
|
return undefined;
|
|
}
|
|
console.log(`JQHTML: Found definition for '${word}' at ${componentDef.uri.fsPath}:${componentDef.position.line + 1}`);
|
|
return new vscode.Location(componentDef.uri, componentDef.position);
|
|
}
|
|
// Check if this word is in a tag context
|
|
// Look for < before the component name (accounting for Define: prefix)
|
|
let isInTagContext = false;
|
|
// Check for opening tag: <ComponentName or <Define:ComponentName
|
|
beforeWord = line.substring(0, wordRange.start.character);
|
|
if (beforeWord.match(/<\s*$/) || beforeWord.match(/<Define:\s*$/)) {
|
|
isInTagContext = true;
|
|
}
|
|
// Check for closing tag: </ComponentName or </Define:ComponentName
|
|
if (beforeWord.match(/<\/\s*$/) || beforeWord.match(/<\/Define:\s*$/)) {
|
|
isInTagContext = true;
|
|
}
|
|
if (!isInTagContext) {
|
|
// Also check if cursor is inside the tag name (not in attributes)
|
|
const afterWord = line.substring(wordRange.end.character);
|
|
// If there's a space or > after the word, and < before it somewhere
|
|
if ((afterWord.match(/^[\s>]/) || afterWord.length === 0) && beforeWord.includes('<')) {
|
|
// Verify we're not in an attribute value
|
|
const lastLessThan = beforeWord.lastIndexOf('<');
|
|
const lastGreaterThan = beforeWord.lastIndexOf('>');
|
|
if (lastLessThan > lastGreaterThan) {
|
|
isInTagContext = true;
|
|
}
|
|
}
|
|
}
|
|
if (!isInTagContext) {
|
|
console.log(`JQHTML: "${word}" not in tag context, ignoring`);
|
|
return undefined;
|
|
}
|
|
console.log(`JQHTML: "${word}" IS in tag context, looking up in index...`);
|
|
console.log(`JQHTML: Current index size: ${this.componentIndex.getAllComponentNames().length} components`);
|
|
console.log(`JQHTML: Index contains: ${this.componentIndex.getAllComponentNames().join(', ')}`);
|
|
// Look up the component in our index
|
|
const componentDef = this.componentIndex.findComponent(word);
|
|
if (!componentDef) {
|
|
// Component not found in index
|
|
//
|
|
// DIAGNOSTIC HISTORY:
|
|
// - Issue: Go to Definition goes to random CSS file instead of component
|
|
// - Symptom: Component correctly identified (e.g., "Contacts_Datagrid")
|
|
// Tag context correctly detected
|
|
// BUT findComponent() returns undefined
|
|
// - Root cause: Component not indexed by the indexing system
|
|
//
|
|
// POSSIBLE REASONS:
|
|
// 1. Component file not in workspace or not discovered during indexing
|
|
// 2. Component definition syntax not matching regex in componentIndex.ts
|
|
// 3. File watcher didn't detect the file creation/modification
|
|
// 4. Index hasn't run yet (extension just activated)
|
|
//
|
|
// WHEN THIS RETURNS UNDEFINED:
|
|
// VS Code falls back to built-in text search providers, which may find
|
|
// the component name in CSS class names (.Contacts_Datagrid), causing
|
|
// navigation to wrong files.
|
|
//
|
|
// NEXT DIAGNOSTIC STEPS:
|
|
// 1. Check if component file exists and is in workspace
|
|
// 2. Verify Define tag syntax matches indexing regex
|
|
// 3. Check file watcher is working (create new component, see if indexed)
|
|
// 4. Manually trigger reindex and check if component appears
|
|
console.log(`JQHTML: Component '${word}' not found in index`);
|
|
console.log(`JQHTML: RETURNING UNDEFINED - VS Code may fall back to other definition providers!`);
|
|
return undefined;
|
|
}
|
|
// Verify the file still exists (catches stale index entries)
|
|
try {
|
|
yield vscode.workspace.fs.stat(componentDef.uri);
|
|
}
|
|
catch (error) {
|
|
// File no longer exists - trigger reindex and return undefined
|
|
console.log(`JQHTML: Component '${word}' definition file no longer exists: ${componentDef.uri.fsPath}`);
|
|
console.log(`JQHTML: Triggering workspace reindex...`);
|
|
this.componentIndex.reindexWorkspace(); // Async, non-blocking
|
|
return undefined;
|
|
}
|
|
console.log(`JQHTML: Found definition for '${word}' at ${componentDef.uri.fsPath}:${componentDef.position.line + 1}`);
|
|
console.log(`JQHTML: RETURNING LOCATION - This should be the ONLY result!`);
|
|
console.log(`========== JQHTML: provideDefinition done ==========\n`);
|
|
// Return the location of the component definition
|
|
return new vscode.Location(componentDef.uri, componentDef.position);
|
|
});
|
|
}
|
|
/**
|
|
* Check if cursor is in a $ attribute with unquoted value like $handler=Controller.method
|
|
* Returns the parsed segments and position info, or undefined if not in such context
|
|
*/
|
|
checkDollarAttributeContext(document, position, line) {
|
|
var _a;
|
|
const char = position.character;
|
|
const beforeCursor = line.substring(0, char);
|
|
const afterCursor = line.substring(char);
|
|
// Look for pattern: $attributeName=FirstSegment.secondSegment
|
|
// Match: $ followed by word, =, then identifier chains
|
|
const dollarAttrMatch = beforeCursor.match(/\$\w+\s*=\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)$/);
|
|
if (!dollarAttrMatch) {
|
|
return undefined;
|
|
}
|
|
// Get the full expression (before cursor + until space or >)
|
|
const expressionBeforeCursor = dollarAttrMatch[1];
|
|
const expressionAfterCursor = ((_a = afterCursor.match(/^([a-zA-Z0-9_$]*)/)) === null || _a === void 0 ? void 0 : _a[1]) || '';
|
|
const fullExpression = expressionBeforeCursor + expressionAfterCursor;
|
|
console.log(`JQHTML: Full $ attribute expression: "${fullExpression}"`);
|
|
// Split by dots
|
|
const segments = fullExpression.split('.');
|
|
// Determine which segment we're on based on cursor position
|
|
const expressionStartChar = char - expressionBeforeCursor.length;
|
|
const cursorOffsetInExpression = char - expressionStartChar;
|
|
let currentSegmentIndex = 0;
|
|
let charCount = 0;
|
|
for (let i = 0; i < segments.length; i++) {
|
|
const segmentLength = segments[i].length;
|
|
if (cursorOffsetInExpression <= charCount + segmentLength) {
|
|
currentSegmentIndex = i;
|
|
break;
|
|
}
|
|
charCount += segmentLength + 1; // +1 for the dot
|
|
}
|
|
console.log(`JQHTML: Cursor on segment ${currentSegmentIndex}: "${segments[currentSegmentIndex]}"`);
|
|
// Handle "this" keyword - resolve to containing Define component
|
|
let className = segments[0];
|
|
if (className === 'this') {
|
|
const containingComponent = this.findContainingDefineComponent(document, position.line);
|
|
if (!containingComponent) {
|
|
console.log(`JQHTML: "this" used but no containing Define component found`);
|
|
return undefined;
|
|
}
|
|
className = containingComponent;
|
|
console.log(`JQHTML: Resolved "this" to component: ${className}`);
|
|
}
|
|
if (currentSegmentIndex === 0) {
|
|
// First segment - just the class name
|
|
return { className, isFirstSegment: true };
|
|
}
|
|
else {
|
|
// Second or later segment - class + member
|
|
return {
|
|
className,
|
|
memberName: segments[currentSegmentIndex],
|
|
isFirstSegment: false
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Find the containing <Define:ComponentName> for a given line
|
|
*/
|
|
findContainingDefineComponent(document, currentLine) {
|
|
// Search backwards from current line to find <Define:ComponentName>
|
|
for (let i = currentLine; i >= 0; i--) {
|
|
const lineText = document.lineAt(i).text;
|
|
const defineMatch = lineText.match(/<Define:([A-Z][A-Za-z0-9_]*)/);
|
|
if (defineMatch) {
|
|
return defineMatch[1];
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
/**
|
|
* Handle goto definition for $ attribute values
|
|
*
|
|
* SEARCH PRIORITY MATRIX:
|
|
*
|
|
* Single Segment (e.g., $handler=Controller):
|
|
* 1. PHP class
|
|
* 2. JS class
|
|
* 3. Standalone JS function (only if first segment is not "this")
|
|
*
|
|
* Multiple Segments - First Segment (e.g., $handler=Controller.method, click on "Controller"):
|
|
* 1. PHP class
|
|
* 2. JS class (if no PHP class found, or if first segment is "this")
|
|
* 3. Standalone JS function (only if not "this")
|
|
*
|
|
* Multiple Segments - Second+ Segment (e.g., $handler=Controller.method, click on "method"):
|
|
* 1. PHP method in PHP class → Fall back to PHP class if method not found
|
|
* 2. JS method in JS class → Fall back to JS class if method not found (if no PHP class found, or if "this")
|
|
* (No standalone function search for second+ segments)
|
|
*
|
|
* Special Case: "this" keyword
|
|
* - Resolves to containing <Define:ComponentName>
|
|
* - Only searches JS (PHP search skipped)
|
|
*/
|
|
handleDollarAttributeDefinition(document, position, context) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
console.log(`JQHTML: Looking for class "${context.className}"${context.memberName ? `, member "${context.memberName}"` : ''}`);
|
|
const isThisKeyword = context.className === 'this';
|
|
// Priority 1: Search PHP classes/methods (skip if "this" keyword)
|
|
if (!isThisKeyword) {
|
|
const phpResult = yield this.searchPhpDefinition(context);
|
|
if (phpResult) {
|
|
return phpResult;
|
|
}
|
|
}
|
|
// Priority 2: Search JS classes/methods
|
|
const jsClassResult = yield this.searchJsClassDefinition(context);
|
|
if (jsClassResult) {
|
|
return jsClassResult;
|
|
}
|
|
// Priority 3: Search standalone JS functions (only for single segment, not "this")
|
|
if (context.isFirstSegment && !context.memberName && !isThisKeyword) {
|
|
const jsFunctionResult = yield this.searchStandaloneJsFunction(context.className);
|
|
if (jsFunctionResult) {
|
|
return jsFunctionResult;
|
|
}
|
|
}
|
|
console.log(`JQHTML: No definition found for "${context.className}"${context.memberName ? `.${context.memberName}` : ''}`);
|
|
return undefined;
|
|
});
|
|
}
|
|
/**
|
|
* Search for PHP class and optionally method
|
|
*
|
|
* IMPLEMENTATION: Uses VS Code's workspace symbol provider API to query Intelephense's symbol index.
|
|
*
|
|
* This approach:
|
|
* - Leverages Intelephense's existing indexed symbol database
|
|
* - No temporary documents or window management issues
|
|
* - Fast symbol lookup via vscode.executeWorkspaceSymbolProvider
|
|
* - Gracefully falls back if Intelephense not installed
|
|
* - No manual indexing or file scanning required
|
|
*
|
|
* We query workspace symbols by name (class or method), then filter results
|
|
* to find PHP symbols and navigate to their definitions.
|
|
*/
|
|
searchPhpDefinition(context) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
try {
|
|
// Check if Intelephense is installed
|
|
const intelephenseExt = vscode.extensions.getExtension('bmewburn.vscode-intelephense-client');
|
|
if (!intelephenseExt) {
|
|
console.log(`JQHTML: Intelephense extension not installed, skipping PHP lookup`);
|
|
return undefined;
|
|
}
|
|
// Search for the class first
|
|
console.log(`JQHTML: Searching workspace symbols for PHP class: ${context.className}`);
|
|
const classSymbols = yield vscode.commands.executeCommand('vscode.executeWorkspaceSymbolProvider', context.className);
|
|
if (!classSymbols || classSymbols.length === 0) {
|
|
console.log(`JQHTML: No workspace symbols found for class: ${context.className}`);
|
|
return undefined;
|
|
}
|
|
// Filter to PHP class symbols
|
|
const phpClassSymbol = classSymbols.find(s => s.name === context.className &&
|
|
s.kind === vscode.SymbolKind.Class &&
|
|
s.location.uri.fsPath.endsWith('.php'));
|
|
if (!phpClassSymbol) {
|
|
console.log(`JQHTML: No PHP class symbol found for: ${context.className}`);
|
|
return undefined;
|
|
}
|
|
console.log(`JQHTML: Found PHP class ${context.className} in ${phpClassSymbol.location.uri.fsPath}`);
|
|
// If we're looking for the class itself (first segment, no member)
|
|
if (context.isFirstSegment && !context.memberName) {
|
|
return phpClassSymbol.location;
|
|
}
|
|
// If we're looking for a method within the class
|
|
if (context.memberName) {
|
|
console.log(`JQHTML: Searching for method: ${context.memberName} in class ${context.className}`);
|
|
// Search for the method name in workspace symbols
|
|
const methodSymbols = yield vscode.commands.executeCommand('vscode.executeWorkspaceSymbolProvider', context.memberName);
|
|
// Filter to methods in the same PHP file as the class
|
|
const phpMethodSymbol = methodSymbols === null || methodSymbols === void 0 ? void 0 : methodSymbols.find(s => s.name === context.memberName &&
|
|
s.kind === vscode.SymbolKind.Method &&
|
|
s.location.uri.toString() === phpClassSymbol.location.uri.toString());
|
|
if (phpMethodSymbol) {
|
|
console.log(`JQHTML: Found PHP method ${context.memberName} in ${phpMethodSymbol.location.uri.fsPath}`);
|
|
return phpMethodSymbol.location;
|
|
}
|
|
// Method not found - fall back to class definition
|
|
console.log(`JQHTML: Method ${context.memberName} not found, falling back to class definition`);
|
|
return phpClassSymbol.location;
|
|
}
|
|
return phpClassSymbol.location;
|
|
}
|
|
catch (error) {
|
|
console.error(`JQHTML: Error using workspace symbol provider:`, error);
|
|
return undefined;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Search for JS class and optionally method
|
|
*/
|
|
searchJsClassDefinition(context) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const jsFiles = yield vscode.workspace.findFiles('**/*.js', '**/node_modules/**');
|
|
for (const fileUri of jsFiles) {
|
|
const fileDoc = yield vscode.workspace.openTextDocument(fileUri);
|
|
const fileText = fileDoc.getText();
|
|
// Look for class definition: class ClassName
|
|
const classRegex = new RegExp(`^\\s*(?:export\\s+)?class\\s+${context.className}\\b`, 'm');
|
|
const classMatch = classRegex.exec(fileText);
|
|
if (classMatch) {
|
|
console.log(`JQHTML: Found JS class ${context.className} in ${fileUri.fsPath}`);
|
|
// If we're on the first segment, go to the class definition
|
|
if (context.isFirstSegment && !context.memberName) {
|
|
const classPos = fileDoc.positionAt(classMatch.index + classMatch[0].indexOf(context.className));
|
|
return new vscode.Location(fileUri, classPos);
|
|
}
|
|
// If we're on a later segment, try to find the method/property in the class
|
|
if (context.memberName) {
|
|
const memberLocation = this.findJsClassMember(fileDoc, fileText, classMatch.index, context.memberName);
|
|
if (memberLocation) {
|
|
console.log(`JQHTML: Found JS method ${context.memberName} in class ${context.className}`);
|
|
return new vscode.Location(fileUri, memberLocation);
|
|
}
|
|
// Method not found, but we found the class - fall back to class definition
|
|
console.log(`JQHTML: JS method ${context.memberName} not found, falling back to JS class definition`);
|
|
const classPos = fileDoc.positionAt(classMatch.index + classMatch[0].indexOf(context.className));
|
|
return new vscode.Location(fileUri, classPos);
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
/**
|
|
* Search for standalone JS function (not in a class)
|
|
* Only called for single-segment expressions where first segment is not "this"
|
|
*/
|
|
searchStandaloneJsFunction(functionName) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const jsFiles = yield vscode.workspace.findFiles('**/*.js', '**/node_modules/**');
|
|
for (const fileUri of jsFiles) {
|
|
const fileDoc = yield vscode.workspace.openTextDocument(fileUri);
|
|
const fileText = fileDoc.getText();
|
|
// Look for function declaration: function functionName
|
|
// or const/let/var functionName = function
|
|
const functionRegex = new RegExp(`^\\s*(?:export\\s+)?(?:async\\s+)?function\\s+${functionName}\\b`, 'm');
|
|
const functionMatch = functionRegex.exec(fileText);
|
|
if (functionMatch) {
|
|
console.log(`JQHTML: Found standalone JS function ${functionName} in ${fileUri.fsPath}`);
|
|
const functionPos = fileDoc.positionAt(functionMatch.index + functionMatch[0].indexOf(functionName));
|
|
return new vscode.Location(fileUri, functionPos);
|
|
}
|
|
// Also check for: const functionName = ...
|
|
const constFunctionRegex = new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var)\\s+${functionName}\\s*=`, 'm');
|
|
const constMatch = constFunctionRegex.exec(fileText);
|
|
if (constMatch) {
|
|
console.log(`JQHTML: Found standalone JS function ${functionName} (const/let/var) in ${fileUri.fsPath}`);
|
|
const functionPos = fileDoc.positionAt(constMatch.index + constMatch[0].indexOf(functionName));
|
|
return new vscode.Location(fileUri, functionPos);
|
|
}
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
/**
|
|
* Find a method or property within a JS class definition
|
|
*/
|
|
findJsClassMember(document, fileText, classStartIndex, memberName) {
|
|
// Find the class body (starts at { after class declaration)
|
|
const classBodyStart = fileText.indexOf('{', classStartIndex);
|
|
if (classBodyStart === -1)
|
|
return undefined;
|
|
// Find matching closing brace
|
|
let braceCount = 1;
|
|
let classBodyEnd = classBodyStart + 1;
|
|
while (classBodyEnd < fileText.length && braceCount > 0) {
|
|
if (fileText[classBodyEnd] === '{')
|
|
braceCount++;
|
|
if (fileText[classBodyEnd] === '}')
|
|
braceCount--;
|
|
classBodyEnd++;
|
|
}
|
|
const classBody = fileText.substring(classBodyStart, classBodyEnd);
|
|
// Look for method: methodName() { or property: methodName =
|
|
const methodRegex = new RegExp(`^\\s*(?:async\\s+)?${memberName}\\s*[=(]`, 'm');
|
|
const methodMatch = methodRegex.exec(classBody);
|
|
if (methodMatch) {
|
|
const absoluteIndex = classBodyStart + methodMatch.index + methodMatch[0].indexOf(memberName);
|
|
return document.positionAt(absoluteIndex);
|
|
}
|
|
return undefined;
|
|
}
|
|
/**
|
|
* Handle goto definition for slot tags (<#SlotName>)
|
|
*
|
|
* IMPLEMENTATION SCOPE (Narrow, for now):
|
|
* - Handles direct extends="ComponentName" on <Define:> tags
|
|
* - Handles direct <ComponentName> invocation tags
|
|
* - Does NOT traverse full inheritance chain (TODO: add later)
|
|
* - Just looks for direct parent component
|
|
*
|
|
* LOGIC:
|
|
* 1. Extract slot name from cursor position
|
|
* 2. Find parent component:
|
|
* - If inside <Define extends="Parent">, use Parent
|
|
* - If inside <Parent> invocation, use Parent
|
|
* 3. Find Parent.jqhtml file
|
|
* 4. Search for <%= content('SlotName') %>
|
|
* 5. Navigate to that line
|
|
*/
|
|
handleSlotDefinition(document, position, slotName) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
console.log(`JQHTML: Handling slot definition for: ${slotName}`);
|
|
// Find the parent component that defines this slot
|
|
const parentComponentName = this.findParentComponentForSlot(document, position);
|
|
if (!parentComponentName) {
|
|
console.log(`JQHTML: Could not determine parent component for slot`);
|
|
return undefined;
|
|
}
|
|
console.log(`JQHTML: Parent component for slot: ${parentComponentName}`);
|
|
// Debug: Show what's in the index
|
|
const allComponents = this.componentIndex.getAllComponentNames();
|
|
console.log(`JQHTML: Index currently contains ${allComponents.length} components:`, allComponents.join(', '));
|
|
// Find the parent component definition file
|
|
const parentComponent = this.componentIndex.findComponent(parentComponentName);
|
|
if (!parentComponent) {
|
|
console.log(`JQHTML: Parent component '${parentComponentName}' not found in index`);
|
|
return undefined;
|
|
}
|
|
console.log(`JQHTML: Found parent component file: ${parentComponent.uri.fsPath}`);
|
|
// Search for content('SlotName') in the parent component file
|
|
const slotUsageLocation = yield this.findSlotUsageInTemplate(parentComponent.uri, slotName);
|
|
if (!slotUsageLocation) {
|
|
console.log(`JQHTML: Slot usage content('${slotName}') not found in ${parentComponent.uri.fsPath}`);
|
|
return undefined;
|
|
}
|
|
console.log(`JQHTML: Found slot usage at line ${slotUsageLocation.range.start.line + 1}`);
|
|
return slotUsageLocation;
|
|
});
|
|
}
|
|
/**
|
|
* Find the parent component that should define this slot
|
|
*
|
|
* Looks for either:
|
|
* 1. <Define:ChildComponent extends="ParentComponent"> - check if slots are top-level
|
|
* 2. <ParentComponent> - find enclosing component invocation tag
|
|
*/
|
|
findParentComponentForSlot(document, position) {
|
|
const currentLine = position.line;
|
|
// Strategy 1: Look for <Define extends="ParentComponent"> where slots are at top level
|
|
// Scan upward to find the Define tag
|
|
let defineTagStartLine = -1;
|
|
for (let i = currentLine; i >= 0; i--) {
|
|
const lineText = document.lineAt(i).text;
|
|
// Check if we found a <Define:ComponentName
|
|
if (lineText.match(/<Define:([A-Z][A-Za-z0-9_]*)/)) {
|
|
defineTagStartLine = i;
|
|
console.log(`JQHTML: Found <Define: tag at line ${i + 1}`);
|
|
break;
|
|
}
|
|
}
|
|
// If we found a Define tag, look for extends attribute in the tag (may be multi-line)
|
|
if (defineTagStartLine >= 0) {
|
|
// Collect all lines from Define tag start until we find the closing >
|
|
let tagContent = '';
|
|
for (let i = defineTagStartLine; i < document.lineCount; i++) {
|
|
const lineText = document.lineAt(i).text;
|
|
tagContent += lineText + ' ';
|
|
// Stop when we find the closing > of the opening tag
|
|
if (lineText.includes('>')) {
|
|
break;
|
|
}
|
|
}
|
|
// Now check if this multi-line tag has extends attribute
|
|
const extendsMatch = tagContent.match(/\bextends\s*=\s*["']([A-Z][A-Za-z0-9_]*)["']/);
|
|
if (extendsMatch) {
|
|
const parentComponentName = extendsMatch[1];
|
|
console.log(`JQHTML: Found extends="${parentComponentName}" in Define tag`);
|
|
// TODO: Verify that the slot is at top level (not nested inside other tags)
|
|
// For now, we assume if we found a Define with extends, that's the parent
|
|
return parentComponentName;
|
|
}
|
|
else {
|
|
console.log(`JQHTML: Define tag found but no extends attribute`);
|
|
}
|
|
}
|
|
// Strategy 2: Look for enclosing <ParentComponent> invocation tag
|
|
// Scan upward to find opening tag
|
|
let tagStack = [];
|
|
for (let i = currentLine; i >= 0; i--) {
|
|
const lineText = document.lineAt(i).text;
|
|
// Find all component tags on this line (both opening and closing)
|
|
// Component tags: <ComponentName> or </ComponentName>
|
|
const tagRegex = /<\/?([A-Z][A-Za-z0-9_]*)[^>]*>/g;
|
|
let match;
|
|
// Collect all tags on this line
|
|
const tagsOnLine = [];
|
|
while ((match = tagRegex.exec(lineText)) !== null) {
|
|
const fullMatch = match[0];
|
|
const componentName = match[1];
|
|
const isClosing = fullMatch.startsWith('</');
|
|
tagsOnLine.push({ tag: componentName, isClosing });
|
|
}
|
|
// Process tags in reverse order (right to left on the line)
|
|
for (let j = tagsOnLine.length - 1; j >= 0; j--) {
|
|
const { tag, isClosing } = tagsOnLine[j];
|
|
if (isClosing) {
|
|
// Closing tag - add to stack
|
|
tagStack.push(tag);
|
|
}
|
|
else {
|
|
// Opening tag
|
|
if (tagStack.length > 0 && tagStack[tagStack.length - 1] === tag) {
|
|
// This opening tag matches the last closing tag on stack - they cancel out
|
|
tagStack.pop();
|
|
}
|
|
else {
|
|
// This is an unclosed opening tag - this is our parent!
|
|
console.log(`JQHTML: Found enclosing component invocation: <${tag}>`);
|
|
return tag;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
console.log(`JQHTML: No parent component found for slot`);
|
|
return undefined;
|
|
}
|
|
/**
|
|
* Search for <%= content('SlotName') %> in a template file
|
|
*/
|
|
findSlotUsageInTemplate(templateUri, slotName) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
try {
|
|
const templateDoc = yield vscode.workspace.openTextDocument(templateUri);
|
|
const templateText = templateDoc.getText();
|
|
// Search for content('SlotName') or content("SlotName")
|
|
// Also handle optional whitespace
|
|
const contentRegex = new RegExp(`<%=\\s*content\\s*\\(\\s*['"]${slotName}['"]\\s*\\)`, 'g');
|
|
const match = contentRegex.exec(templateText);
|
|
if (match) {
|
|
const matchPosition = templateDoc.positionAt(match.index);
|
|
console.log(`JQHTML: Found content('${slotName}') at line ${matchPosition.line + 1}`);
|
|
// Return location pointing to the slot name within content('SlotName')
|
|
const slotNameStartIndex = match.index + match[0].indexOf(slotName);
|
|
const slotNamePosition = templateDoc.positionAt(slotNameStartIndex);
|
|
const slotNameRange = new vscode.Range(slotNamePosition, new vscode.Position(slotNamePosition.line, slotNamePosition.character + slotName.length));
|
|
return new vscode.Location(templateUri, slotNameRange);
|
|
}
|
|
console.log(`JQHTML: No content('${slotName}') found in template`);
|
|
return undefined;
|
|
}
|
|
catch (error) {
|
|
console.error(`JQHTML: Error reading template file:`, error);
|
|
return undefined;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
exports.JqhtmlDefinitionProvider = JqhtmlDefinitionProvider;
|
|
/**
|
|
* JQHTML Hover Provider
|
|
*
|
|
* Provides hover information for JQHTML components.
|
|
* Shows the file and line where the component is defined.
|
|
*/
|
|
class JqhtmlHoverProvider {
|
|
constructor(componentIndex) {
|
|
this.componentIndex = componentIndex;
|
|
}
|
|
provideHover(document, position, token) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const line = document.lineAt(position.line).text;
|
|
const char = position.character;
|
|
// Check for $redrawable attribute
|
|
const redrawableMatch = line.match(/\$redrawable(?=\s|>|\/)/);
|
|
if (redrawableMatch && line.indexOf('$redrawable') <= char && char <= line.indexOf('$redrawable') + '$redrawable'.length) {
|
|
const markdown = new vscode.MarkdownString();
|
|
markdown.appendMarkdown(`**\`$redrawable\` Attribute**\n\n`);
|
|
markdown.appendMarkdown(`Converts this tag into an anonymous component class, allowing it to be redrawn on demand.\n\n`);
|
|
markdown.appendMarkdown(`**Usage:**\n\`\`\`javascript\nthis.$id('element_id').render()\n\`\`\`\n\n`);
|
|
markdown.appendMarkdown(`Call \`render()\` on the element's scoped ID to trigger a re-render of just this element without affecting the rest of the component.`);
|
|
const wordRange = new vscode.Range(new vscode.Position(position.line, line.indexOf('$redrawable')), new vscode.Position(position.line, line.indexOf('$redrawable') + '$redrawable'.length));
|
|
return new vscode.Hover(markdown, wordRange);
|
|
}
|
|
// Check for tag="" attribute on components (Define tags or component invocations)
|
|
// Look backwards from cursor to find if we're in a tag="" attribute
|
|
const beforeCursor = line.substring(0, char);
|
|
const afterCursor = line.substring(char);
|
|
// Check if we're hovering over "tag" attribute name or its value
|
|
const tagAttrMatch = beforeCursor.match(/<(Define:[A-Z][A-Za-z0-9_]*|[A-Z][A-Za-z0-9_]*)[^>]*\btag\s*=\s*["']?(\w*)$/);
|
|
if (tagAttrMatch) {
|
|
// We're in or near a tag attribute
|
|
const markdown = new vscode.MarkdownString();
|
|
markdown.appendMarkdown(`**\`tag\` Attribute**\n\n`);
|
|
markdown.appendMarkdown(`Sets the HTML element type for this component.\n\n`);
|
|
markdown.appendMarkdown(`**Default:** \`div\`\n\n`);
|
|
markdown.appendMarkdown(`**Examples:**\n`);
|
|
markdown.appendMarkdown(`- \`tag="button"\` - Creates a \`<button>\` element\n`);
|
|
markdown.appendMarkdown(`- \`tag="span"\` - Creates a \`<span>\` element\n`);
|
|
markdown.appendMarkdown(`- \`tag="a"\` - Creates an \`<a>\` element\n\n`);
|
|
markdown.appendMarkdown(`Use this when your component should be a specific HTML element instead of the default \`<div>\`.`);
|
|
return new vscode.Hover(markdown);
|
|
}
|
|
// Original component hover logic
|
|
// Get the word at the cursor 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 a component reference
|
|
if (!componentIndex_1.JqhtmlComponentIndex.isComponentReference(word)) {
|
|
return undefined;
|
|
}
|
|
// Verify this is in a tag context (same logic as definition provider)
|
|
// line already declared at top of function
|
|
const beforeWord = line.substring(0, wordRange.start.character);
|
|
let isInTagContext = false;
|
|
if (beforeWord.match(/<\s*$/) || beforeWord.match(/<Define:\s*$/) ||
|
|
beforeWord.match(/<\/\s*$/) || beforeWord.match(/<\/Define:\s*$/)) {
|
|
isInTagContext = true;
|
|
}
|
|
if (!isInTagContext) {
|
|
const afterWord = line.substring(wordRange.end.character);
|
|
if ((afterWord.match(/^[\s>]/) || afterWord.length === 0) && beforeWord.includes('<')) {
|
|
const lastLessThan = beforeWord.lastIndexOf('<');
|
|
const lastGreaterThan = beforeWord.lastIndexOf('>');
|
|
if (lastLessThan > lastGreaterThan) {
|
|
isInTagContext = true;
|
|
}
|
|
}
|
|
}
|
|
if (!isInTagContext) {
|
|
return undefined;
|
|
}
|
|
// Look up the component in our index
|
|
const componentDef = this.componentIndex.findComponent(word);
|
|
if (!componentDef) {
|
|
// Show that component is not defined
|
|
const markdown = new vscode.MarkdownString();
|
|
markdown.appendMarkdown(`**JQHTML Component:** \`${word}\`\n\n`);
|
|
markdown.appendMarkdown(`⚠️ *Component definition not found in workspace*`);
|
|
return new vscode.Hover(markdown, wordRange);
|
|
}
|
|
// Create hover content
|
|
const markdown = new vscode.MarkdownString();
|
|
markdown.appendMarkdown(`**JQHTML Component:** \`${word}\`\n\n`);
|
|
// Show file location
|
|
const relativePath = vscode.workspace.asRelativePath(componentDef.uri);
|
|
markdown.appendMarkdown(`📁 **Defined in:** \`${relativePath}:${componentDef.position.line + 1}\`\n\n`);
|
|
// Show the definition line
|
|
if (componentDef.line) {
|
|
markdown.appendCodeblock(componentDef.line, 'jqhtml');
|
|
}
|
|
// Make the file path clickable
|
|
markdown.isTrusted = true;
|
|
return new vscode.Hover(markdown, wordRange);
|
|
});
|
|
}
|
|
}
|
|
exports.JqhtmlHoverProvider = JqhtmlHoverProvider;
|
|
//# sourceMappingURL=definitionProvider.js.map
|