diff --git a/Latest/advanced-search.js b/Latest/advanced-search.js new file mode 100644 index 0000000..f2be810 --- /dev/null +++ b/Latest/advanced-search.js @@ -0,0 +1,598 @@ +/** + * Advanced Search Overlay for Neptune SDK Browser + * Provides comprehensive search across classes and members with ranking and fuzzy matching + */ + +(function() { + 'use strict'; + + // Search indexes + let classIndex = []; + let memberIndex = []; + let isIndexBuilt = false; + + // UI state + let focusedIndex = -1; + let currentResults = []; + const MAX_RESULTS = 250; + const FUZZY_THRESHOLD = 15; + + // Performance tracking + let indexBuildTime = 0; + + /** + * Build search indexes from Classes data + */ + function buildIndex(Classes) { + const startTime = performance.now(); + console.log('[AdvancedSearch] Building search index...'); + + classIndex = []; + memberIndex = []; + + Object.keys(Classes).forEach(className => { + const classData = Classes[className]; + + // Add to class index + classIndex.push({ + name: className, + lower: className.toLowerCase() + }); + + // Add members to member index + if (classData.m && Array.isArray(classData.m)) { + classData.m.forEach(member => { + if (member.n) { + memberIndex.push({ + className: className, + name: member.n, + lower: member.n.toLowerCase(), + type: member.t || '', + typeLower: (member.t || '').toLowerCase(), + offset: member.o || '', + offsetLower: (member.o || '').toLowerCase() + }); + } + }); + } + }); + + indexBuildTime = performance.now() - startTime; + isIndexBuilt = true; + console.log(`[AdvancedSearch] Index built in ${indexBuildTime.toFixed(2)}ms`); + console.log(`[AdvancedSearch] Classes: ${classIndex.length}, Members: ${memberIndex.length}`); + } + + /** + * Initialize the advanced search system + */ + function initIndex(Classes) { + if (!Classes || Object.keys(Classes).length === 0) { + console.warn('[AdvancedSearch] No Classes data available'); + return; + } + buildIndex(Classes); + setupEventListeners(); + } + + /** + * Setup event listeners for the overlay + */ + function setupEventListeners() { + const overlay = document.getElementById('AdvancedSearchOverlay'); + const closeBtn = document.getElementById('ASClose'); + const queryInput = document.getElementById('ASQuery'); + const classesCheckbox = document.getElementById('ASClasses'); + const membersCheckbox = document.getElementById('ASMembers'); + const typeSearchCheckbox = document.getElementById('ASTypeSearch'); + + if (!overlay || !closeBtn || !queryInput) { + console.warn('[AdvancedSearch] Required elements not found'); + return; + } + + // Close button + closeBtn.addEventListener('click', closeAdvancedSearch); + + // Close on overlay click (outside panel) + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + closeAdvancedSearch(); + } + }); + + // Close on ESC key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && overlay.style.display === 'flex') { + closeAdvancedSearch(); + } + }); + + // Search input + queryInput.addEventListener('input', debounce(() => { + performSearch(); + }, 150)); + + // Keyboard navigation in search input + queryInput.addEventListener('keydown', (e) => { + handleKeyboardNavigation(e); + }); + + // Filter checkboxes + if (classesCheckbox) { + classesCheckbox.addEventListener('change', performSearch); + } + if (membersCheckbox) { + membersCheckbox.addEventListener('change', performSearch); + } + if (typeSearchCheckbox) { + typeSearchCheckbox.addEventListener('change', performSearch); + } + + console.log('[AdvancedSearch] Event listeners setup complete'); + } + + /** + * Open the advanced search overlay + */ + function openAdvancedSearch() { + const overlay = document.getElementById('AdvancedSearchOverlay'); + const queryInput = document.getElementById('ASQuery'); + + if (!overlay) return; + + overlay.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + + if (queryInput) { + queryInput.focus(); + queryInput.select(); + } + + // Clear previous search + focusedIndex = -1; + performSearch(); + } + + /** + * Close the advanced search overlay + */ + function closeAdvancedSearch() { + const overlay = document.getElementById('AdvancedSearchOverlay'); + const queryInput = document.getElementById('ASQuery'); + const topSearchInput = document.getElementById('TopSearchInput'); + + if (!overlay) return; + + overlay.style.display = 'none'; + document.body.style.overflow = ''; + + if (queryInput) { + queryInput.value = ''; + } + + // Clear results + const resultsDiv = document.getElementById('ASResults'); + if (resultsDiv) { + resultsDiv.innerHTML = ''; + } + + // Restore focus + if (topSearchInput) { + topSearchInput.focus(); + } + } + + /** + * Perform the search operation + */ + function performSearch() { + const startTime = performance.now(); + + const query = document.getElementById('ASQuery')?.value.trim() || ''; + const searchClasses = document.getElementById('ASClasses')?.checked || false; + const searchMembers = document.getElementById('ASMembers')?.checked || false; + const typeSearch = document.getElementById('ASTypeSearch')?.checked || false; + + if (!query) { + renderResults([]); + updateStatus('Enter a search term', 0); + return; + } + + const queryLower = query.toLowerCase(); + let results = []; + + // Search classes + if (searchClasses) { + classIndex.forEach(item => { + const score = scoreMatch(item.lower, queryLower); + if (score > 0) { + results.push({ + type: 'class', + name: item.name, + score: score, + query: query + }); + } + }); + } + + // Search members + if (searchMembers) { + memberIndex.forEach(item => { + let score = 0; + let matchField = ''; + + if (typeSearch) { + // Type search mode + score = scoreMatch(item.typeLower, queryLower); + matchField = 'type'; + } else { + // Name search mode + score = scoreMatch(item.lower, queryLower); + matchField = 'name'; + + // Also check offset for hex matches + const offsetScore = scoreOffsetMatch(item.offsetLower, queryLower); + if (offsetScore > score) { + score = offsetScore; + matchField = 'offset'; + } + } + + if (score > 0) { + results.push({ + type: 'member', + className: item.className, + name: item.name, + typeName: item.type, + offset: item.offset, + score: score, + matchField: matchField, + query: query + }); + } + }); + } + + // Fuzzy search fallback if results are too few + if (results.length < FUZZY_THRESHOLD) { + if (searchClasses) { + classIndex.forEach(item => { + // Skip if already matched + if (results.some(r => r.type === 'class' && r.name === item.name)) { + return; + } + + const distance = levenshtein(item.lower, queryLower); + if (distance <= 2 && distance < queryLower.length) { + results.push({ + type: 'class', + name: item.name, + score: 20 - distance * 5, // Lower score for fuzzy matches + query: query, + fuzzy: true + }); + } + }); + } + + if (searchMembers && !typeSearch) { + memberIndex.forEach(item => { + // Skip if already matched + if (results.some(r => r.type === 'member' && r.name === item.name && r.className === item.className)) { + return; + } + + const distance = levenshtein(item.lower, queryLower); + if (distance <= 2 && distance < queryLower.length) { + results.push({ + type: 'member', + className: item.className, + name: item.name, + typeName: item.type, + offset: item.offset, + score: 15 - distance * 5, + matchField: 'name', + query: query, + fuzzy: true + }); + } + }); + } + } + + // Sort by score (descending) then by type priority (class > member) + results.sort((a, b) => { + if (b.score !== a.score) return b.score - a.score; + if (a.type === 'class' && b.type === 'member') return -1; + if (a.type === 'member' && b.type === 'class') return 1; + return 0; + }); + + // Limit results + results = results.slice(0, MAX_RESULTS); + + const queryTime = performance.now() - startTime; + currentResults = results; + focusedIndex = -1; + + renderResults(results); + updateStatus(`${results.length} result${results.length !== 1 ? 's' : ''}`, queryTime); + } + + /** + * Score a match between text and query + */ + function scoreMatch(text, query) { + if (text === query) return 100; // Exact match + if (text.startsWith(query)) return 80; // Prefix match + if (text.includes(query)) return 50; // Substring match + return 0; + } + + /** + * Score a match for offset (hex values) + */ + function scoreOffsetMatch(offset, query) { + if (!offset || !query) return 0; + + // Remove '0x' prefix for comparison + const cleanOffset = offset.replace(/^0x/i, '').toLowerCase(); + const cleanQuery = query.replace(/^0x/i, '').toLowerCase(); + + if (cleanOffset === cleanQuery) return 90; // Exact hex match + if (cleanOffset.includes(cleanQuery)) return 40; // Substring hex match + return 0; + } + + /** + * Render search results + */ + function renderResults(results) { + const resultsDiv = document.getElementById('ASResults'); + if (!resultsDiv) return; + + if (results.length === 0) { + resultsDiv.innerHTML = '