// ===== Navigation & Search Module =====
// Wait for editor to be ready
document.addEventListener('DOMContentLoaded', () => {
// Get editor reference from global API
const { editor } = window.editorAPI || {};
if (!editor) {
console.warn('Editor not found, navigation functionality may be limited');
return;
}
// DOM elements
const searchInput = document.getElementById("searchInput");
const searchNext = document.getElementById("searchNext");
const searchPrev = document.getElementById("searchPrev");
const searchType = document.getElementById("searchType");
const selectMode = document.getElementById("selectMode");
// Track current navigation position
let currentIndex = -1;
let currentItems = [];
// ===== Block Range Detection =====
// Get the complete range for a code block using folding
function getBlockRange(row, type) {
const session = editor.getSession();
const doc = session.getDocument();
const lines = doc.getAllLines();
// Try to use Ace's fold widget first
const foldRange = session.getFoldWidgetRange(row);
if (foldRange) {
return {
start: { row: row, column: 0 },
end: { row: foldRange.end.row, column: lines[foldRange.end.row].length }
};
}
// Fallback: smart block detection based on type
switch (type) {
case 'function':
return getFunctionRange(row, lines);
case 'variable':
return getVariableRange(row, lines);
case 'tag':
return getTagRange(row, lines);
case 'css':
return getCSSRange(row, lines);
default:
return getSingleLineRange(row, lines);
}
}
// Get function range (from declaration to closing brace)
function getFunctionRange(startRow, lines) {
const startLine = lines[startRow];
// Find opening brace
let braceRow = startRow;
let braceFound = false;
for (let i = startRow; i < lines.length && i < startRow + 5; i++) {
if (lines[i].includes('{')) {
braceRow = i;
braceFound = true;
break;
}
}
if (!braceFound) {
return getSingleLineRange(startRow, lines);
}
// Find matching closing brace
let braceCount = 0;
let endRow = braceRow;
for (let i = braceRow; i < lines.length; i++) {
const line = lines[i];
braceCount += (line.match(/{/g) || []).length;
braceCount -= (line.match(/}/g) || []).length;
if (braceCount === 0 && i > braceRow) {
endRow = i;
break;
}
}
return {
start: { row: startRow, column: 0 },
end: { row: endRow, column: lines[endRow].length }
};
}
// Get variable range (including initialization)
function getVariableRange(startRow, lines) {
const startLine = lines[startRow];
let endRow = startRow;
// If it's an object/array initialization, find the end
if (startLine.includes('{') || startLine.includes('[')) {
let braceCount = 0;
let bracketCount = 0;
for (let i = startRow; i < lines.length; i++) {
const line = lines[i];
braceCount += (line.match(/{/g) || []).length;
braceCount -= (line.match(/}/g) || []).length;
bracketCount += (line.match(/\[/g) || []).length;
bracketCount -= (line.match(/]/g) || []).length;
if (braceCount === 0 && bracketCount === 0 && (line.includes(';') || line.includes(',') || i === startRow)) {
endRow = i;
break;
}
}
}
return {
start: { row: startRow, column: 0 },
end: { row: endRow, column: lines[endRow].length }
};
}
// Get HTML tag range (opening to closing tag)
function getTagRange(startRow, lines) {
const startLine = lines[startRow].trim();
// Self-closing tag
if (startLine.endsWith('/>') || startLine.includes('/>')) {
return getSingleLineRange(startRow, lines);
}
// Extract tag name
const tagMatch = startLine.match(/<(\w+)/);
if (!tagMatch) return getSingleLineRange(startRow, lines);
const tagName = tagMatch[1];
const closingTag = `</${tagName}>`;
// Find closing tag
for (let i = startRow + 1; i < lines.length; i++) {
if (lines[i].includes(closingTag)) {
return {
start: { row: startRow, column: 0 },
end: { row: i, column: lines[i].length }
};
}
}
return getSingleLineRange(startRow, lines);
}
// Get CSS rule range (selector to closing brace)
function getCSSRange(startRow, lines) {
let endRow = startRow;
let braceCount = 0;
let foundOpenBrace = false;
for (let i = startRow; i < lines.length; i++) {
const line = lines[i];
braceCount += (line.match(/{/g) || []).length;
braceCount -= (line.match(/}/g) || []).length;
if (line.includes('{')) foundOpenBrace = true;
if (foundOpenBrace && braceCount === 0) {
endRow = i;
break;
}
}
return {
start: { row: startRow, column: 0 },
end: { row: endRow, column: lines[endRow].length }
};
}
// Single line range fallback
function getSingleLineRange(row, lines) {
return {
start: { row: row, column: 0 },
end: { row: row, column: lines[row].length }
};
}
// Select a range in the editor
function selectRange(range) {
const Range = ace.require("ace/range").Range;
const aceRange = new Range(range.start.row, range.start.column, range.end.row, range.end.column);
editor.selection.setSelectionRange(aceRange);
}
// ===== Code Element Detection =====
// Get specific code elements based on search type
function getCodeElements(type) {
const session = editor.getSession();
const doc = session.getDocument();
const lines = doc.getAllLines();
const items = [];
switch (type) {
case "functions":
lines.forEach((line, row) => {
const trimmed = line.trim();
// Match various function patterns
if (trimmed.match(/^(function\s+\w+|const\s+\w+\s*=\s*(?:function|\()|let\s+\w+\s*=\s*(?:function|\()|var\s+\w+\s*=\s*(?:function|\()|.*function\s*\(|\w+\s*:\s*function|\w+\s*\([^)]*\)\s*{)/)) {
const name = extractFunctionName(trimmed);
items.push({
row: row,
text: trimmed,
name: name,
type: 'function'
});
}
});
break;
case "variables":
lines.forEach((line, row) => {
const trimmed = line.trim();
// Match variable declarations
const varMatch = trimmed.match(/^(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
if (varMatch && !trimmed.includes('function')) {
items.push({
row: row,
text: trimmed,
name: varMatch[2],
type: 'variable'
});
}
});
break;
case "divs":
lines.forEach((line, row) => {
const trimmed = line.trim();
// Match HTML tags and CSS selectors
const tagMatch = trimmed.match(/<(\w+)(?:\s+[^>]*)?(?:\s+id=["']([^"']+)["'])?(?:\s+class=["']([^"']+)["'])?[^>]*>/);
const cssMatch = trimmed.match(/^([.#]?[\w-]+)\s*[{,]/);
if (tagMatch) {
const tag = tagMatch[1];
const id = tagMatch[2];
const className = tagMatch[3];
let displayName = `<${tag}>`;
if (id) displayName += `#${id}`;
if (className) displayName += `.${className.split(' ')[0]}`;
items.push({
row: row,
text: trimmed,
name: displayName,
type: 'tag'
});
} else if (cssMatch) {
items.push({
row: row,
text: trimmed,
name: cssMatch[1],
type: 'css'
});
}
});
break;
case "normal":
return getTopLevelFolds();
}
return items;
}
// Extract function name from line
function extractFunctionName(line) {
// Try different function patterns
const patterns = [
/function\s+(\w+)/,
/(const|let|var)\s+(\w+)\s*=/,
/(\w+)\s*:\s*function/,
/(\w+)\s*\(/
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
return match[2] || match[1];
}
}
return 'anonymous';
}
// Get top-level foldable ranges (for normal mode)
function getTopLevelFolds() {
const session = editor.getSession();
const foldWidgets = session.foldWidgets;
const folds = [];
if (!foldWidgets) return folds;
const doc = session.getDocument();
const lines = doc.getAllLines();
for (let row = 0; row < lines.length; row++) {
const foldWidget = foldWidgets[row];
if (foldWidget === "start") {
const line = lines[row];
const indent = line.match(/^(\s*)/)[1].length;
if (indent <= 4) {
const range = session.getFoldWidgetRange(row);
if (range) {
folds.push({
row: row,
range: range,
text: line.trim(),
indent: indent,
type: 'fold'
});
}
}
}
}
return folds;
}
// ===== Navigation Logic =====
// Navigate through filtered items (cursor-aware)
function navigateItems(backwards = false) {
const type = searchType.value;
currentItems = getCodeElements(type);
if (currentItems.length === 0) {
searchInput.placeholder = `No ${type} found`;
return;
}
// Get current cursor position
const currentRow = editor.getCursorPosition().row;
// If this is the first navigation or we need to find position relative to cursor
if (currentIndex === -1) {
if (backwards) {
// Find the last item before current cursor position
currentIndex = currentItems.findLastIndex(item => item.row < currentRow);
if (currentIndex < 0) currentIndex = currentItems.length - 1; // Wrap to end
} else {
// Find the first item after current cursor position
currentIndex = currentItems.findIndex(item => item.row > currentRow);
if (currentIndex < 0) currentIndex = 0; // Wrap to beginning
}
} else {
// Continue from current index
if (backwards) {
currentIndex = currentIndex <= 0 ? currentItems.length - 1 : currentIndex - 1;
} else {
currentIndex = currentIndex >= currentItems.length - 1 ? 0 : currentIndex + 1;
}
}
const item = currentItems[currentIndex];
if (selectMode.checked) {
// Select the entire block
const blockRange = getBlockRange(item.row, item.type);
selectRange(blockRange);
editor.scrollToRow(item.row);
} else {
// Just navigate to the line
editor.gotoLine(item.row + 1, 0, true);
editor.scrollToRow(item.row);
highlightLine(item.row);
}
// Update placeholder
const displayText = item.name || item.text.slice(0, 30);
const action = selectMode.checked ? "Select" : "Navigate";
searchInput.placeholder = `${action} ${type} (${currentIndex + 1}/${currentItems.length}): ${displayText}${displayText.length > 30 ? '...' : ''}`;
}
// Highlight a line briefly (only when not selecting)
function highlightLine(row) {
const Range = ace.require("ace/range").Range;
const range = new Range(row, 0, row, 1);
const marker = editor.session.addMarker(range, "ace_active-line", "fullLine");
setTimeout(() => {
editor.session.removeMarker(marker);
}, 1500);
}
// ===== Search & Navigation =====
// Main search/navigate function
function doFind(backwards = false) {
const query = searchInput.value.trim();
if (!query) {
// Empty search: navigate through filtered items
navigateItems(backwards);
editor.focus();
return;
}
// Reset navigation when actually searching
currentIndex = -1;
const type = searchType.value;
if (type === "normal") {
// Regular search
searchInput.placeholder = "Search…";
editor.find(query, {
backwards,
wrap: true,
caseSensitive: false,
wholeWord: false,
regExp: false,
preventScroll: false
});
} else {
// Filtered search within specific elements
currentItems = getCodeElements(type);
const filteredItems = currentItems.filter(item =>
item.text.toLowerCase().includes(query.toLowerCase()) ||
(item.name && item.name.toLowerCase().includes(query.toLowerCase()))
);
if (filteredItems.length === 0) {
searchInput.placeholder = `No ${type} matching "${query}"`;
return;
}
// Find current position in filtered results
const currentRow = editor.getCursorPosition().row;
let startIndex = 0;
if (backwards) {
startIndex = filteredItems.findLastIndex(item => item.row < currentRow);
if (startIndex < 0) startIndex = filteredItems.length - 1;
} else {
startIndex = filteredItems.findIndex(item => item.row > currentRow);
if (startIndex < 0) startIndex = 0;
}
const item = filteredItems[startIndex];
if (selectMode.checked) {
// Select the found block
const blockRange = getBlockRange(item.row, item.type);
selectRange(blockRange);
editor.scrollToRow(item.row);
} else {
// Just navigate
editor.gotoLine(item.row + 1, 0, true);
highlightLine(item.row);
}
const displayText = item.name || item.text.slice(0, 30);
const action = selectMode.checked ? "Selected" : "Found";
searchInput.placeholder = `${action} ${type}: ${displayText}`;
}
editor.focus();
}
// Update placeholder when search type or select mode changes
function updatePlaceholder() {
const type = searchType.value;
currentIndex = -1; // Reset navigation to start from cursor next time
if (searchInput.value.trim() === "") {
const action = selectMode.checked ? "select" : "navigate";
searchInput.placeholder = `Search ${type} or ${action} with arrows…`;
}
}
// ===== Event Listeners =====
searchType.addEventListener("change", updatePlaceholder);
selectMode.addEventListener("change", updatePlaceholder);
searchInput.addEventListener("input", () => {
if (searchInput.value.trim() === "") {
updatePlaceholder();
} else {
const type = searchType.value;
searchInput.placeholder = `Search ${type}…`;
}
});
searchNext.addEventListener("click", () => doFind(false));
searchPrev.addEventListener("click", () => doFind(true));
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
doFind(e.shiftKey);
}
});
// Initialize placeholder
updatePlaceholder();
// ===== Export Navigation API =====
window.navigationAPI = {
doFind,
navigateItems,
getCodeElements,
updatePlaceholder
};
console.log("Navigation module loaded successfully");
});